| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "log" |
| 6 | "net/http" |
| 7 | "net/url" |
| 8 | "os" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/domains" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | ) |
| 16 | |
| 17 | // devAccountID is the id whose sqid is "dev" (#285). |
| 18 | const devAccountID = int64(1168) |
| 19 | |
| 20 | // Every console door that creates an account (landing's personal-account |
| 21 | // mint, POST /accounts/new, POST /auth/cli/accounts) mints through |
| 22 | // domains.MintAccount, so none of them can hand out the reserved "dev". Each |
| 23 | // door is driven against a bucket whose highest account is 1167, so the next |
| 24 | // plain id would be 1168. |
| 25 | func TestConsoleCreationDoorsSkipReservedSqids(t *testing.T) { |
| 26 | doors := map[string]func(t *testing.T, fx *consoleFixture) (owner string){ |
| 27 | "landing": func(t *testing.T, fx *consoleFixture) string { |
| 28 | fx.setUser("newbie@keymail.dev") |
| 29 | defer fx.setUser("paul@keymail.dev") |
| 30 | c := newBrowser(t) |
| 31 | signUp(t, fx, c, "code-new") |
| 32 | if st, loc, _ := get(t, c, fx.ts.URL+"/"); st != http.StatusFound || !strings.HasPrefix(loc, "/a/") { |
| 33 | t.Fatalf("landing = %d -> %q, want 302 into an account", st, loc) |
| 34 | } |
| 35 | return "newbie@keymail.dev" |
| 36 | }, |
| 37 | "POST /accounts/new": func(t *testing.T, fx *consoleFixture) string { |
| 38 | c := signInPaul(t, fx) |
| 39 | csrf := sessionToken(t, fx, c) |
| 40 | if st, loc, _ := postForm(t, c, fx.ts.URL+"/accounts/new", |
| 41 | url.Values{"csrf": {csrf}, "name": {"shop"}}); st != http.StatusSeeOther { |
| 42 | t.Fatalf("create = %d -> %q, want 303", st, loc) |
| 43 | } |
| 44 | return "paul@keymail.dev" |
| 45 | }, |
| 46 | "POST /auth/cli/accounts": func(t *testing.T, fx *consoleFixture) string { |
| 47 | token := cliToken(t, fx, "paul@keymail.dev", "code-acct") |
| 48 | if st, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "shop"}, token); st != http.StatusOK { |
| 49 | t.Fatalf("accounts create = %d %v", st, out) |
| 50 | } |
| 51 | return "paul@keymail.dev" |
| 52 | }, |
| 53 | } |
| 54 | for name, door := range doors { |
| 55 | t.Run(name, func(t *testing.T) { |
| 56 | fx := newConsoleFixture(t) |
| 57 | ctx := t.Context() |
| 58 | if err := WriteAccountManifest(ctx, fx.rel, devAccountID-1, domains.Sqid(devAccountID-1), |
| 59 | "neighbour", time.Unix(1700000000, 0)); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | owner := door(t, fx) |
| 63 | |
| 64 | accts, err := fx.st.AccountsFor(owner) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | var made *Account |
| 69 | for i := range accts { |
| 70 | if accts[i].ID > devAccountID-1 { |
| 71 | made = &accts[i] |
| 72 | } |
| 73 | } |
| 74 | if made == nil { |
| 75 | t.Fatalf("no new account for %s in %+v", owner, accts) |
| 76 | } |
| 77 | if made.ID != devAccountID+1 || made.Sqid != "dew" { |
| 78 | t.Fatalf("minted (%d, %q), want (%d, \"dew\"): the door skipped the reserved-sqid rule", |
| 79 | made.ID, made.Sqid, devAccountID+1) |
| 80 | } |
| 81 | if _, _, err := fx.rel.Get(ctx, AccountManifestKey("dev")); err == nil { |
| 82 | t.Fatal("a manifest landed at accounts/dev/") |
| 83 | } |
| 84 | if a, err := fx.st.AccountBySqid("dev"); err != nil || a != nil { |
| 85 | t.Fatalf("AccountBySqid(dev) = %+v %v, want none", a, err) |
| 86 | } |
| 87 | }) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // The ruling leaves existing accounts alone, so an account that already holds |
| 92 | // a reserved sqid (the list grew over it, or a manifest was written by hand) |
| 93 | // is materialized as usual and named in the log, once per pass. A tombstoned |
| 94 | // one is not named: it serves nothing. |
| 95 | func TestMaterializeFlagsALiveReservedSqid(t *testing.T) { |
| 96 | var logged bytes.Buffer |
| 97 | log.SetOutput(&logged) |
| 98 | t.Cleanup(func() { log.SetOutput(os.Stderr) }) |
| 99 | |
| 100 | st := openTestStore(t) |
| 101 | rel := release.NewDirStore(t.TempDir()) |
| 102 | ctx := t.Context() |
| 103 | created := time.Unix(1700000000, 0).UTC() |
| 104 | edgeID := int64(49868) // Sqid "edge" |
| 105 | for _, a := range []struct { |
| 106 | id int64 |
| 107 | name string |
| 108 | }{{devAccountID, "held"}, {devAccountID + 1, "plain"}, {edgeID, "retired"}} { |
| 109 | if err := WriteAccountManifest(ctx, rel, a.id, domains.Sqid(a.id), a.name, created); err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | } |
| 113 | if err := domains.TombstoneAccount(ctx, rel, "edge", created); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | |
| 117 | if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if a, err := st.AccountBySqid("dev"); err != nil || a == nil || a.ID != devAccountID { |
| 121 | t.Fatalf("AccountBySqid(dev) = %+v %v: an existing account must be left as it is", a, err) |
| 122 | } |
| 123 | out := logged.String() |
| 124 | if !strings.Contains(out, "account dev (id 1168) has a reserved sqid") { |
| 125 | t.Errorf("no reserved-sqid line for the live dev account:\n%s", out) |
| 126 | } |
| 127 | flagged := 0 |
| 128 | for _, line := range strings.Split(out, "\n") { |
| 129 | if !strings.Contains(line, "reserved sqid") { |
| 130 | continue |
| 131 | } |
| 132 | flagged++ |
| 133 | if strings.Contains(line, "—") { |
| 134 | t.Errorf("the reserved-sqid log line carries an em dash: %s", line) |
| 135 | } |
| 136 | } |
| 137 | if flagged != 1 { |
| 138 | t.Errorf("%d reserved-sqid lines, want exactly one (dev; not the plain dew, not the retired edge):\n%s", flagged, out) |
| 139 | } |
| 140 | } |
| 141 | |