package main import ( "strings" "testing" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // The load-bearing invariant behind the account-default branch (#157): the // flag default flipped from "ops" to "" for every dual-mode verb, and // direct mode survives ONLY because resolveAccount reads "" as ops — with // no registry involved, since CI and fresh boxes have none. func TestResolveAccountEmptyMeansOps(t *testing.T) { for _, want := range []string{"", "ops", " ops ", " "} { if got := resolveAccount("/nonexistent/registry.db", want, ""); got != release.OpsAccount { t.Errorf("resolveAccount(%q) = %q, want %q", want, got, release.OpsAccount) } } } func TestPickAccount(t *testing.T) { accts := []registry.Account{ {ID: 1, Sqid: "bab", Name: "ops"}, {ID: 2, Sqid: "bah", Name: "amadan"}, {ID: 3, Sqid: "bad", Name: "amadan"}, {ID: 4, Sqid: "baf", Name: "bah"}, // name collides with #2's sqid } if got, err := pickAccount(accts, "bab"); err != nil || got != "bab" { t.Errorf("sqid: %q, %v", got, err) } if got, err := pickAccount(accts, "ops"); err != nil || got != "bab" { t.Errorf("unique name: %q, %v", got, err) } if got, err := pickAccount(accts, "OPS"); err != nil || got != "bab" { t.Errorf("case-insensitive name: %q, %v", got, err) } // A sqid nobody has materialized passes through — a fresh box may not // have reconciled, and CI has no registry at all. if got, err := pickAccount(accts, "zzz"); err != nil || got != "zzz" { t.Errorf("passthrough sqid: %q, %v", got, err) } if _, err := pickAccount(accts, "not-a-sqid-or-name"); err == nil { t.Error("unresolvable non-sqid name must error") } // The two ambiguity shapes: a shared name, and a name/sqid collision. // Both must refuse, and the fix-up named must be -account-id. for _, want := range []string{"amadan", "bah"} { _, err := pickAccount(accts, want) if err == nil || !strings.Contains(err.Error(), "-account-id") { t.Errorf("pickAccount(%q) = %v, want ambiguity error naming --account-id", want, err) } } if _, err := pickAccount(accts, "bah"); err == nil || !strings.Contains(err.Error(), "bah") || !strings.Contains(err.Error(), "baf") { t.Error("name/sqid conflict error must list both candidates") } } // TestAccountIDShape pins the gate BOTH modes share (checkAccountID, which // resolveAccount and apiAccount each call). The values are the ones that // matter: "ops" and "oil" pass release.ValidAccount (^[a-z]{3,16}$) and must // still be refused, because the frozen encoder has no i, l or o and so can // never emit them for any id — a phantom account, written to and served by // nobody. "bab" is real; "zzz" is unminted-but-shaped and must pass, since // -account-id is the flag you reach for on a box that hasn't reconciled. // // The refusals themselves are log.Fatal (os.Exit, unobservable in-process), // so they are pinned two ways instead: the predicate directly here, and the // whole `carlos add -account-id ops` invocation end to end in // TestAddAccountIDRefusesAName (e2e_test.go), which asserts the non-zero // exit and that no row was written. func TestAccountIDShape(t *testing.T) { for _, want := range []string{"bab", "zzz", "baf"} { if !domains.ValidSqid(want) { t.Errorf("--account-id %q must be accepted — it is shaped like something Sqid emits", want) } // The accept path IS reachable in-process: only a refusal exits. empty, id := "", want if a := apiAccount(&empty, &id); a.Seg != want || !a.Exact { t.Errorf("apiAccount(--account-id %q) = %+v, want {%s true}", want, a, want) } } for _, bad := range []string{"ops", "oil", "ill", "ba", "BAB", "ba1", ""} { if domains.ValidSqid(bad) { t.Errorf("--account-id %q must be refused — it is a name, not a sqid", bad) } } // The specific trap the reviewer caught: release.ValidAccount would have // waved "ops" straight through. If this ever stops being true, the two // predicates have converged and the gate is doing nothing. if !release.ValidAccount("ops") { t.Error("release.ValidAccount no longer accepts \"ops\" — this gate's whole reason to exist has moved") } } func TestApiAccountExact(t *testing.T) { acct, id := "ops", "bah" a := apiAccount(&acct, &id) if a.Seg != "bah" || !a.Exact { t.Errorf("apiAccount with --account-id = %+v, want {bah true}", a) } id = "" a = apiAccount(&acct, &id) if a.Seg != "ops" || a.Exact { t.Errorf("apiAccount without --account-id = %+v, want {ops false}", a) } // Both flags are trimmed the way resolveAccount trims them, so a value // that picked up whitespace from a shell means the same thing in either // mode — API mode used to send " ops " to the console verbatim. padded, id := " ops ", " bah " if a := apiAccount(&padded, &id); a.Seg != "bah" || !a.Exact { t.Errorf("apiAccount(padded --account-id) = %+v, want {bah true}", a) } id = " " if a := apiAccount(&padded, &id); a.Seg != "ops" || a.Exact { t.Errorf("apiAccount(padded --account, blank --account-id) = %+v, want {ops false}", a) } if p := apiPath(apiAcct{Seg: "bah", Exact: true}, "web", "/channels"); !strings.HasSuffix(p, "?exact=sqid") { t.Errorf("apiPath exact = %q, want ?exact=sqid suffix", p) } if p := apiPath(apiAcct{Seg: "ops"}, "web", "/channels"); strings.Contains(p, "exact") { t.Errorf("apiPath non-exact = %q, must carry no exact param", p) } }