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 "encoding/json"
5 "math"
6 "os"
7 "path/filepath"
8 "runtime"
9 "slices"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/domains"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// devID is the account id whose sqid is "dev". Pinned here as well as
20// derived, because #285 was filed against the wrong id (1169, which is
21// "dew"): a test that only ever derived it would not have caught that.
22const devID = 1168
23
24// sqidID inverts Sqid by plain base-23 decoding, the way the encoder builds
25// its digits. ok is false for a string Sqid can never emit for a positive id.
26// It lives in the test on purpose: nothing in production decodes a sqid, and
27// the producibility check should not trust the encoder to vouch for itself.
28func sqidID(s string) (int64, bool) {
29 const alphabet = "abcdefghjkmnpqrstuvwxyz"
30 if len(s) < 3 || len(s) > 16 {
31 return 0, false
32 }
33 var n int64
34 for i := 0; i < len(s); i++ {
35 d := strings.IndexByte(alphabet, s[i])
36 if d < 0 {
37 return 0, false
38 }
39 if n > (math.MaxInt64-int64(d))/23 {
40 return 0, false // past every int64 id: Sqid never gets here
41 }
42 n = n*23 + int64(d)
43 }
44 id := n - 23*23
45 if id < 1 || domains.Sqid(id) != s {
46 return 0, false // a leading "a", or "baa" (id 0)
47 }
48 return id, true
49}
50
51func TestDevIsReservedAndIsSqid1168(t *testing.T) {
52 if got := domains.Sqid(devID); got != "dev" {
53 t.Fatalf("Sqid(%d) = %q, want \"dev\"", devID, got)
54 }
55 if !domains.IsReservedSqid("dev") {
56 t.Fatal(`"dev" is not reserved: account 1168's apps would resolve to the dev deployment's edge`)
57 }
58 if domains.Sqid(1169) != "dew" || domains.IsReservedSqid("dew") {
59 t.Fatalf("Sqid(1169) = %q (reserved %v): 1169 is the id the allocator should land on",
60 domains.Sqid(1169), domains.IsReservedSqid(domains.Sqid(1169)))
61 }
62}
63
64// Every listed label must be one Sqid can actually emit. A label with i, l
65// or o in it, under 3 letters, or starting with "a" can never be an account's
66// sqid, and listing it would only suggest a protection that is not needed.
67func TestReservedSqidsAreProducible(t *testing.T) {
68 list := domains.ReservedSqids()
69 if len(list) == 0 {
70 t.Fatal("no reserved sqids")
71 }
72 seen := map[string]bool{}
73 for _, label := range list {
74 if seen[label] {
75 t.Errorf("%q is listed twice", label)
76 }
77 seen[label] = true
78 id, ok := sqidID(label)
79 if !ok {
80 t.Errorf("%q is listed as reserved but Sqid can never produce it; drop it", label)
81 continue
82 }
83 if !domains.ValidSqid(label) {
84 t.Errorf("%q decodes to id %d but ValidSqid rejects it", label, id)
85 }
86 t.Logf("reserved %q = Sqid(%d)", label, id)
87 }
88 // The decoder itself, against the frozen golden values and the cases it
89 // must refuse.
90 for s, want := range map[string]int64{"bab": 1, "cxn": 1000, "dev": devID} {
91 if got, ok := sqidID(s); !ok || got != want {
92 t.Errorf("sqidID(%q) = %d %v, want %d", s, got, ok, want)
93 }
94 }
95 for _, s := range []string{"app", "acme", "baa", "relay", "us", "ops"} {
96 if _, ok := sqidID(s); ok {
97 t.Errorf("sqidID(%q) decoded, but Sqid never emits it", s)
98 }
99 }
100}
101
102// ReservedSqids hands out a copy: a caller editing its slice must not change
103// what the allocator skips.
104func TestReservedSqidsIsACopy(t *testing.T) {
105 list := domains.ReservedSqids()
106 list[0] = "zzz"
107 if domains.IsReservedSqid("zzz") || !slices.Contains(domains.ReservedSqids(), "dev") {
108 t.Fatal("editing ReservedSqids' result changed the reserved set")
109 }
110}
111
112// The allocator steps over every reserved id, seeded one below it with a
113// single manifest rather than a thousand real rows.
114func TestNextAccountIDSkipsReservedSqids(t *testing.T) {
115 ctx := t.Context()
116 for _, label := range domains.ReservedSqids() {
117 id, ok := sqidID(label)
118 if !ok {
119 t.Fatalf("%q is not producible", label)
120 }
121 s := release.NewDirStore(t.TempDir())
122 writeManifest(t, s, id-1, domains.Sqid(id-1))
123 got, err := domains.NextAccountID(ctx, s)
124 if err != nil {
125 t.Fatal(err)
126 }
127 if got <= id {
128 t.Errorf("after id %d, NextAccountID = %d (%q); want past %d, whose sqid is the reserved %q",
129 id-1, got, domains.Sqid(got), id, label)
130 }
131 if domains.IsReservedSqid(domains.Sqid(got)) {
132 t.Errorf("NextAccountID = %d, whose sqid %q is reserved", got, domains.Sqid(got))
133 }
134 }
135
136 // And the plain case is untouched: an id whose sqid is free is the
137 // answer, exactly as before.
138 s := release.NewDirStore(t.TempDir())
139 writeManifest(t, s, devID-2, domains.Sqid(devID-2))
140 if got, err := domains.NextAccountID(ctx, s); err != nil || got != devID-1 {
141 t.Errorf("NextAccountID after %d = %d (%v), want %d", devID-2, got, err, devID-1)
142 }
143}
144
145// MintAccount, the one allocator every creation path uses, lands on 1169
146// across the skip, and writes nothing at accounts/dev/.
147func TestMintAccountNeverMintsDev(t *testing.T) {
148 ctx := t.Context()
149 s := release.NewDirStore(t.TempDir())
150 writeManifest(t, s, devID-1, domains.Sqid(devID-1))
151 id, sqid, err := domains.MintAccount(ctx, s, "shop", "owner@example.com", time.Unix(1700000000, 0))
152 if err != nil {
153 t.Fatal(err)
154 }
155 if id != devID+1 || sqid != "dew" {
156 t.Fatalf("minted (%d, %q), want (%d, \"dew\")", id, sqid, devID+1)
157 }
158 if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil {
159 t.Fatal("a manifest landed at accounts/dev/")
160 }
161}
162
163// The race at the skip: another minter takes 1169 between our listing and our
164// write. We must re-list and take 1170, never fall back to 1168.
165func TestMintAccountLosesTheRaceAtTheSkip(t *testing.T) {
166 ctx := t.Context()
167 inner := release.NewDirStore(t.TempDir())
168 writeManifest(t, inner, devID-1, domains.Sqid(devID-1))
169 planted := false
170 s := &racingStore{Store: inner, beforePut: func(key string) {
171 if planted {
172 return
173 }
174 planted = true
175 if key != domains.AccountManifestKey("dew") {
176 t.Errorf("first create-only write went to %s, want accounts/dew/", key)
177 }
178 writeManifest(t, inner, devID+1, "dew")
179 }}
180 id, sqid, err := domains.MintAccount(ctx, s, "shop", "", time.Now())
181 if err != nil {
182 t.Fatal(err)
183 }
184 if id != devID+2 || sqid != domains.Sqid(devID+2) {
185 t.Fatalf("minted (%d, %q), want (%d, %q)", id, sqid, devID+2, domains.Sqid(devID+2))
186 }
187}
188
189// Concurrent minters around the skip: every one gets a distinct id, none gets
190// 1168, and together they fill 1169.. with no gap but the reserved one.
191func TestConcurrentMintsAroundTheSkip(t *testing.T) {
192 ctx := t.Context()
193 s := release.NewDirStore(t.TempDir())
194 writeManifest(t, s, devID-1, domains.Sqid(devID-1))
195
196 // Five at once: a minter loses at most once to each of the others, and
197 // MintAccount allows five attempts.
198 const n = 5
199 ids := make([]int64, n)
200 errs := make([]error, n)
201 var wg sync.WaitGroup
202 start := make(chan struct{})
203 for i := range n {
204 wg.Add(1)
205 go func() {
206 defer wg.Done()
207 <-start
208 ids[i], _, errs[i] = domains.MintAccount(ctx, s, "shop", "", time.Now())
209 }()
210 }
211 close(start)
212 wg.Wait()
213 for i, err := range errs {
214 if err != nil {
215 t.Fatalf("minter %d: %v", i, err)
216 }
217 }
218 slices.Sort(ids)
219 for i, id := range ids {
220 if want := int64(devID + 1 + i); id != want {
221 t.Fatalf("minted ids %v, want %d..%d", ids, devID+1, devID+n)
222 }
223 }
224 if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil {
225 t.Fatal("a concurrent mint landed at accounts/dev/")
226 }
227}
228
229// The skip holds across restarts: a fresh store over the same bucket (a new
230// process, another box) computes the same answer, before and after the id
231// past the skip is minted. Nothing is stored for the skipped id, so there is
232// nothing for a restart to lose.
233func TestReservedSkipSurvivesRestart(t *testing.T) {
234 ctx := t.Context()
235 dir := t.TempDir()
236 writeManifest(t, release.NewDirStore(dir), devID-1, domains.Sqid(devID-1))
237
238 for range 2 {
239 got, err := domains.NextAccountID(ctx, release.NewDirStore(dir))
240 if err != nil || got != devID+1 {
241 t.Fatalf("NextAccountID on a fresh store = %d (%v), want %d", got, err, devID+1)
242 }
243 }
244 if _, _, err := domains.MintAccount(ctx, release.NewDirStore(dir), "shop", "", time.Now()); err != nil {
245 t.Fatal(err)
246 }
247 got, err := domains.NextAccountID(ctx, release.NewDirStore(dir))
248 if err != nil || got != devID+2 {
249 t.Fatalf("NextAccountID after minting %d = %d (%v), want %d", devID+1, got, err, devID+2)
250 }
251 keys, err := release.NewDirStore(dir).List(ctx, "accounts/")
252 if err != nil {
253 t.Fatal(err)
254 }
255 for _, k := range keys {
256 body, _, err := release.NewDirStore(dir).Get(ctx, k)
257 if err != nil {
258 t.Fatal(err)
259 }
260 var m domains.AccountManifest
261 if err := json.Unmarshal(body, &m); err != nil {
262 t.Fatal(err)
263 }
264 if domains.IsReservedSqid(m.Sqid) {
265 t.Fatalf("%s holds the reserved sqid %q", k, m.Sqid)
266 }
267 }
268}
269
270// Every creation path goes through the one rule. The rule lives in
271// NextAccountID, MintAccount is its only caller, and the account rows the
272// console and the registry write must take their ids from MintAccount. This
273// census pins the callers, so a new creation path has to be added here on
274// purpose, with someone checking that it mints through MintAccount.
275//
276// The materialization writers (console EnsureAccount, registry PutAccount in
277// domains.Reconcile and the CLI) copy an id the bucket already holds; they
278// never choose one, so they are not creation paths.
279func TestAccountCreationPathsCensus(t *testing.T) {
280 _, here, _, ok := runtime.Caller(0)
281 if !ok {
282 t.Fatal("no caller")
283 }
284 root := filepath.Join(filepath.Dir(here), "..", "..")
285 want := map[string][]string{
286 "domains.NextAccountID(": nil, // only MintAccount, inside this package
287 "NextAccountID(ctx": {"internal/domains/mint.go"},
288 "domains.MintAccount(": {
289 "accounts.go", // carlos accounts create --owner
290 "internal/console/server.go", // Server.mintAccount: landing, /accounts/new, /auth/cli/accounts
291 },
292 "s.mintAccount(": {
293 "internal/console/authcli.go", // POST /auth/cli/accounts
294 "internal/console/server.go", // landing mint and POST /accounts/new
295 },
296 ".CreateAccount(": {"internal/console/server.go"}, // inside Server.mintAccount only
297 }
298 got := map[string][]string{}
299 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
300 if err != nil {
301 return err
302 }
303 if d.IsDir() {
304 switch d.Name() {
305 case ".git", "node_modules", "vendor", "testdata":
306 return filepath.SkipDir
307 }
308 if strings.HasPrefix(d.Name(), ".") && path != root {
309 return filepath.SkipDir
310 }
311 return nil
312 }
313 if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
314 return nil
315 }
316 b, err := os.ReadFile(path)
317 if err != nil {
318 return err
319 }
320 rel, _ := filepath.Rel(root, path)
321 rel = filepath.ToSlash(rel)
322 for needle := range want {
323 if strings.Contains(string(b), needle) && !slices.Contains(got[needle], rel) {
324 got[needle] = append(got[needle], rel)
325 }
326 }
327 return nil
328 })
329 if err != nil {
330 t.Fatal(err)
331 }
332 for needle, files := range want {
333 slices.Sort(files)
334 slices.Sort(got[needle])
335 if !slices.Equal(files, got[needle]) {
336 t.Errorf("%q is called from %v, want %v. A new account creation path must mint through "+
337 "domains.MintAccount so it skips reserved sqids (#285); then add it here", needle, got[needle], files)
338 }
339 }
340}
341