internal/store/idreuse_test.go
484 lines · 19976 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 alice, err := s.UserByUsername("alice")
153 if err != nil {
154 t.Fatal(err)
155 }
156 if _, err := s.DB.Exec("DELETE FROM users WHERE id = ?", alice.ID); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
157 t.Fatalf("deleting a repository owner: %v", err)
158 }
159 if _, err := s.DB.Exec("DELETE FROM orgs WHERE name = 'acme'"); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
160 t.Fatalf("deleting an owning org: %v", err)
161 }
162 // Cascades still reach the rebuilt tables' children.
163 mustExec(t, s, "DELETE FROM push_devices WHERE token = 'devtok'")
164 if got := count(t, s, "push_queue"); got != 0 {
165 t.Fatalf("push_queue after its device went: %d rows", got)
166 }
167
168 // The sequences start above the ids a deploy key and a grant still
169 // name, though neither row survived.
170 var seq int64
171 s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = 'repos'").Scan(&seq)
172 if seq < goneRepo {
173 t.Fatalf("repos sequence %d, want at least %d", seq, goneRepo)
174 }
175 r, err := s.CreateRepo("user", alice.ID, "new", "public")
176 if err != nil {
177 t.Fatal(err)
178 }
179 if r <= goneRepo {
180 t.Fatalf("new repository took id %d; a deploy key still names %d", r, goneRepo)
181 }
182 u, err := s.CreateUser("dave", false)
183 if err != nil {
184 t.Fatal(err)
185 }
186 if u <= goneUser {
187 t.Fatalf("new account took id %d; a grant still names %d", u, goneUser)
188 }
189
190 // Down and up again keep the rows.
191 if err := s.MigrateTo(v - 1); err != nil {
192 t.Fatal(err)
193 }
194 if got := count(t, s, "users"); got != before["users"]+1 {
195 t.Fatalf("users after down: %d", got)
196 }
197 if err := s.DB.QueryRow("SELECT COUNT(*) FROM sqlite_sequence WHERE name = 'users'").Scan(&n); err != nil || n != 0 {
198 t.Fatalf("sqlite_sequence keeps users after down: %d, %v", n, err)
199 }
200 fkClean(t, s)
201 if err := s.MigrateUp(); err != nil {
202 t.Fatal(err)
203 }
204 fkClean(t, s)
205}
206
207// A parked profile about text names its owner by kind and id with no
208// foreign key; the sequences start above the ids it names (#306).
209func TestIDMigrationSeedsFromAboutBackfill(t *testing.T) {
210 v := idMigration(t)
211 s := open(t)
212 if err := s.MigrateTo(v - 1); err != nil {
213 t.Fatal(err)
214 }
215 mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
216 mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
217 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
218 VALUES ('user', 50, 'a', 'md'), ('org', 40, 'b', 'md')`)
219 if err := s.MigrateTo(v); err != nil {
220 t.Fatal(err)
221 }
222 for table, want := range map[string]int64{"users": 50, "orgs": 40} {
223 var seq int64
224 if err := s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = ?", table).Scan(&seq); err != nil {
225 t.Fatal(err)
226 }
227 if seq < want {
228 t.Errorf("%s sequence %d, want at least %d", table, seq, want)
229 }
230 }
231}
232
233// Deleting the row with the highest id does not free that id, in any of
234// the rebuilt tables.
235func TestIDsNotReusedAfterDelete(t *testing.T) {
236 s := open(t)
237 if err := s.MigrateUp(); err != nil {
238 t.Fatal(err)
239 }
240 u1 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u1')")
241 u2 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u2')")
242 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u1)
243 hook := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/h')", repo)
244 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
245 dev := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'devtok')", u1)
246
247 cases := []struct {
248 table, insert string
249 args func(i int) []any
250 }{
251 {"users", "INSERT INTO users (username) VALUES (?)", func(i int) []any { return []any{"x" + itoa(int64(i))} }},
252 {"orgs", "INSERT INTO orgs (name) VALUES (?)", func(i int) []any { return []any{"o" + itoa(int64(i))} }},
253 {"repos", "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, ?, 'public')",
254 func(i int) []any { return []any{u1, "n" + itoa(int64(i))} }},
255 {"api_tokens", "INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, ?, ?)",
256 func(i int) []any { return []any{u2, "t" + itoa(int64(i)), "h" + itoa(int64(i))} }},
257 {"ssh_keys", "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob) VALUES (?, ?, 'ssh-ed25519', x'00')",
258 func(i int) []any { return []any{u2, "SHA256:" + itoa(int64(i))} }},
259 {"webhook_deliveries", "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)",
260 func(int) []any { return []any{hook, ev} }},
261 {"push_queue", "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')",
262 func(int) []any { return []any{dev} }},
263 }
264 for _, c := range cases {
265 first := mustExec(t, s, c.insert, c.args(1)...)
266 mustExec(t, s, "DELETE FROM "+c.table+" WHERE id = ?", first)
267 second := mustExec(t, s, c.insert, c.args(2)...)
268 if second <= first {
269 t.Errorf("%s: id %d handed out again after its row was deleted (got %d)", c.table, first, second)
270 }
271 }
272}
273
274// A sender marks a webhook delivery or a push by id after its request
275// returns. If the hook or device was removed meanwhile, the cascade took
276// the row, and the next row must not take its id and its mark (#306).
277func TestInFlightMarksAfterCascade(t *testing.T) {
278 s := open(t)
279 if err := s.MigrateUp(); err != nil {
280 t.Fatal(err)
281 }
282 u := mustExec(t, s, "INSERT INTO users (username) VALUES ('u')")
283 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u)
284 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
285 h1 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/1')", repo)
286 h2 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/2')", repo)
287 inFlight := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h1, ev)
288 if err := s.RemoveWebhook(repo, h1); err != nil {
289 t.Fatal(err)
290 }
291 next := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h2, ev)
292 if err := s.MarkDelivered(inFlight, 200); err != nil {
293 t.Fatal(err)
294 }
295 var delivered *string
296 if err := s.DB.QueryRow("SELECT delivered_at FROM webhook_deliveries WHERE id = ?", next).Scan(&delivered); err != nil {
297 t.Fatal(err)
298 }
299 if delivered != nil {
300 t.Fatal("the removed hook's delivery was marked on the next hook's")
301 }
302
303 d1 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd1')", u)
304 d2 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd2')", u)
305 pushing := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d1)
306 if err := s.RemovePushDevice(u, d1); err != nil {
307 t.Fatal(err)
308 }
309 queued := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d2)
310 if err := s.MarkPushSent(pushing); err != nil {
311 t.Fatal(err)
312 }
313 var sent *string
314 if err := s.DB.QueryRow("SELECT sent_at FROM push_queue WHERE id = ?", queued).Scan(&sent); err != nil {
315 t.Fatal(err)
316 }
317 if sent != nil {
318 t.Fatal("the removed device's push was marked on the next device's")
319 }
320}
321
322// Deleting a repository takes the deploy keys scoped to it, and only
323// those; deleting an account or an org takes its grants (#306).
324func TestDeletesTakeGrantsAndDeployKeys(t *testing.T) {
325 s := open(t)
326 if err := s.MigrateUp(); err != nil {
327 t.Fatal(err)
328 }
329 var revoked []Revoked
330 s.OnRevoke(func(r Revoked) { revoked = append(revoked, r) })
331 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
332 var repos []int64
333 for i := range 12 {
334 repos = append(repos, mustExec(t, s,
335 "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, ?, 'public')", alice, "r"+itoa(int64(i))))
336 }
337 // repos[0] is 2 and repos[10] is 12: a prefix of one id must not
338 // match the other.
339 gone, other := repos[0], repos[len(repos)-2]
340 if !strings.HasPrefix(itoa(other), itoa(gone)) {
341 t.Fatalf("ids %d and %d do not share a prefix", gone, other)
342 }
343 goneKey := mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:g', 'a', x'00', ?)",
344 alice, "deploy:"+itoa(gone)+":rw")
345 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:o', 'a', x'00', ?)",
346 alice, "deploy:"+itoa(other)+":ro")
347 if err := s.DeleteRepo(gone); err != nil {
348 t.Fatal(err)
349 }
350 var n int
351 s.DB.QueryRow("SELECT COUNT(*) FROM ssh_keys WHERE fingerprint = 'SHA256:g'").Scan(&n)
352 if n != 0 {
353 t.Fatal("the deleted repository's deploy key survived")
354 }
355 s.DB.QueryRow("SELECT COUNT(*) FROM ssh_keys WHERE fingerprint = 'SHA256:o'").Scan(&n)
356 if n != 1 {
357 t.Fatal("another repository's deploy key went with it")
358 }
359 if len(revoked) != 1 || len(revoked[0].KeyIDs) != 1 || revoked[0].KeyIDs[0] != goneKey {
360 t.Fatalf("revocations announced: %+v", revoked)
361 }
362 if err := s.DeleteRepo(gone); err != ErrNotFound {
363 t.Fatalf("deleting it again: %v", err)
364 }
365
366 bob := mustExec(t, s, "INSERT INTO users (username) VALUES ('bob')")
367 org := mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
368 if err := s.GrantAccess(other, bob, "write"); err != nil {
369 t.Fatal(err)
370 }
371 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', ?, 'read')", other, org)
372 // An org with the same id as bob's keeps its grant when bob goes.
373 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', ?, 'read')", repos[1], bob)
374 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
375 VALUES ('user', ?, 'a', 'md'), ('org', ?, 'b', 'md'), ('org', ?, 'c', 'md')`, bob, org, bob)
376 if err := s.DeleteUser(bob); err != nil {
377 t.Fatal(err)
378 }
379 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'user' AND subject_id = ?", bob).Scan(&n)
380 if n != 0 {
381 t.Fatal("the deleted account's grant survived")
382 }
383 s.DB.QueryRow("SELECT COUNT(*) FROM profile_about_backfill WHERE owner_kind = 'user' AND owner_id = ?", bob).Scan(&n)
384 if n != 0 {
385 t.Fatal("the deleted account's about text survived")
386 }
387 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'org' AND subject_id = ?", bob).Scan(&n)
388 if n != 1 {
389 t.Fatal("an org grant went with the account of the same id")
390 }
391 if err := s.DeleteOrg(org); err != nil {
392 t.Fatal(err)
393 }
394 s.DB.QueryRow("SELECT COUNT(*) FROM repo_access WHERE subject_kind = 'org' AND subject_id = ?", org).Scan(&n)
395 if n != 0 {
396 t.Fatal("the deleted org's grant survived")
397 }
398 s.DB.QueryRow("SELECT COUNT(*) FROM profile_about_backfill WHERE owner_kind = 'org'").Scan(&n)
399 if n != 1 {
400 t.Fatalf("org about texts after deleting acme: %d, want only the one with bob's id", n)
401 }
402}
403
404// The cleanup migration removes grants and deploy keys left by earlier
405// deletes, keeps live ones, and leaves a note with the counts.
406func TestOrphanCleanupMigration(t *testing.T) {
407 ms, err := loadMigrations()
408 if err != nil {
409 t.Fatal(err)
410 }
411 v := 0
412 for _, m := range ms {
413 if m.name == "orphan_grants_deploy_keys" {
414 v = m.version
415 }
416 }
417 if v == 0 {
418 t.Fatal("no orphan_grants_deploy_keys migration")
419 }
420 s := open(t)
421 if err := s.MigrateTo(v - 1); err != nil {
422 t.Fatal(err)
423 }
424 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
425 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", alice)
426 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'user', ?, 'read')", repo, alice)
427 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'user', 999, 'write')", repo)
428 mustExec(t, s, "INSERT INTO repo_access (repo_id, subject_kind, subject_id, role) VALUES (?, 'org', 998, 'read')", repo)
429 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:live', 'a', x'00', ?)",
430 alice, "deploy:"+itoa(repo)+":rw")
431 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:dead', 'a', x'00', 'deploy:997:ro')", alice)
432 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob) VALUES (?, 'SHA256:user', 'a', x'00')", alice)
433 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
434 VALUES ('user', ?, 'live', 'md'), ('user', 996, 'dead', 'md'), ('org', 995, 'dead', 'md')`, alice)
435 var epoch int
436 s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&epoch)
437 if err := s.MigrateTo(v); err != nil {
438 t.Fatal(err)
439 }
440 if got := count(t, s, "repo_access"); got != 1 {
441 t.Fatalf("repo_access: %d rows, want the live grant", got)
442 }
443 var about string
444 if err := s.DB.QueryRow("SELECT group_concat(about) FROM profile_about_backfill").Scan(&about); err != nil || about != "live" {
445 t.Fatalf("about texts after cleanup: %q, %v", about, err)
446 }
447 var fps []string
448 rows, err := s.DB.Query("SELECT fingerprint FROM ssh_keys ORDER BY fingerprint")
449 if err != nil {
450 t.Fatal(err)
451 }
452 for rows.Next() {
453 var fp string
454 rows.Scan(&fp)
455 fps = append(fps, fp)
456 }
457 rows.Close()
458 if strings.Join(fps, " ") != "SHA256:live SHA256:user" {
459 t.Fatalf("keys after cleanup: %v", fps)
460 }
461 var after int
462 s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&after)
463 if after != epoch+1 {
464 t.Fatalf("key_epoch %d, want %d", after, epoch+1)
465 }
466 note, err := s.TakeMigrationNote()
467 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" {
468 t.Fatalf("note %q, %v", note, err)
469 }
470 if note, _ := s.TakeMigrationNote(); note != "" {
471 t.Fatalf("note not cleared: %q", note)
472 }
473
474 // Nothing to remove, no note.
475 if err := s.MigrateTo(v - 1); err != nil {
476 t.Fatal(err)
477 }
478 if err := s.MigrateTo(v); err != nil {
479 t.Fatal(err)
480 }
481 if note, _ := s.TakeMigrationNote(); note != "" {
482 t.Fatalf("note with nothing removed: %q", note)
483 }
484}