| index 3c3f98f69..fb7579a7a 100644 |
| --- a/apimode_test.go |
| +++ b/apimode_test.go |
| @@ -2778,3 +2778,147 @@ func TestAPISessionCreateAppSendsPlace(t *testing.T) { |
| t.Fatalf("empty place must not travel as a non-empty value; got %q", gotPlace) |
| } |
| } |
| + |
| +// accountsConsole is a console stub for the omitted--account resolution |
| +// tests: it serves GET /api/cli/apps with the given status and body, and |
| +// counts how often the endpoint is consulted. |
| +func accountsConsole(t *testing.T, status int, body string) (*apiSession, *int) { |
| + t.Helper() |
| + hits := 0 |
| + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + if r.URL.Path != "/api/cli/apps" || r.Method != http.MethodGet { |
| + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) |
| + http.NotFound(w, r) |
| + return |
| + } |
| + hits++ |
| + w.WriteHeader(status) |
| + io.WriteString(w, body) |
| + })) |
| + t.Cleanup(ts.Close) |
| + return newTestSession(ts.URL), &hits |
| +} |
| + |
| +func TestDefaultAccountSingleMembership(t *testing.T) { |
| + useTempCredentials(t) |
| + out := captureStdout(t) |
| + s, hits := accountsConsole(t, http.StatusOK, |
| + `{"accounts":[{"sqid":"bdk","name":"tom@keymail.dev","role":"owner","apps":[]}]}`) |
| + |
| + a, err := s.defaultAccount(context.Background()) |
| + if err != nil || a.Seg != "bdk" || !a.Exact { |
| + t.Fatalf("one membership = %+v, %v; want its sqid, exact", a, err) |
| + } |
| + if *hits != 1 { |
| + t.Fatalf("endpoint hits = %d, want 1", *hits) |
| + } |
| + if !strings.Contains(out.String(), "account: tom@keymail.dev (bdk)") { |
| + t.Fatalf("the resolved account must be printed; got:\n%s", out.String()) |
| + } |
| +} |
| + |
| +func TestDefaultAccountSeveralNonTTY(t *testing.T) { |
| + useTempCredentials(t) |
| + captureStdout(t) |
| + s, _ := accountsConsole(t, http.StatusOK, |
| + `{"accounts":[{"sqid":"bdk","name":"tom@keymail.dev"},{"sqid":"bce","name":"acme"}]}`) |
| + |
| + // Under go test stdin is not a terminal, so several memberships take |
| + // the non-interactive refusal — which names every account. |
| + _, err := s.defaultAccount(context.Background()) |
| + want := "you belong to several accounts — say which with -account: tom@keymail.dev (bdk), acme (bce)" |
| + if err == nil || err.Error() != want { |
| + t.Fatalf("several = %v\nwant %q", err, want) |
| + } |
| +} |
| + |
| +func TestDefaultAccountOldConsoleFallsBackToOps(t *testing.T) { |
| + useTempCredentials(t) |
| + captureStdout(t) |
| + s, _ := accountsConsole(t, http.StatusNotFound, "404 page not found\n") |
| + |
| + a, err := s.defaultAccount(context.Background()) |
| + if err != nil || a.Seg != "ops" || a.Exact { |
| + t.Fatalf("old console = %+v, %v; want the pre-discovery ops default", a, err) |
| + } |
| +} |
| + |
| +func TestDefaultAccountZeroMemberships(t *testing.T) { |
| + useTempCredentials(t) |
| + captureStdout(t) |
| + s, _ := accountsConsole(t, http.StatusOK, `{"accounts":[]}`) |
| + |
| + if _, err := s.defaultAccount(context.Background()); err == nil || |
| + !strings.Contains(err.Error(), s.console) { |
| + t.Fatalf("zero memberships = %v, want an error naming the console", err) |
| + } |
| +} |
| + |
| +func TestDefaultAccountAuthErrorSurfaces(t *testing.T) { |
| + useTempCredentials(t) |
| + captureStdout(t) |
| + s, _ := accountsConsole(t, http.StatusUnauthorized, `{"error":"token expired"}`) |
| + |
| + // A 401 is an answer about the TOKEN, not about memberships — it must |
| + // surface, never turn into an ops fallback that then 404s confusingly. |
| + if _, err := s.defaultAccount(context.Background()); err == nil || |
| + !strings.Contains(err.Error(), "token expired") { |
| + t.Fatalf("401 = %v, want the console's own diagnosis", err) |
| + } |
| +} |
| + |
| +func TestDefaultAccountConfigDefault(t *testing.T) { |
| + useTempCredentials(t) |
| + out := captureStdout(t) |
| + s, hits := accountsConsole(t, http.StatusOK, `{"accounts":[]}`) |
| + |
| + // A config account paired with THIS console is honored without asking |
| + // the console anything. |
| + cfgPath := writeConfig(t, ".", `{"console": "`+s.console+`", "account": "acme"}`) |
| + a, err := s.defaultAccount(context.Background()) |
| + if err != nil || a.Seg != "acme" || a.Exact { |
| + t.Fatalf("config account = %+v, %v; want the name, resolved server-side", a, err) |
| + } |
| + if *hits != 0 { |
| + t.Fatalf("endpoint hits = %d, want 0 — config answered it", *hits) |
| + } |
| + if !strings.Contains(out.String(), "acme") || !strings.Contains(out.String(), cfgPath) { |
| + t.Fatalf("the config-sourced account and its file must be printed; got:\n%s", out.String()) |
| + } |
| + |
| + // A config account with NO console is a pure default: honored too. |
| + writeConfig(t, ".", `{"account": "acme"}`) |
| + if a, err = s.defaultAccount(context.Background()); err != nil || a.Seg != "acme" { |
| + t.Fatalf("consoleless config account = %+v, %v", a, err) |
| + } |
| + |
| + // Paired with a DIFFERENT console it is ignored — and with zero |
| + // memberships behind it, resolution reports those, proving the walk |
| + // fell through to the endpoint. |
| + writeConfig(t, ".", `{"console": "https://other.example", "account": "acme"}`) |
| + if _, err = s.defaultAccount(context.Background()); err == nil || |
| + !strings.Contains(err.Error(), "no account") { |
| + t.Fatalf("mismatched config console = %v, want fall-through to memberships", err) |
| + } |
| + if *hits != 1 { |
| + t.Fatalf("endpoint hits = %d, want exactly the fall-through call", *hits) |
| + } |
| +} |
| + |
| +func TestResolvedAccountExplicitFlagsBypass(t *testing.T) { |
| + useTempCredentials(t) |
| + captureStdout(t) |
| + s, hits := accountsConsole(t, http.StatusOK, `{"accounts":[]}`) |
| + |
| + raw, rawID := "acme", "" |
| + if a := resolvedAccount(s, &raw, &rawID); a.Seg != "acme" || a.Exact { |
| + t.Fatalf("-account acme = %+v", a) |
| + } |
| + raw, rawID = "", "bdk" |
| + if a := resolvedAccount(s, &raw, &rawID); a.Seg != "bdk" || !a.Exact { |
| + t.Fatalf("-account-id bdk = %+v", a) |
| + } |
| + if *hits != 0 { |
| + t.Fatalf("explicit flags must never consult the endpoint; hits = %d", *hits) |
| + } |
| +} |