Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions internal/cmd/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
package auth

import (
"fmt"

"github.com/spf13/cobra"

customerrors "go.datum.net/datumctl/internal/errors"
)

// Command creates the base "auth" command and adds subcommands for login,
// logout, token retrieval, etc.
// movedCommands maps subcommands that used to live under "auth" to their
// current top-level spelling. Login and logout were promoted to top-level
// commands in the context-discovery redesign (#149); users following older
// docs or muscle memory still reach for "datumctl auth login".
var movedCommands = map[string]string{
"login": "datumctl login",
"logout": "datumctl logout",
}

// Command creates the base "auth" command and adds subcommands for session
// management, token retrieval, and kubectl integration.
func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Expand Down Expand Up @@ -33,6 +46,13 @@ Advanced — kubectl integration:

# Log out a specific account
datumctl logout user@example.com`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
return unknownSubcommandError(args[0])
},
}

cmd.AddCommand(
Expand All @@ -44,3 +64,21 @@ Advanced — kubectl integration:

return cmd
}

// unknownSubcommandError turns an unrecognized "datumctl auth <name>" into a
// hard error instead of a silently-successful help dump. Names that were moved
// to the top level get a hint pointing at the replacement command.
func unknownSubcommandError(name string) error {
if replacement, moved := movedCommands[name]; moved {
return &customerrors.UserError{
Message: fmt.Sprintf("'datumctl auth %s' is now '%s'", name, replacement),
Hint: fmt.Sprintf("Login and logout are top-level commands — run '%s'.", replacement),
Code: "AUTH_COMMAND_MOVED",
}
}
return &customerrors.UserError{
Message: fmt.Sprintf("unknown command %q for 'datumctl auth'", name),
Hint: "Run 'datumctl auth --help' for available commands.",
Code: "UNKNOWN_SUBCOMMAND",
}
}
87 changes: 87 additions & 0 deletions internal/cmd/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package auth

import (
"bytes"
"strings"
"testing"

customerrors "go.datum.net/datumctl/internal/errors"
)

func TestAuthUnknownSubcommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
wantCode string
wantHint string
}{
{
name: "bare auth shows help",
args: []string{},
},
{
name: "moved login errors with top-level hint",
args: []string{"login"},
wantErr: true,
wantCode: "AUTH_COMMAND_MOVED",
wantHint: "datumctl login",
},
{
name: "moved logout errors with top-level hint",
args: []string{"logout"},
wantErr: true,
wantCode: "AUTH_COMMAND_MOVED",
wantHint: "datumctl logout",
},
{
name: "unknown subcommand errors generically",
args: []string{"bogus"},
wantErr: true,
wantCode: "UNKNOWN_SUBCOMMAND",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cmd := Command()
cmd.SetArgs(tc.args)
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})

err := cmd.Execute()

if !tc.wantErr {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return
}

if err == nil {
t.Fatal("expected error, got nil")
}
userErr, ok := customerrors.IsUserError(err)
if !ok {
t.Fatalf("expected *UserError, got %T: %v", err, err)
}
if userErr.Code != tc.wantCode {
t.Errorf("Code = %q, want %q", userErr.Code, tc.wantCode)
}
if tc.wantHint != "" && !strings.Contains(userErr.Hint, tc.wantHint) {
t.Errorf("Hint = %q, want it to contain %q", userErr.Hint, tc.wantHint)
}
})
}
}

func TestAuthValidSubcommandStillRoutes(t *testing.T) {
cmd := Command()
sub, _, err := cmd.Find([]string{"list"})
if err != nil {
t.Fatalf("Find(list) returned error: %v", err)
}
if sub.Name() != "list" {
t.Fatalf("expected to resolve the 'list' subcommand, got %q", sub.Name())
}
}