| index bb8a50c74..6ace3ddac 100644 |
| --- a/auth_test.go |
| +++ b/auth_test.go |
| @@ -182,13 +182,14 @@ func TestPollBoundsAreEnforced(t *testing.T) { |
| // fakeConsole answers the CLI endpoints the way the real console does, so the |
| // login loop can be driven end to end without a browser. |
| type fakeConsole struct { |
| - ts *httptest.Server |
| - polls atomic.Int32 |
| - approved atomic.Bool |
| - denied atomic.Bool |
| - revoked atomic.Bool |
| - lastAuth atomic.Value // string: the bearer the last request carried |
| - label atomic.Value // string |
| + ts *httptest.Server |
| + polls atomic.Int32 |
| + approved atomic.Bool |
| + denied atomic.Bool |
| + revoked atomic.Bool |
| + noAccounts atomic.Bool // serve 404 on GET /api/cli/apps: a pre-5856dc5 console |
| + lastAuth atomic.Value // string: the bearer the last request carried |
| + label atomic.Value // string |
| } |
| |
| func newFakeConsole(t *testing.T) *fakeConsole { |
| @@ -237,6 +238,21 @@ func newFakeConsole(t *testing.T) *fakeConsole { |
| "expires_at": time.Now().Add(90 * 24 * time.Hour).UTC().Format(time.RFC3339), |
| }) |
| }) |
| + mux.HandleFunc("GET /api/cli/apps", func(w http.ResponseWriter, r *http.Request) { |
| + if fc.noAccounts.Load() { |
| + http.NotFound(w, r) |
| + return |
| + } |
| + if r.Header.Get("Authorization") != "Bearer cli-token" { |
| + w.WriteHeader(http.StatusUnauthorized) |
| + json.NewEncoder(w).Encode(map[string]string{"error": "not signed in"}) |
| + return |
| + } |
| + json.NewEncoder(w).Encode(map[string]any{"accounts": []map[string]any{ |
| + {"sqid": "bab", "name": "paul@keymail.dev", "role": "owner", "apps": []string{}}, |
| + {"sqid": "bce", "name": "acme", "role": "member", "apps": []string{"shop"}}, |
| + }}) |
| + }) |
| mux.HandleFunc("POST /auth/cli/revoke", func(w http.ResponseWriter, r *http.Request) { |
| if r.Header.Get("Authorization") != "Bearer cli-token" { |
| w.WriteHeader(http.StatusUnauthorized) |
| @@ -325,6 +341,41 @@ func TestAuthLoginStoresACredential(t *testing.T) { |
| } |
| } |
| |
| +// whoami closes the loop #157 opened: who am I, AND what can I act on. |
| +// Memberships come from GET /api/cli/apps; a console too old to answer |
| +// (bare 404) must not make whoami look broken — identity prints, the |
| +// bonus line is simply absent. |
| +func TestAuthWhoamiPrintsMemberships(t *testing.T) { |
| + useTempCredentials(t) |
| + fc := newFakeConsole(t) |
| + var out bytes.Buffer |
| + if err := authLogin(&out, []string{"-console", fc.ts.URL}); err != nil { |
| + t.Fatalf("authLogin: %v", err) |
| + } |
| + |
| + out.Reset() |
| + if err := authWhoami(&out, []string{"-console", fc.ts.URL}); err != nil { |
| + t.Fatalf("authWhoami: %v", err) |
| + } |
| + if !strings.Contains(out.String(), "accounts: paul@keymail.dev (bab), acme (bce)") { |
| + t.Fatalf("whoami must list memberships:\n%s", out.String()) |
| + } |
| + |
| + // Same command against a console that predates the endpoint: identity |
| + // only, silently — no error, no accounts line. |
| + fc.noAccounts.Store(true) |
| + out.Reset() |
| + if err := authWhoami(&out, []string{"-console", fc.ts.URL}); err != nil { |
| + t.Fatalf("authWhoami against an old console: %v", err) |
| + } |
| + if !strings.Contains(out.String(), "paul@keymail.dev at "+fc.ts.URL) { |
| + t.Fatalf("identity line missing:\n%s", out.String()) |
| + } |
| + if strings.Contains(out.String(), "accounts:") { |
| + t.Fatalf("old console must degrade silently:\n%s", out.String()) |
| + } |
| +} |
| + |
| func TestAuthLoginDenied(t *testing.T) { |
| useTempCredentials(t) |
| fc := newFakeConsole(t) |