carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package domains_test
2
3import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/domains"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// Account ids are SEQUENTIAL, and the sqid length is why: the frozen encoder
15// is base-23, so a 40-bit id encodes to nine characters —
16// kass.nyfbskdye.oncarlos.com — which nobody can read out, type, or trust.
17// Sequential ids give bab, bac, bad…, growing a character every 23x.
18func TestNextAccountIDIsTheNextFreeInteger(t *testing.T) {
19 ctx := t.Context()
20 s := release.NewDirStore(t.TempDir())
21
22 // An empty bucket starts at 2: 0 is not an id and 1 is reserved for the
23 // built-in ops account on every deployment.
24 got, err := domains.NextAccountID(ctx, s)
25 if err != nil {
26 t.Fatal(err)
27 }
28 if got != 2 {
29 t.Errorf("NextAccountID(empty) = %d, want 2", got)
30 }
31 if len(domains.Sqid(got)) > 3 {
32 t.Errorf("Sqid(%d) = %q — sequential ids exist to keep this short", got, domains.Sqid(got))
33 }
34
35 for _, id := range []int64{2, 7} {
36 writeManifest(t, s, id, domains.Sqid(id))
37 }
38 if got, err := domains.NextAccountID(ctx, s); err != nil || got != 8 {
39 t.Errorf("NextAccountID = %d (%v), want 8 — one past the highest claimed", got, err)
40 }
41}
42
43// A manifest that disagrees with itself still CONSUMES its id. It is skipped
44// for materialization (it is corruption, not an account), but handing its id
45// to somebody else would make two objects claim one identity — the one thing
46// the whole minting scheme exists to prevent.
47func TestNextAccountIDCountsIdsItWouldNotMaterialize(t *testing.T) {
48 ctx := t.Context()
49 s := release.NewDirStore(t.TempDir())
50 if _, err := s.Put(ctx, "accounts/wrong/account.json",
51 []byte(`{"id":9,"sqid":"wrong","name":"Corrupt"}`), ""); err != nil {
52 t.Fatal(err)
53 }
54 if _, err := s.Put(ctx, "accounts/junk/account.json", []byte("{not json"), ""); err != nil {
55 t.Fatal(err)
56 }
57 if got, err := domains.NextAccountID(ctx, s); err != nil || got != 10 {
58 t.Errorf("NextAccountID = %d (%v), want 10 — a claimed id is claimed", got, err)
59 }
60}
61
62// Two boxes minting at once must never agree on an id. The listing that picks
63// max+1 is not a lock, so the manifest write is CREATE-ONLY: the loser gets a
64// CAS conflict, re-lists, and takes the next one. Overwriting would erase an
65// account's identity and re-home every domain it owns.
66func TestMintAccountLosesTheRaceAndTakesTheNextID(t *testing.T) {
67 ctx := t.Context()
68 inner := release.NewDirStore(t.TempDir())
69 planted := false
70 s := &racingStore{Store: inner, beforePut: func(key string) {
71 if planted {
72 return
73 }
74 planted = true
75 // Another minter got there first, between our List and our Put.
76 writeManifest(t, inner, 2, domains.Sqid(2))
77 }}
78
79 id, sqid, err := domains.MintAccount(ctx, s, "Kass", "", time.Unix(1700000000, 0))
80 if err != nil {
81 t.Fatal(err)
82 }
83 if id != 3 || sqid != domains.Sqid(3) {
84 t.Errorf("minted (%d, %q), want the next free id 3 (%q)", id, sqid, domains.Sqid(3))
85 }
86 // The competitor's manifest is untouched.
87 body, _, err := inner.Get(ctx, "accounts/"+domains.Sqid(2)+"/account.json")
88 if err != nil {
89 t.Fatal(err)
90 }
91 var m struct {
92 Name string `json:"name"`
93 }
94 if err := json.Unmarshal(body, &m); err != nil || m.Name != "Competitor" {
95 t.Errorf("the racing minter's manifest was overwritten: %s", body)
96 }
97 // And ours landed, with the id in the body matching the sqid in the path.
98 body, _, err = inner.Get(ctx, "accounts/"+sqid+"/account.json")
99 if err != nil {
100 t.Fatal(err)
101 }
102 var mine struct {
103 ID int64 `json:"id"`
104 Sqid string `json:"sqid"`
105 Name string `json:"name"`
106 }
107 if err := json.Unmarshal(body, &mine); err != nil {
108 t.Fatal(err)
109 }
110 if mine.ID != id || mine.Sqid != sqid || mine.Name != "Kass" {
111 t.Errorf("minted manifest = %+v, want id %d sqid %q", mine, id, sqid)
112 }
113}
114
115// Retries are BOUNDED. A store that conflicts forever is a broken deployment,
116// not something to spin on: fail loudly rather than loop.
117func TestMintAccountGivesUpLoudly(t *testing.T) {
118 ctx := t.Context()
119 inner := release.NewDirStore(t.TempDir())
120 next := int64(2)
121 s := &racingStore{Store: inner, beforePut: func(key string) {
122 writeManifest(t, inner, next, domains.Sqid(next))
123 next++
124 }}
125 _, _, err := domains.MintAccount(ctx, s, "Kass", "", time.Now())
126 if err == nil {
127 t.Fatal("minting against a permanently racing store succeeded")
128 }
129 if !strings.Contains(err.Error(), "account") {
130 t.Errorf("give-up error does not say what failed: %v", err)
131 }
132}
133
134// FOUND IN PRODUCTION: `carlos accounts create` recorded an owner NOWHERE, so
135// the accounts kass and keymail were migrated into were unreachable through the
136// console until someone ran `console member -add … -role owner` by hand. The
137// operator hit exactly that and could not open either account.
138//
139// The manifest is where the fix lives, because the bucket is the control plane:
140// whoever materializes the account — this box's console, or any other box's —
141// reads the same object and lands on the same owner. An account created from
142// the CLI and one created from the console now converge on the same end state.
143func TestMintAccountRecordsTheOwnerInTheManifest(t *testing.T) {
144 ctx := t.Context()
145 s := release.NewDirStore(t.TempDir())
146 _, sqid, err := domains.MintAccount(ctx, s, "Kass", " Paul@Keymail.dev ", time.Unix(1700000000, 0))
147 if err != nil {
148 t.Fatal(err)
149 }
150 body, _, err := s.Get(ctx, domains.AccountManifestKey(sqid))
151 if err != nil {
152 t.Fatal(err)
153 }
154 var m domains.AccountManifest
155 if err := json.Unmarshal(body, &m); err != nil {
156 t.Fatal(err)
157 }
158 // NORMALIZED at the mint, not at the reader: the console's memberships key
159 // is literal text with no COLLATE NOCASE, so a manifest carrying
160 // "Paul@Keymail.dev" would materialize a membership the real session can
161 // never match — an owner who still cannot get in, which is the very failure
162 // this field exists to end.
163 if m.Owner != "paul@keymail.dev" {
164 t.Errorf("manifest owner = %q, want the trimmed, lowercased address", m.Owner)
165 }
166}
167
168// Absent is legal and means ownerless: every manifest written before this field
169// existed has no owner, and they must keep materializing exactly as they did.
170func TestMintAccountLeavesTheOwnerOutWhenThereIsNone(t *testing.T) {
171 ctx := t.Context()
172 s := release.NewDirStore(t.TempDir())
173 _, sqid, err := domains.MintAccount(ctx, s, "Ownerless", "", time.Unix(1700000000, 0))
174 if err != nil {
175 t.Fatal(err)
176 }
177 body, _, err := s.Get(ctx, domains.AccountManifestKey(sqid))
178 if err != nil {
179 t.Fatal(err)
180 }
181 if strings.Contains(string(body), "owner") {
182 t.Errorf("manifest carries an owner key for an ownerless account: %s", body)
183 }
184}
185
186// A manifest owner that keymail could never authenticate is a grant nobody can
187// claim — the same reason `console member` refuses a malformed address. Caught
188// at the MINT, because the manifest is written once and read forever: a bad
189// address here is permanent, and the id it consumed is not reusable.
190func TestMintAccountRefusesAnUnusableOwner(t *testing.T) {
191 ctx := t.Context()
192 for _, bad := range []string{"paul", "@keymail.dev", "paul@", "paul @keymail.dev"} {
193 s := release.NewDirStore(t.TempDir())
194 if _, _, err := domains.MintAccount(ctx, s, "Kass", bad, time.Now()); err == nil {
195 t.Errorf("MintAccount with owner %q succeeded", bad)
196 }
197 // And nothing was written: a refused mint must not burn an id or leave
198 // an account behind.
199 if keys, err := s.List(ctx, "accounts/"); err != nil || len(keys) != 0 {
200 t.Errorf("owner %q left %v in the bucket", bad, keys)
201 }
202 }
203}
204
205// One rule, in one place. The console's sign-in form, `console member -add` and
206// now `carlos accounts create` must all accept and reject the same strings —
207// an address one of them takes and another refuses is a membership somebody
208// can create and nobody can use.
209func TestAddressNormalizationAndValidity(t *testing.T) {
210 if got := domains.NormalizeAddress(" Paul@Keymail.dev\n"); got != "paul@keymail.dev" {
211 t.Errorf("NormalizeAddress = %q", got)
212 }
213 for _, ok := range []string{"paul@keymail.dev", "a@b"} {
214 if !domains.ValidAddress(ok) {
215 t.Errorf("ValidAddress(%q) = false", ok)
216 }
217 }
218 for _, bad := range []string{"", "paul", "@keymail.dev", "paul@", "paul@key mail.dev"} {
219 if domains.ValidAddress(bad) {
220 t.Errorf("ValidAddress(%q) = true", bad)
221 }
222 }
223}
224
225func writeManifest(t *testing.T, s release.Store, id int64, sqid string) {
226 t.Helper()
227 body := []byte(`{"id":` + itoa(id) + `,"sqid":"` + sqid + `","name":"Competitor",` +
228 `"created_at":"2026-08-02T09:00:00Z"}`)
229 if _, err := s.Put(context.Background(), "accounts/"+sqid+"/account.json", body, ""); err != nil {
230 t.Fatal(err)
231 }
232}
233
234func itoa(i int64) string {
235 b, _ := json.Marshal(i)
236 return string(b)
237}
238
239// racingStore runs a hook immediately before each Put, so a test can plant the
240// object the minter is about to create — the one interleaving that matters and
241// the one a sequential allocator has to survive.
242type racingStore struct {
243 release.Store
244 beforePut func(key string)
245}
246
247func (r *racingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
248 r.beforePut(key)
249 return r.Store.Put(ctx, key, body, ifMatch)
250}
251