internal/store/idreuse_test.go
485 lines · 20111 bytes
1package store
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7)
8
9// idMigration is the version of the migration that makes ids
10// AUTOINCREMENT (#306), found by name so the test survives renumbering.
11func idMigration(t *testing.T) int {
12 t.Helper()
13 ms, err := loadMigrations()
14 if err != nil {
15 t.Fatal(err)
16 }
17 for _, m := range ms {
18 if m.name == "id_autoincrement" {
19 return m.version
20 }
21 }
22 t.Fatal("no id_autoincrement migration")
23 return 0
24}
25
26var autoincTables = []string{"users", "orgs", "repos", "api_tokens", "ssh_keys", "webhook_deliveries", "push_queue"}
27
28func mustExec(t *testing.T, s *Store, q string, args ...any) int64 {
29 t.Helper()
30 res, err := s.DB.Exec(q, args...)
31 if err != nil {
32 t.Fatalf("%s: %v", q, err)
33 }
34 id, _ := res.LastInsertId()
35 return id
36}
37
38func count(t *testing.T, s *Store, table string) int {
39 t.Helper()
40 var n int
41 if err := s.DB.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n); err != nil {
42 t.Fatal(err)
43 }
44 return n
45}
46
47func fkClean(t *testing.T, s *Store) {
48 t.Helper()
49 rows, err := s.DB.Query("PRAGMA foreign_key_check")
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer rows.Close()
54 if rows.Next() {
55 var table, parent string
56 var rowid, fk any
57 rows.Scan(&table, &rowid, &parent, &fk)
58 t.Fatalf("foreign_key_check: %s row %v -> %s", table, rowid, parent)
59 }
60}
61
62// seedForIDs fills every rebuilt table and the rows that name their ids,
63// then deletes the newest repository and the newest account while a
64// deploy key and a grant still name them.
65func seedForIDs(t *testing.T, s *Store) (goneRepo, goneUser int64) {
66 t.Helper()
67 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
68 org := mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
69 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'app', 'public')", alice)
70 mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility, fork_of) VALUES ('org', ?, 'fork', 'private', ?)", org, repo)
71 tok := mustExec(t, s, "INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, 't1', 'h1')", alice)
72 mustExec(t, s, "INSERT INTO api_tokens (user_id, name, token_hash, created_by_token) VALUES (?, 't2', 'h2', ?)", alice, tok)
73 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, created_by_token) VALUES (?, 'SHA256:a', 'ssh-ed25519', x'00', ?)", alice, tok)
74 hook := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/h')", repo)
75 ev := mustExec(t, s, "INSERT INTO events (repo_id, actor_id, kind) VALUES (?, ?, 'push')", repo, alice)
76 mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", hook, ev)
77 dev := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'devtok')", alice)
78 mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", dev)
79 mustExec(t, s, "INSERT INTO org_members (org_id, user_id, role) VALUES (?, ?, 'admin')", org, alice)
80
81 goneRepo = mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'gone', 'private')", alice)
82 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:d', 'ssh-ed25519', x'00', ?)",
83 alice, "deploy:"+itoa(goneRepo)+":rw")
84 // Raw deletes: DeleteRepo and DeleteUser now take the deploy key and
85 // the grant with them, and these orphans stand for ones left earlier.
86 mustExec(t, s, "DELETE FROM repos WHERE id = ?", goneRepo)
87 goneUser = mustExec(t, s, "INSERT INTO users (username) VALUES ('carol')")
88 if err := s.GrantAccess(repo, goneUser, "write"); err != nil {
89 t.Fatal(err)
90 }
91 mustExec(t, s, "DELETE FROM users WHERE id = ?", goneUser)
92 return goneRepo, goneUser
93}
94
95func itoa(n int64) string { return strconv.FormatInt(n, 10) }
96
97func TestIDMigrationKeepsRowsAndForeignKeys(t *testing.T) {
98 v := idMigration(t)
99 s := open(t)
100 if err := s.MigrateTo(v - 1); err != nil {
101 t.Fatal(err)
102 }
103 goneRepo, goneUser := seedForIDs(t, s)
104 before := map[string]int{}
105 for _, tbl := range append(autoincTables, "org_members", "webhooks", "events", "push_devices", "repo_access") {
106 before[tbl] = count(t, s, tbl)
107 }
108 if err := s.MigrateTo(v); err != nil {
109 t.Fatal(err)
110 }
111 for tbl, n := range before {
112 if got := count(t, s, tbl); got != n {
113 t.Errorf("%s: %d rows after the migration, want %d", tbl, got, n)
114 }
115 }
116 for _, tbl := range autoincTables {
117 var sql string
118 if err := s.DB.QueryRow("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", tbl).Scan(&sql); err != nil {
119 t.Fatal(err)
120 }
121 if !strings.Contains(sql, "AUTOINCREMENT") {
122 t.Errorf("%s is not AUTOINCREMENT", tbl)
123 }
124 }
125 fkClean(t, s)
126
127 // Children still name the rebuilt parents, not the *_old tables.
128 var n int
129 if err := s.DB.QueryRow(`SELECT COUNT(*) FROM sqlite_master m, pragma_foreign_key_list(m.name) f
130 WHERE m.type = 'table' AND f."table" LIKE '%_old'`).Scan(&n); err != nil {
131 t.Fatal(err)
132 }
133 if n != 0 {
134 t.Fatalf("%d foreign keys name a *_old table", n)
135 }
136 for _, c := range []struct{ child, parent string }{
137 {"ssh_keys", "users"}, {"ssh_keys", "api_tokens"}, {"api_tokens", "api_tokens"},
138 {"repos", "repos"}, {"issues", "repos"}, {"teams", "orgs"}, {"mr_merge_queue", "ssh_keys"},
139 {"push_queue", "push_devices"}, {"webhook_deliveries", "webhooks"},
140 } {
141 if err := s.DB.QueryRow(`SELECT COUNT(*) FROM pragma_foreign_key_list(?) WHERE "table" = ?`, c.child, c.parent).Scan(&n); err != nil || n == 0 {
142 t.Errorf("%s has no foreign key to %s (%v)", c.child, c.parent, err)
143 }
144 }
145 for _, idx := range []string{"ssh_keys_user", "webhook_deliveries_due", "push_queue_due"} {
146 if err := s.DB.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = ?", idx).Scan(&n); err != nil || n != 1 {
147 t.Errorf("index %s missing", idx)
148 }
149 }
150
151 // The triggers still fire.
152 // Read by column: the schema here is 0074's, older than User's loader.
153 var alice User
154 if err := s.DB.QueryRow("SELECT id FROM users WHERE username = 'alice'").Scan(&alice.ID); err != nil {
155 t.Fatal(err)
156 }
157 if _, err := s.DB.Exec("DELETE FROM users WHERE id = ?", alice.ID); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
158 t.Fatalf("deleting a repository owner: %v", err)
159 }
160 if _, err := s.DB.Exec("DELETE FROM orgs WHERE name = 'acme'"); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
161 t.Fatalf("deleting an owning org: %v", err)
162 }
163 // Cascades still reach the rebuilt tables' children.
164 mustExec(t, s, "DELETE FROM push_devices WHERE token = 'devtok'")
165 if got := count(t, s, "push_queue"); got != 0 {
166 t.Fatalf("push_queue after its device went: %d rows", got)
167 }
168
169 // The sequences start above the ids a deploy key and a grant still
170 // name, though neither row survived.
171 var seq int64
172 s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = 'repos'").Scan(&seq)
173 if seq < goneRepo {
174 t.Fatalf("repos sequence %d, want at least %d", seq, goneRepo)
175 }
176 r, err := s.CreateRepo("user", alice.ID, "new", "public")
177 if err != nil {
178 t.Fatal(err)
179 }
180 if r <= goneRepo {
181 t.Fatalf("new repository took id %d; a deploy key still names %d", r, goneRepo)
182 }
183 u, err := s.CreateUser("dave", false)
184 if err != nil {
185 t.Fatal(err)
186 }
187 if u <= goneUser {
188 t.Fatalf("new account took id %d; a grant still names %d", u, goneUser)
189 }
190
191 // Down and up again keep the rows.
192 if err := s.MigrateTo(v - 1); err != nil {
193 t.Fatal(err)
194 }
195 if got := count(t, s, "users"); got != before["users"]+1 {
196 t.Fatalf("users after down: %d", got)
197 }
198 if err := s.DB.QueryRow("SELECT COUNT(*) FROM sqlite_sequence WHERE name = 'users'").Scan(&n); err != nil || n != 0 {
199 t.Fatalf("sqlite_sequence keeps users after down: %d, %v", n, err)
200 }
201 fkClean(t, s)
202 if err := s.MigrateUp(); err != nil {
203 t.Fatal(err)
204 }
205 fkClean(t, s)
206}
207
208// A parked profile about text names its owner by kind and id with no
209// foreign key; the sequences start above the ids it names (#306).
210func TestIDMigrationSeedsFromAboutBackfill(t *testing.T) {
211 v := idMigration(t)
212 s := open(t)
213 if err := s.MigrateTo(v - 1); err != nil {
214 t.Fatal(err)
215 }
216 mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
217 mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
218 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
219 VALUES ('user', 50, 'a', 'md'), ('org', 40, 'b', 'md')`)
220 if err := s.MigrateTo(v); err != nil {
221 t.Fatal(err)
222 }
223 for table, want := range map[string]int64{"users": 50, "orgs": 40} {
224 var seq int64
225 if err := s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = ?", table).Scan(&seq); err != nil {
226 t.Fatal(err)
227 }
228 if seq < want {
229 t.Errorf("%s sequence %d, want at least %d", table, seq, want)
230 }
231 }
232}
233
234// Deleting the row with the highest id does not free that id, in any of
235// the rebuilt tables.
236func TestIDsNotReusedAfterDelete(t *testing.T) {
237 s := open(t)
238 if err := s.MigrateUp(); err != nil {
239 t.Fatal(err)
240 }
241 u1 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u1')")
242 u2 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u2')")
243 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u1)
244 hook := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/h')", repo)
245 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
246 dev := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'devtok')", u1)
247
248 cases := []struct {
249 table, insert string
250 args func(i int) []any
251 }{
252 {"users", "INSERT INTO users (username) VALUES (?)", func(i int) []any { return []any{"x" + itoa(int64(i))} }},
253 {"orgs", "INSERT INTO orgs (name) VALUES (?)", func(i int) []any { return []any{"o" + itoa(int64(i))} }},
254 {"repos", "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, ?, 'public')",
255 func(i int) []any { return []any{u1, "n" + itoa(int64(i))} }},
256 {"api_tokens", "INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, ?, ?)",
257 func(i int) []any { return []any{u2, "t" + itoa(int64(i)), "h" + itoa(int64(i))} }},
258 {"ssh_keys", "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob) VALUES (?, ?, 'ssh-ed25519', x'00')",
259 func(i int) []any { return []any{u2, "SHA256:" + itoa(int64(i))} }},
260 {"webhook_deliveries", "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)",
261 func(int) []any { return []any{hook, ev} }},
262 {"push_queue", "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')",
263 func(int) []any { return []any{dev} }},
264 }
265 for _, c := range cases {
266 first := mustExec(t, s, c.insert, c.args(1)...)
267 mustExec(t, s, "DELETE FROM "+c.table+" WHERE id = ?", first)
268 second := mustExec(t, s, c.insert, c.args(2)...)
269 if second <= first {
270 t.Errorf("%s: id %d handed out again after its row was deleted (got %d)", c.table, first, second)
271 }
272 }
273}
274
275// A sender marks a webhook delivery or a push by id after its request
276// returns. If the hook or device was removed meanwhile, the cascade took
277// the row, and the next row must not take its id and its mark (#306).
278func TestInFlightMarksAfterCascade(t *testing.T) {
279 s := open(t)
280 if err := s.MigrateUp(); err != nil {
281 t.Fatal(err)
282 }
283 u := mustExec(t, s, "INSERT INTO users (username) VALUES ('u')")
284 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u)
285 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
286 h1 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/1')", repo)
287 h2 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/2')", repo)
288 inFlight := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h1, ev)
289 if err := s.RemoveWebhook(repo, h1); err != nil {
290 t.Fatal(err)
291 }
292 next := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h2, ev)
293 if err := s.MarkDelivered(inFlight, 200); err != nil {
294 t.Fatal(err)
295 }
296 var delivered *string
297 if err := s.DB.QueryRow("SELECT delivered_at FROM webhook_deliveries WHERE id = ?", next).Scan(&delivered); err != nil {
298 t.Fatal(err)
299 }
300 if delivered != nil {
301 t.Fatal("the removed hook's delivery was marked on the next hook's")
302 }
303
304 d1 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd1')", u)
305 d2 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd2')", u)
306 pushing := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d1)
307 if err := s.RemovePushDevice(u, d1); err != nil {
308 t.Fatal(err)
309 }
310 queued := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d2)
311 if err := s.MarkPushSent(pushing); err != nil {
312 t.Fatal(err)
313 }
314 var sent *string
315 if err := s.DB.QueryRow("SELECT sent_at FROM push_queue WHERE id = ?", queued).Scan(&sent); err != nil {
316 t.Fatal(err)
317 }
318 if sent != nil {
319 t.Fatal("the removed device's push was marked on the next device's")
320 }
321}
322
323// Deleting a repository takes the deploy keys scoped to it, and only
324// those; deleting an account or an org takes its grants (#306).
325func TestDeletesTakeGrantsAndDeployKeys(t *testing.T) {
326 s := open(t)
327 if err := s.MigrateUp(); err != nil {
328 t.Fatal(err)
329 }
330 var revoked []Revoked
331 s.OnRevoke(func(r Revoked) { revoked = append(revoked, r) })
332 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
333 var repos []int64
334 for i := range 12 {
335 repos = append(repos, mustExec(t, s,
336 "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, ?, 'public')", alice, "r"+itoa(int64(i))))
337 }
338 // repos[0] is 2 and repos[10] is 12: a prefix of one id must not
339 // match the other.
340 gone, other := repos[0], repos[len(repos)-2]
341 if !strings.HasPrefix(itoa(other), itoa(gone)) {
342 t.Fatalf("ids %d and %d do not share a prefix", gone, other)
343 }
344 goneKey := mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:g', 'a', x'00', ?)",
345 alice, "deploy:"+itoa(gone)+":rw")
346 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:o', 'a', x'00', ?)",
347 alice, "deploy:"+itoa(other)+":ro")
348 if err := s.DeleteRepo(gone); err != nil {
349 t.Fatal(err)
350 }
351 var n int
352 s.DB.QueryRow("SELECT COUNT(*) FROM ssh_keys WHERE fingerprint = 'SHA256:g'").Scan(&n)
353 if n != 0 {
354 t.Fatal("the deleted repository's deploy key survived")
355 }
356 s.DB.QueryRow("SELECT COUNT(*) FROM ssh_keys WHERE fingerprint = 'SHA256:o'").Scan(&n)
357 if n != 1 {
358 t.Fatal("another repository's deploy key went with it")
359 }
360 if len(revoked) != 1 || len(revoked[0].KeyIDs) != 1 || revoked[0].KeyIDs[0] != goneKey {
361 t.Fatalf("revocations announced: %+v", revoked)
362 }
363 if err := s.DeleteRepo(gone); err != ErrNotFound {
364 t.Fatalf("deleting it again: %v", err)
365 }
366
367 bob := mustExec(t, s, "INSERT INTO users (username) VALUES ('bob')")
368 org := mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
369 if err := s.GrantAccess(other, bob, "write"); err != nil {
370 t.Fatal(err)
371 }
372 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', ?, 'read')", other, org)
373 // An org with the same id as bob's keeps its grant when bob goes.
374 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', ?, 'read')", repos[1], bob)
375 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
376 VALUES ('user', ?, 'a', 'md'), ('org', ?, 'b', 'md'), ('org', ?, 'c', 'md')`, bob, org, bob)
377 if err := s.DeleteUser(bob); err != nil {
378 t.Fatal(err)
379 }
380 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'user' AND subject_id = ?", bob).Scan(&n)
381 if n != 0 {
382 t.Fatal("the deleted account's grant survived")
383 }
384 s.DB.QueryRow("SELECT COUNT(*) FROM profile_about_backfill WHERE owner_kind = 'user' AND owner_id = ?", bob).Scan(&n)
385 if n != 0 {
386 t.Fatal("the deleted account's about text survived")
387 }
388 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'org' AND subject_id = ?", bob).Scan(&n)
389 if n != 1 {
390 t.Fatal("an org grant went with the account of the same id")
391 }
392 if err := s.DeleteOrg(org); err != nil {
393 t.Fatal(err)
394 }
395 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'org' AND subject_id = ?", org).Scan(&n)
396 if n != 0 {
397 t.Fatal("the deleted org's grant survived")
398 }
399 s.DB.QueryRow("SELECT COUNT(*) FROM profile_about_backfill WHERE owner_kind = 'org'").Scan(&n)
400 if n != 1 {
401 t.Fatalf("org about texts after deleting acme: %d, want only the one with bob's id", n)
402 }
403}
404
405// The cleanup migration removes grants and deploy keys left by earlier
406// deletes, keeps live ones, and leaves a note with the counts.
407func TestOrphanCleanupMigration(t *testing.T) {
408 ms, err := loadMigrations()
409 if err != nil {
410 t.Fatal(err)
411 }
412 v := 0
413 for _, m := range ms {
414 if m.name == "orphan_grants_deploy_keys" {
415 v = m.version
416 }
417 }
418 if v == 0 {
419 t.Fatal("no orphan_grants_deploy_keys migration")
420 }
421 s := open(t)
422 if err := s.MigrateTo(v - 1); err != nil {
423 t.Fatal(err)
424 }
425 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
426 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", alice)
427 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'user', ?, 'read')", repo, alice)
428 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'user', 999, 'write')", repo)
429 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', 998, 'read')", repo)
430 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:live', 'a', x'00', ?)",
431 alice, "deploy:"+itoa(repo)+":rw")
432 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:dead', 'a', x'00', 'deploy:997:ro')", alice)
433 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob) VALUES (?, 'SHA256:user', 'a', x'00')", alice)
434 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
435 VALUES ('user', ?, 'live', 'md'), ('user', 996, 'dead', 'md'), ('org', 995, 'dead', 'md')`, alice)
436 var epoch int
437 s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&epoch)
438 if err := s.MigrateTo(v); err != nil {
439 t.Fatal(err)
440 }
441 if got := count(t, s, "repo_access"); got != 1 {
442 t.Fatalf("repo_access: %d rows, want the live grant", got)
443 }
444 var about string
445 if err := s.DB.QueryRow("SELECT group_concat(about) FROM profile_about_backfill").Scan(&about); err != nil || about != "live" {
446 t.Fatalf("about texts after cleanup: %q, %v", about, err)
447 }
448 var fps []string
449 rows, err := s.DB.Query("SELECT fingerprint FROM ssh_keys ORDER BY fingerprint")
450 if err != nil {
451 t.Fatal(err)
452 }
453 for rows.Next() {
454 var fp string
455 rows.Scan(&fp)
456 fps = append(fps, fp)
457 }
458 rows.Close()
459 if strings.Join(fps, " ") != "SHA256:live SHA256:user" {
460 t.Fatalf("keys after cleanup: %v", fps)
461 }
462 var after int
463 s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&after)
464 if after != epoch+1 {
465 t.Fatalf("key_epoch %d, want %d", after, epoch+1)
466 }
467 note, err := s.TakeMigrationNote()
468 if err != nil || note != "removed grants of deleted accounts or organizations: 2; deploy keys of deleted repositories: 1; profile about texts of deleted accounts or organizations: 2" {
469 t.Fatalf("note %q, %v", note, err)
470 }
471 if note, _ := s.TakeMigrationNote(); note != "" {
472 t.Fatalf("note not cleared: %q", note)
473 }
474
475 // Nothing to remove, no note.
476 if err := s.MigrateTo(v - 1); err != nil {
477 t.Fatal(err)
478 }
479 if err := s.MigrateTo(v); err != nil {
480 t.Fatal(err)
481 }
482 if note, _ := s.TakeMigrationNote(); note != "" {
483 t.Fatalf("note with nothing removed: %q", note)
484 }
485}