carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/carlosframework/platform/internal/domains"
8 "github.com/carlosframework/platform/internal/registry"
9 "github.com/carlosframework/platform/internal/release"
10)
11
12// The load-bearing invariant behind the account-default branch (#157): the
13// flag default flipped from "ops" to "" for every dual-mode verb, and
14// direct mode survives ONLY because resolveAccount reads "" as ops — with
15// no registry involved, since CI and fresh boxes have none.
16func TestResolveAccountEmptyMeansOps(t *testing.T) {
17 for _, want := range []string{"", "ops", " ops ", " "} {
18 if got := resolveAccount("/nonexistent/registry.db", want, ""); got != release.OpsAccount {
19 t.Errorf("resolveAccount(%q) = %q, want %q", want, got, release.OpsAccount)
20 }
21 }
22}
23
24func TestPickAccount(t *testing.T) {
25 accts := []registry.Account{
26 {ID: 1, Sqid: "bab", Name: "ops"},
27 {ID: 2, Sqid: "bah", Name: "amadan"},
28 {ID: 3, Sqid: "bad", Name: "amadan"},
29 {ID: 4, Sqid: "baf", Name: "bah"}, // name collides with #2's sqid
30 }
31 if got, err := pickAccount(accts, "bab"); err != nil || got != "bab" {
32 t.Errorf("sqid: %q, %v", got, err)
33 }
34 if got, err := pickAccount(accts, "ops"); err != nil || got != "bab" {
35 t.Errorf("unique name: %q, %v", got, err)
36 }
37 if got, err := pickAccount(accts, "OPS"); err != nil || got != "bab" {
38 t.Errorf("case-insensitive name: %q, %v", got, err)
39 }
40 // A sqid nobody has materialized passes through — a fresh box may not
41 // have reconciled, and CI has no registry at all.
42 if got, err := pickAccount(accts, "zzz"); err != nil || got != "zzz" {
43 t.Errorf("passthrough sqid: %q, %v", got, err)
44 }
45 if _, err := pickAccount(accts, "not-a-sqid-or-name"); err == nil {
46 t.Error("unresolvable non-sqid name must error")
47 }
48 // The two ambiguity shapes: a shared name, and a name/sqid collision.
49 // Both must refuse, and the fix-up named must be -account-id.
50 for _, want := range []string{"amadan", "bah"} {
51 _, err := pickAccount(accts, want)
52 if err == nil || !strings.Contains(err.Error(), "-account-id") {
53 t.Errorf("pickAccount(%q) = %v, want ambiguity error naming --account-id", want, err)
54 }
55 }
56 if _, err := pickAccount(accts, "bah"); err == nil ||
57 !strings.Contains(err.Error(), "bah") || !strings.Contains(err.Error(), "baf") {
58 t.Error("name/sqid conflict error must list both candidates")
59 }
60}
61
62// TestAccountIDShape pins the gate BOTH modes share (checkAccountID, which
63// resolveAccount and apiAccount each call). The values are the ones that
64// matter: "ops" and "oil" pass release.ValidAccount (^[a-z]{3,16}$) and must
65// still be refused, because the frozen encoder has no i, l or o and so can
66// never emit them for any id — a phantom account, written to and served by
67// nobody. "bab" is real; "zzz" is unminted-but-shaped and must pass, since
68// -account-id is the flag you reach for on a box that hasn't reconciled.
69//
70// The refusals themselves are log.Fatal (os.Exit, unobservable in-process),
71// so they are pinned two ways instead: the predicate directly here, and the
72// whole `carlos add -account-id ops` invocation end to end in
73// TestAddAccountIDRefusesAName (e2e_test.go), which asserts the non-zero
74// exit and that no row was written.
75func TestAccountIDShape(t *testing.T) {
76 for _, want := range []string{"bab", "zzz", "baf"} {
77 if !domains.ValidSqid(want) {
78 t.Errorf("--account-id %q must be accepted — it is shaped like something Sqid emits", want)
79 }
80 // The accept path IS reachable in-process: only a refusal exits.
81 empty, id := "", want
82 if a := apiAccount(&empty, &id); a.Seg != want || !a.Exact {
83 t.Errorf("apiAccount(--account-id %q) = %+v, want {%s true}", want, a, want)
84 }
85 }
86 for _, bad := range []string{"ops", "oil", "ill", "ba", "BAB", "ba1", ""} {
87 if domains.ValidSqid(bad) {
88 t.Errorf("--account-id %q must be refused — it is a name, not a sqid", bad)
89 }
90 }
91 // The specific trap the reviewer caught: release.ValidAccount would have
92 // waved "ops" straight through. If this ever stops being true, the two
93 // predicates have converged and the gate is doing nothing.
94 if !release.ValidAccount("ops") {
95 t.Error("release.ValidAccount no longer accepts \"ops\" — this gate's whole reason to exist has moved")
96 }
97}
98
99func TestApiAccountExact(t *testing.T) {
100 acct, id := "ops", "bah"
101 a := apiAccount(&acct, &id)
102 if a.Seg != "bah" || !a.Exact {
103 t.Errorf("apiAccount with --account-id = %+v, want {bah true}", a)
104 }
105 id = ""
106 a = apiAccount(&acct, &id)
107 if a.Seg != "ops" || a.Exact {
108 t.Errorf("apiAccount without --account-id = %+v, want {ops false}", a)
109 }
110 // Both flags are trimmed the way resolveAccount trims them, so a value
111 // that picked up whitespace from a shell means the same thing in either
112 // mode — API mode used to send " ops " to the console verbatim.
113 padded, id := " ops ", " bah "
114 if a := apiAccount(&padded, &id); a.Seg != "bah" || !a.Exact {
115 t.Errorf("apiAccount(padded --account-id) = %+v, want {bah true}", a)
116 }
117 id = " "
118 if a := apiAccount(&padded, &id); a.Seg != "ops" || a.Exact {
119 t.Errorf("apiAccount(padded --account, blank --account-id) = %+v, want {ops false}", a)
120 }
121 if p := apiPath(apiAcct{Seg: "bah", Exact: true}, "web", "/channels"); !strings.HasSuffix(p, "?exact=sqid") {
122 t.Errorf("apiPath exact = %q, want ?exact=sqid suffix", p)
123 }
124 if p := apiPath(apiAcct{Seg: "ops"}, "web", "/channels"); strings.Contains(p, "exact") {
125 t.Errorf("apiPath non-exact = %q, must carry no exact param", p)
126 }
127}
128