carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "database/sql"
5 "path/filepath"
6 "testing"
7 "time"
8
9 _ "modernc.org/sqlite"
10)
11
12func nowForTest() time.Time { return time.Unix(1700000100, 0) }
13
14// The apps table is REBUILT on upgrade (SQLite cannot ALTER a primary key,
15// and "unique within an account" is not something PRIMARY KEY(app) can
16// express). A table rebuild is where data quietly dies, so this seeds the
17// pre-accounts shape by hand, re-opens through the real migration, and
18// asserts every row survived with every column intact.
19func TestAppsRebuildPreservesEveryRow(t *testing.T) {
20 path := filepath.Join(t.TempDir(), "console.db")
21
22 // The exact pre-multi-account shape: PRIMARY KEY(app), no account_id.
23 raw, err := sql.Open("sqlite", path)
24 if err != nil {
25 t.Fatal(err)
26 }
27 if _, err := raw.Exec(`CREATE TABLE apps (
28 app TEXT PRIMARY KEY,
29 created_by TEXT NOT NULL,
30 created_at INTEGER NOT NULL
31);`); err != nil {
32 t.Fatal(err)
33 }
34 seed := map[string][2]any{
35 "kass": {"paul@keymail.dev", int64(1700000000)},
36 "keymail": {"paul@keymail.dev", int64(1700000001)},
37 "woodstar": {"sam@keymail.dev", int64(1700000002)},
38 }
39 for app, row := range seed {
40 if _, err := raw.Exec(`INSERT INTO apps (app, created_by, created_at) VALUES (?, ?, ?)`,
41 app, row[0], row[1]); err != nil {
42 t.Fatal(err)
43 }
44 }
45 if err := raw.Close(); err != nil {
46 t.Fatal(err)
47 }
48
49 // Two opens: the rebuild must also be idempotent, so a console that
50 // restarts twice does not rebuild twice (or lose anything on the second).
51 for pass := 1; pass <= 2; pass++ {
52 st, err := Open(path)
53 if err != nil {
54 t.Fatalf("pass %d: open: %v", pass, err)
55 }
56 rows, err := st.db.Query(`SELECT app, account_id, created_by, created_at FROM apps ORDER BY app`)
57 if err != nil {
58 t.Fatalf("pass %d: %v", pass, err)
59 }
60 got := map[string][3]any{}
61 for rows.Next() {
62 var app, by string
63 var acct, at int64
64 if err := rows.Scan(&app, &acct, &by, &at); err != nil {
65 t.Fatal(err)
66 }
67 got[app] = [3]any{acct, by, at}
68 }
69 rows.Close()
70 st.Close()
71
72 if len(got) != len(seed) {
73 t.Fatalf("pass %d: %d rows survived, want %d: %+v", pass, len(got), len(seed), got)
74 }
75 for app, want := range seed {
76 g, ok := got[app]
77 if !ok {
78 t.Errorf("pass %d: row %q vanished in the rebuild", pass, app)
79 continue
80 }
81 // account_id defaults to 1 (ops): before accounts existed, every
82 // app was in ops by definition.
83 if g[0] != int64(OpsAccountID) {
84 t.Errorf("pass %d: %s account_id = %v, want %d", pass, app, g[0], OpsAccountID)
85 }
86 if g[1] != want[0] {
87 t.Errorf("pass %d: %s created_by = %v, want %v", pass, app, g[1], want[0])
88 }
89 if g[2] != want[1] {
90 t.Errorf("pass %d: %s created_at = %v, want %v", pass, app, g[2], want[1])
91 }
92 }
93 }
94
95 // And the point of the rebuild: the same name in two accounts.
96 st, err := Open(path)
97 if err != nil {
98 t.Fatal(err)
99 }
100 defer st.Close()
101 if err := st.CreateAccount(42, "cdef", "Other", "sam@keymail.dev", nowForTest()); err != nil {
102 t.Fatal(err)
103 }
104 if created, err := st.CreateAppIn(42, "kass", "sam@keymail.dev", nowForTest()); err != nil || !created {
105 t.Fatalf("same name in another account = %v %v, want it to be free", created, err)
106 }
107}
108
109// TestAppFlagsSurviveTheAppsKeyRebuild: control_plane and build_probe are
110// added by the ALTER block, which runs BEFORE migrateAppsKey's table rebuild
111// (store.go:189 vs :231). The rebuild copies an ENUMERATED column list, so a
112// column missing from apps_new is silently dropped on any database that has
113// not been rebuilt yet — a self-hoster jumping versions. deleted_at and
114// placement are in both places for this reason (store.go:280); these must be
115// too, and this test is what proves it rather than a comment.
116func TestAppFlagsSurviveTheAppsKeyRebuild(t *testing.T) {
117 dir := t.TempDir()
118 path := filepath.Join(dir, "old.db")
119
120 // A pre-rebuild database: PRIMARY KEY(app), the shape migrateAppsKey exists
121 // to replace. Built by hand because no released binary produces it any more.
122 raw, err := sql.Open("sqlite", path)
123 if err != nil {
124 t.Fatal(err)
125 }
126 if _, err := raw.Exec(`CREATE TABLE apps (
127 app TEXT PRIMARY KEY,
128 account_id INTEGER NOT NULL DEFAULT 1,
129 created_by TEXT NOT NULL,
130 created_at INTEGER NOT NULL,
131 deleted_at INTEGER NOT NULL DEFAULT 0,
132 placement TEXT NOT NULL DEFAULT ''
133);`); err != nil {
134 t.Fatal(err)
135 }
136 if _, err := raw.Exec(`INSERT INTO apps (app, account_id, created_by, created_at)
137VALUES ('console', 1, 'paul@keymail.dev', 1)`); err != nil {
138 t.Fatal(err)
139 }
140 if err := raw.Close(); err != nil {
141 t.Fatal(err)
142 }
143
144 st, err := Open(path)
145 if err != nil {
146 t.Fatalf("Open (which runs migrate) on a pre-rebuild database: %v", err)
147 }
148 defer st.Close()
149
150 if err := st.SetAppControlPlane(OpsAccountID, "console", true); err != nil {
151 t.Fatalf("SetAppControlPlane: %v — the column did not survive the rebuild", err)
152 }
153 if err := st.SetAppBuildProbe(OpsAccountID, "console", "/healthz"); err != nil {
154 t.Fatalf("SetAppBuildProbe: %v", err)
155 }
156 cp, probe, err := st.AppFlags(OpsAccountID, "console")
157 if err != nil {
158 t.Fatalf("AppFlags: %v", err)
159 }
160 if !cp || probe != "/healthz" {
161 t.Fatalf("AppFlags = %v %q, want true \"/healthz\"", cp, probe)
162 }
163}
164
165// TestAppFlagsDefaultOff: every row that existed before this change means
166// "not the control plane, no probe" — the additive-default idiom the kind and
167// service-principal columns used.
168func TestAppFlagsDefaultOff(t *testing.T) {
169 fx := newConsoleFixture(t)
170 if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil {
171 t.Fatal(err)
172 }
173 cp, probe, err := fx.st.AppFlags(OpsAccountID, "titogo")
174 if err != nil {
175 t.Fatal(err)
176 }
177 if cp || probe != "" {
178 t.Fatalf("a freshly claimed app = %v %q, want false \"\"", cp, probe)
179 }
180}
181