package domains_test import ( "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/release" ) // Account ids are SEQUENTIAL, and the sqid length is why: the frozen encoder // is base-23, so a 40-bit id encodes to nine characters — // kass.nyfbskdye.oncarlos.com — which nobody can read out, type, or trust. // Sequential ids give bab, bac, bad…, growing a character every 23x. func TestNextAccountIDIsTheNextFreeInteger(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) // An empty bucket starts at 2: 0 is not an id and 1 is reserved for the // built-in ops account on every deployment. got, err := domains.NextAccountID(ctx, s) if err != nil { t.Fatal(err) } if got != 2 { t.Errorf("NextAccountID(empty) = %d, want 2", got) } if len(domains.Sqid(got)) > 3 { t.Errorf("Sqid(%d) = %q — sequential ids exist to keep this short", got, domains.Sqid(got)) } for _, id := range []int64{2, 7} { writeManifest(t, s, id, domains.Sqid(id)) } if got, err := domains.NextAccountID(ctx, s); err != nil || got != 8 { t.Errorf("NextAccountID = %d (%v), want 8 — one past the highest claimed", got, err) } } // A manifest that disagrees with itself still CONSUMES its id. It is skipped // for materialization (it is corruption, not an account), but handing its id // to somebody else would make two objects claim one identity — the one thing // the whole minting scheme exists to prevent. func TestNextAccountIDCountsIdsItWouldNotMaterialize(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) if _, err := s.Put(ctx, "accounts/wrong/account.json", []byte(`{"id":9,"sqid":"wrong","name":"Corrupt"}`), ""); err != nil { t.Fatal(err) } if _, err := s.Put(ctx, "accounts/junk/account.json", []byte("{not json"), ""); err != nil { t.Fatal(err) } if got, err := domains.NextAccountID(ctx, s); err != nil || got != 10 { t.Errorf("NextAccountID = %d (%v), want 10 — a claimed id is claimed", got, err) } } // Two boxes minting at once must never agree on an id. The listing that picks // max+1 is not a lock, so the manifest write is CREATE-ONLY: the loser gets a // CAS conflict, re-lists, and takes the next one. Overwriting would erase an // account's identity and re-home every domain it owns. func TestMintAccountLosesTheRaceAndTakesTheNextID(t *testing.T) { ctx := t.Context() inner := release.NewDirStore(t.TempDir()) planted := false s := &racingStore{Store: inner, beforePut: func(key string) { if planted { return } planted = true // Another minter got there first, between our List and our Put. writeManifest(t, inner, 2, domains.Sqid(2)) }} id, sqid, err := domains.MintAccount(ctx, s, "Kass", "", time.Unix(1700000000, 0)) if err != nil { t.Fatal(err) } if id != 3 || sqid != domains.Sqid(3) { t.Errorf("minted (%d, %q), want the next free id 3 (%q)", id, sqid, domains.Sqid(3)) } // The competitor's manifest is untouched. body, _, err := inner.Get(ctx, "accounts/"+domains.Sqid(2)+"/account.json") if err != nil { t.Fatal(err) } var m struct { Name string `json:"name"` } if err := json.Unmarshal(body, &m); err != nil || m.Name != "Competitor" { t.Errorf("the racing minter's manifest was overwritten: %s", body) } // And ours landed, with the id in the body matching the sqid in the path. body, _, err = inner.Get(ctx, "accounts/"+sqid+"/account.json") if err != nil { t.Fatal(err) } var mine struct { ID int64 `json:"id"` Sqid string `json:"sqid"` Name string `json:"name"` } if err := json.Unmarshal(body, &mine); err != nil { t.Fatal(err) } if mine.ID != id || mine.Sqid != sqid || mine.Name != "Kass" { t.Errorf("minted manifest = %+v, want id %d sqid %q", mine, id, sqid) } } // Retries are BOUNDED. A store that conflicts forever is a broken deployment, // not something to spin on: fail loudly rather than loop. func TestMintAccountGivesUpLoudly(t *testing.T) { ctx := t.Context() inner := release.NewDirStore(t.TempDir()) next := int64(2) s := &racingStore{Store: inner, beforePut: func(key string) { writeManifest(t, inner, next, domains.Sqid(next)) next++ }} _, _, err := domains.MintAccount(ctx, s, "Kass", "", time.Now()) if err == nil { t.Fatal("minting against a permanently racing store succeeded") } if !strings.Contains(err.Error(), "account") { t.Errorf("give-up error does not say what failed: %v", err) } } // FOUND IN PRODUCTION: `carlos accounts create` recorded an owner NOWHERE, so // the accounts kass and keymail were migrated into were unreachable through the // console until someone ran `console member -add … -role owner` by hand. The // operator hit exactly that and could not open either account. // // The manifest is where the fix lives, because the bucket is the control plane: // whoever materializes the account — this box's console, or any other box's — // reads the same object and lands on the same owner. An account created from // the CLI and one created from the console now converge on the same end state. func TestMintAccountRecordsTheOwnerInTheManifest(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) _, sqid, err := domains.MintAccount(ctx, s, "Kass", " Paul@Keymail.dev ", time.Unix(1700000000, 0)) if err != nil { t.Fatal(err) } body, _, err := s.Get(ctx, domains.AccountManifestKey(sqid)) if err != nil { t.Fatal(err) } var m domains.AccountManifest if err := json.Unmarshal(body, &m); err != nil { t.Fatal(err) } // NORMALIZED at the mint, not at the reader: the console's memberships key // is literal text with no COLLATE NOCASE, so a manifest carrying // "Paul@Keymail.dev" would materialize a membership the real session can // never match — an owner who still cannot get in, which is the very failure // this field exists to end. if m.Owner != "paul@keymail.dev" { t.Errorf("manifest owner = %q, want the trimmed, lowercased address", m.Owner) } } // Absent is legal and means ownerless: every manifest written before this field // existed has no owner, and they must keep materializing exactly as they did. func TestMintAccountLeavesTheOwnerOutWhenThereIsNone(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) _, sqid, err := domains.MintAccount(ctx, s, "Ownerless", "", time.Unix(1700000000, 0)) if err != nil { t.Fatal(err) } body, _, err := s.Get(ctx, domains.AccountManifestKey(sqid)) if err != nil { t.Fatal(err) } if strings.Contains(string(body), "owner") { t.Errorf("manifest carries an owner key for an ownerless account: %s", body) } } // A manifest owner that keymail could never authenticate is a grant nobody can // claim — the same reason `console member` refuses a malformed address. Caught // at the MINT, because the manifest is written once and read forever: a bad // address here is permanent, and the id it consumed is not reusable. func TestMintAccountRefusesAnUnusableOwner(t *testing.T) { ctx := t.Context() for _, bad := range []string{"paul", "@keymail.dev", "paul@", "paul @keymail.dev"} { s := release.NewDirStore(t.TempDir()) if _, _, err := domains.MintAccount(ctx, s, "Kass", bad, time.Now()); err == nil { t.Errorf("MintAccount with owner %q succeeded", bad) } // And nothing was written: a refused mint must not burn an id or leave // an account behind. if keys, err := s.List(ctx, "accounts/"); err != nil || len(keys) != 0 { t.Errorf("owner %q left %v in the bucket", bad, keys) } } } // One rule, in one place. The console's sign-in form, `console member -add` and // now `carlos accounts create` must all accept and reject the same strings — // an address one of them takes and another refuses is a membership somebody // can create and nobody can use. func TestAddressNormalizationAndValidity(t *testing.T) { if got := domains.NormalizeAddress(" Paul@Keymail.dev\n"); got != "paul@keymail.dev" { t.Errorf("NormalizeAddress = %q", got) } for _, ok := range []string{"paul@keymail.dev", "a@b"} { if !domains.ValidAddress(ok) { t.Errorf("ValidAddress(%q) = false", ok) } } for _, bad := range []string{"", "paul", "@keymail.dev", "paul@", "paul@key mail.dev"} { if domains.ValidAddress(bad) { t.Errorf("ValidAddress(%q) = true", bad) } } } func writeManifest(t *testing.T, s release.Store, id int64, sqid string) { t.Helper() body := []byte(`{"id":` + itoa(id) + `,"sqid":"` + sqid + `","name":"Competitor",` + `"created_at":"2026-08-02T09:00:00Z"}`) if _, err := s.Put(context.Background(), "accounts/"+sqid+"/account.json", body, ""); err != nil { t.Fatal(err) } } func itoa(i int64) string { b, _ := json.Marshal(i) return string(b) } // racingStore runs a hook immediately before each Put, so a test can plant the // object the minter is about to create — the one interleaving that matters and // the one a sequential allocator has to survive. type racingStore struct { release.Store beforePut func(key string) } func (r *racingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { r.beforePut(key) return r.Store.Put(ctx, key, body, ifMatch) }