package domains_test import ( "encoding/json" "math" "os" "path/filepath" "runtime" "slices" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/release" ) // devID is the account id whose sqid is "dev". Pinned here as well as // derived, because #285 was filed against the wrong id (1169, which is // "dew"): a test that only ever derived it would not have caught that. const devID = 1168 // sqidID inverts Sqid by plain base-23 decoding, the way the encoder builds // its digits. ok is false for a string Sqid can never emit for a positive id. // It lives in the test on purpose: nothing in production decodes a sqid, and // the producibility check should not trust the encoder to vouch for itself. func sqidID(s string) (int64, bool) { const alphabet = "abcdefghjkmnpqrstuvwxyz" if len(s) < 3 || len(s) > 16 { return 0, false } var n int64 for i := 0; i < len(s); i++ { d := strings.IndexByte(alphabet, s[i]) if d < 0 { return 0, false } if n > (math.MaxInt64-int64(d))/23 { return 0, false // past every int64 id: Sqid never gets here } n = n*23 + int64(d) } id := n - 23*23 if id < 1 || domains.Sqid(id) != s { return 0, false // a leading "a", or "baa" (id 0) } return id, true } func TestDevIsReservedAndIsSqid1168(t *testing.T) { if got := domains.Sqid(devID); got != "dev" { t.Fatalf("Sqid(%d) = %q, want \"dev\"", devID, got) } if !domains.IsReservedSqid("dev") { t.Fatal(`"dev" is not reserved: account 1168's apps would resolve to the dev deployment's edge`) } if domains.Sqid(1169) != "dew" || domains.IsReservedSqid("dew") { t.Fatalf("Sqid(1169) = %q (reserved %v): 1169 is the id the allocator should land on", domains.Sqid(1169), domains.IsReservedSqid(domains.Sqid(1169))) } } // Every listed label must be one Sqid can actually emit. A label with i, l // or o in it, under 3 letters, or starting with "a" can never be an account's // sqid, and listing it would only suggest a protection that is not needed. func TestReservedSqidsAreProducible(t *testing.T) { list := domains.ReservedSqids() if len(list) == 0 { t.Fatal("no reserved sqids") } seen := map[string]bool{} for _, label := range list { if seen[label] { t.Errorf("%q is listed twice", label) } seen[label] = true id, ok := sqidID(label) if !ok { t.Errorf("%q is listed as reserved but Sqid can never produce it; drop it", label) continue } if !domains.ValidSqid(label) { t.Errorf("%q decodes to id %d but ValidSqid rejects it", label, id) } t.Logf("reserved %q = Sqid(%d)", label, id) } // The decoder itself, against the frozen golden values and the cases it // must refuse. for s, want := range map[string]int64{"bab": 1, "cxn": 1000, "dev": devID} { if got, ok := sqidID(s); !ok || got != want { t.Errorf("sqidID(%q) = %d %v, want %d", s, got, ok, want) } } for _, s := range []string{"app", "acme", "baa", "relay", "us", "ops"} { if _, ok := sqidID(s); ok { t.Errorf("sqidID(%q) decoded, but Sqid never emits it", s) } } } // ReservedSqids hands out a copy: a caller editing its slice must not change // what the allocator skips. func TestReservedSqidsIsACopy(t *testing.T) { list := domains.ReservedSqids() list[0] = "zzz" if domains.IsReservedSqid("zzz") || !slices.Contains(domains.ReservedSqids(), "dev") { t.Fatal("editing ReservedSqids' result changed the reserved set") } } // The allocator steps over every reserved id, seeded one below it with a // single manifest rather than a thousand real rows. func TestNextAccountIDSkipsReservedSqids(t *testing.T) { ctx := t.Context() for _, label := range domains.ReservedSqids() { id, ok := sqidID(label) if !ok { t.Fatalf("%q is not producible", label) } s := release.NewDirStore(t.TempDir()) writeManifest(t, s, id-1, domains.Sqid(id-1)) got, err := domains.NextAccountID(ctx, s) if err != nil { t.Fatal(err) } if got <= id { t.Errorf("after id %d, NextAccountID = %d (%q); want past %d, whose sqid is the reserved %q", id-1, got, domains.Sqid(got), id, label) } if domains.IsReservedSqid(domains.Sqid(got)) { t.Errorf("NextAccountID = %d, whose sqid %q is reserved", got, domains.Sqid(got)) } } // And the plain case is untouched: an id whose sqid is free is the // answer, exactly as before. s := release.NewDirStore(t.TempDir()) writeManifest(t, s, devID-2, domains.Sqid(devID-2)) if got, err := domains.NextAccountID(ctx, s); err != nil || got != devID-1 { t.Errorf("NextAccountID after %d = %d (%v), want %d", devID-2, got, err, devID-1) } } // MintAccount, the one allocator every creation path uses, lands on 1169 // across the skip, and writes nothing at accounts/dev/. func TestMintAccountNeverMintsDev(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) writeManifest(t, s, devID-1, domains.Sqid(devID-1)) id, sqid, err := domains.MintAccount(ctx, s, "shop", "owner@example.com", time.Unix(1700000000, 0)) if err != nil { t.Fatal(err) } if id != devID+1 || sqid != "dew" { t.Fatalf("minted (%d, %q), want (%d, \"dew\")", id, sqid, devID+1) } if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil { t.Fatal("a manifest landed at accounts/dev/") } } // The race at the skip: another minter takes 1169 between our listing and our // write. We must re-list and take 1170, never fall back to 1168. func TestMintAccountLosesTheRaceAtTheSkip(t *testing.T) { ctx := t.Context() inner := release.NewDirStore(t.TempDir()) writeManifest(t, inner, devID-1, domains.Sqid(devID-1)) planted := false s := &racingStore{Store: inner, beforePut: func(key string) { if planted { return } planted = true if key != domains.AccountManifestKey("dew") { t.Errorf("first create-only write went to %s, want accounts/dew/", key) } writeManifest(t, inner, devID+1, "dew") }} id, sqid, err := domains.MintAccount(ctx, s, "shop", "", time.Now()) if err != nil { t.Fatal(err) } if id != devID+2 || sqid != domains.Sqid(devID+2) { t.Fatalf("minted (%d, %q), want (%d, %q)", id, sqid, devID+2, domains.Sqid(devID+2)) } } // Concurrent minters around the skip: every one gets a distinct id, none gets // 1168, and together they fill 1169.. with no gap but the reserved one. func TestConcurrentMintsAroundTheSkip(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) writeManifest(t, s, devID-1, domains.Sqid(devID-1)) // Five at once: a minter loses at most once to each of the others, and // MintAccount allows five attempts. const n = 5 ids := make([]int64, n) errs := make([]error, n) var wg sync.WaitGroup start := make(chan struct{}) for i := range n { wg.Add(1) go func() { defer wg.Done() <-start ids[i], _, errs[i] = domains.MintAccount(ctx, s, "shop", "", time.Now()) }() } close(start) wg.Wait() for i, err := range errs { if err != nil { t.Fatalf("minter %d: %v", i, err) } } slices.Sort(ids) for i, id := range ids { if want := int64(devID + 1 + i); id != want { t.Fatalf("minted ids %v, want %d..%d", ids, devID+1, devID+n) } } if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil { t.Fatal("a concurrent mint landed at accounts/dev/") } } // The skip holds across restarts: a fresh store over the same bucket (a new // process, another box) computes the same answer, before and after the id // past the skip is minted. Nothing is stored for the skipped id, so there is // nothing for a restart to lose. func TestReservedSkipSurvivesRestart(t *testing.T) { ctx := t.Context() dir := t.TempDir() writeManifest(t, release.NewDirStore(dir), devID-1, domains.Sqid(devID-1)) for range 2 { got, err := domains.NextAccountID(ctx, release.NewDirStore(dir)) if err != nil || got != devID+1 { t.Fatalf("NextAccountID on a fresh store = %d (%v), want %d", got, err, devID+1) } } if _, _, err := domains.MintAccount(ctx, release.NewDirStore(dir), "shop", "", time.Now()); err != nil { t.Fatal(err) } got, err := domains.NextAccountID(ctx, release.NewDirStore(dir)) if err != nil || got != devID+2 { t.Fatalf("NextAccountID after minting %d = %d (%v), want %d", devID+1, got, err, devID+2) } keys, err := release.NewDirStore(dir).List(ctx, "accounts/") if err != nil { t.Fatal(err) } for _, k := range keys { body, _, err := release.NewDirStore(dir).Get(ctx, k) if err != nil { t.Fatal(err) } var m domains.AccountManifest if err := json.Unmarshal(body, &m); err != nil { t.Fatal(err) } if domains.IsReservedSqid(m.Sqid) { t.Fatalf("%s holds the reserved sqid %q", k, m.Sqid) } } } // Every creation path goes through the one rule. The rule lives in // NextAccountID, MintAccount is its only caller, and the account rows the // console and the registry write must take their ids from MintAccount. This // census pins the callers, so a new creation path has to be added here on // purpose, with someone checking that it mints through MintAccount. // // The materialization writers (console EnsureAccount, registry PutAccount in // domains.Reconcile and the CLI) copy an id the bucket already holds; they // never choose one, so they are not creation paths. func TestAccountCreationPathsCensus(t *testing.T) { _, here, _, ok := runtime.Caller(0) if !ok { t.Fatal("no caller") } root := filepath.Join(filepath.Dir(here), "..", "..") want := map[string][]string{ "domains.NextAccountID(": nil, // only MintAccount, inside this package "NextAccountID(ctx": {"internal/domains/mint.go"}, "domains.MintAccount(": { "accounts.go", // carlos accounts create --owner "internal/console/server.go", // Server.mintAccount: landing, /accounts/new, /auth/cli/accounts }, "s.mintAccount(": { "internal/console/authcli.go", // POST /auth/cli/accounts "internal/console/server.go", // landing mint and POST /accounts/new }, ".CreateAccount(": {"internal/console/server.go"}, // inside Server.mintAccount only } got := map[string][]string{} err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { switch d.Name() { case ".git", "node_modules", "vendor", "testdata": return filepath.SkipDir } if strings.HasPrefix(d.Name(), ".") && path != root { return filepath.SkipDir } return nil } if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return nil } b, err := os.ReadFile(path) if err != nil { return err } rel, _ := filepath.Rel(root, path) rel = filepath.ToSlash(rel) for needle := range want { if strings.Contains(string(b), needle) && !slices.Contains(got[needle], rel) { got[needle] = append(got[needle], rel) } } return nil }) if err != nil { t.Fatal(err) } for needle, files := range want { slices.Sort(files) slices.Sort(got[needle]) if !slices.Equal(files, got[needle]) { t.Errorf("%q is called from %v, want %v. A new account creation path must mint through "+ "domains.MintAccount so it skips reserved sqids (#285); then add it here", needle, got[needle], files) } } }