1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore_test
4
5import (
6 "context"
7 "slices"
8 "sort"
9 "strconv"
10 "sync"
11 "testing"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15 "google.golang.org/protobuf/proto"
16
17 storagepb "bnema.dev/ermite/gen/ermite/storage"
18 "bnema.dev/ermite/internal/adapters/kvstore"
19 "bnema.dev/ermite/internal/domain"
20 "bnema.dev/ermite/internal/ports"
21)
22
23func newIssue(t *testing.T, repo domain.RepoName, title string) domain.Issue {
24 t.Helper()
25 ti, err := domain.ParseIssueTitle(title)
26 require.NoError(t, err)
27 body, err := domain.ParseIssueBody("what\nhappened")
28 require.NoError(t, err)
29 return domain.Issue{
30 Repo: repo, Title: ti, Body: body, State: domain.IssueStateOpen,
31 Author: "agent", CreatedAt: created, UpdatedAt: created,
32 }
33}
34
35func newIssueComment(t *testing.T, id int64, text string) domain.IssueComment {
36 t.Helper()
37 body, err := domain.ParseIssueBody(text)
38 require.NoError(t, err)
39 return domain.IssueComment{ID: id, Author: "agent", Text: body, CreatedAt: created}
40}
41
42// countKeys counts the keys of the store under prefix.
43func countKeys(t *testing.T, s *kvstore.Store, prefix string) int {
44 t.Helper()
45 return s.CountKeysForTest(context.Background(), prefix)
46}
47
48func TestIssueRoundTripAndPaging(t *testing.T) {
49 ctx := context.Background()
50 s, _, n := repoWithMRs(t)
51 is := s.Issues()
52
53 first, err := is.Create(ctx, newIssue(t, n, "one"))
54 require.NoError(t, err)
55 assert.Equal(t, int64(1), first.Number)
56 assert.NotZero(t, first.Version)
57 got, err := is.Get(ctx, n, 1)
58 require.NoError(t, err)
59 assert.Equal(t, first, got)
60 _, err = is.Get(ctx, n, 2)
61 assert.ErrorIs(t, err, domain.ErrNotFound)
62
63 for _, title := range []string{"two", "three", "four"} {
64 _, err := is.Create(ctx, newIssue(t, n, title))
65 require.NoError(t, err)
66 }
67 closed, err := is.Get(ctx, n, 2)
68 require.NoError(t, err)
69 closed.State = domain.IssueStateClosed
70 require.NoError(t, is.Update(ctx, closed))
71
72 page, err := is.List(ctx, n, ports.ListIssuesParams{PageSize: 3})
73 require.NoError(t, err)
74 require.Len(t, page.Issues, 3)
75 assert.Equal(t, int64(4), page.Issues[0].Number)
76 require.NotEmpty(t, page.NextPageToken)
77 page, err = is.List(ctx, n, ports.ListIssuesParams{PageSize: 3, PageToken: page.NextPageToken})
78 require.NoError(t, err)
79 require.Len(t, page.Issues, 1)
80 assert.Equal(t, int64(1), page.Issues[0].Number)
81 assert.Empty(t, page.NextPageToken)
82
83 page, err = is.List(ctx, n, ports.ListIssuesParams{State: domain.IssueStateClosed})
84 require.NoError(t, err)
85 require.Len(t, page.Issues, 1)
86 assert.Equal(t, int64(2), page.Issues[0].Number)
87
88 _, err = is.List(ctx, n, ports.ListIssuesParams{PageToken: "x"})
89 assert.ErrorIs(t, err, domain.ErrInvalid)
90
91 _, err = is.Create(ctx, newIssue(t, name(t, "ghost"), "x"))
92 assert.ErrorIs(t, err, domain.ErrNotFound)
93}
94
95func TestIssueUpdateNeedsTheVersionItWasReadAt(t *testing.T) {
96 ctx := context.Background()
97 s, _, n := repoWithMRs(t)
98 is := s.Issues()
99 first, err := is.Create(ctx, newIssue(t, n, "t"))
100 require.NoError(t, err)
101 a, b := first, first
102 a.State = domain.IssueStateClosed
103 require.NoError(t, is.Update(ctx, a))
104 b.State = domain.IssueStateClosed
105 assert.ErrorIs(t, is.Update(ctx, b), domain.ErrConflict)
106}
107
108func TestIssueCommentsAndSaveCommentPreconditions(t *testing.T) {
109 ctx := context.Background()
110 s, _, n := repoWithMRs(t)
111 is := s.Issues()
112 issue, err := is.Create(ctx, newIssue(t, n, "t"))
113 require.NoError(t, err)
114
115 issue.CommentSeq = 1
116 require.NoError(t, is.SaveComment(ctx, issue, newIssueComment(t, 1, "first")))
117 // The issue moved on: the same version is refused.
118 issue.CommentSeq = 2
119 assert.ErrorIs(t, is.SaveComment(ctx, issue, newIssueComment(t, 2, "second")), domain.ErrConflict)
120 issue, err = is.Get(ctx, n, issue.Number)
121 require.NoError(t, err)
122 issue.CommentSeq = 2
123 require.NoError(t, is.SaveComment(ctx, issue, newIssueComment(t, 2, "second")))
124
125 comments, err := is.ListComments(ctx, n, issue.Number)
126 require.NoError(t, err)
127 require.Len(t, comments, 2)
128 assert.Equal(t, "first", comments[0].Text.String())
129 assert.Equal(t, int64(2), comments[1].ID)
130
131 require.NoError(t, s.Delete(ctx, n))
132 assert.ErrorIs(t, is.SaveComment(ctx, issue, newIssueComment(t, 3, "late")), domain.ErrNotFound)
133}
134
135func TestConcurrentMergeRequestsAndIssuesNeverShareANumber(t *testing.T) {
136 ctx := context.Background()
137 s, mrs, n := repoWithMRs(t)
138 is := s.Issues()
139 const workers = 16
140 var (
141 wg sync.WaitGroup
142 mu sync.Mutex
143 numbers []int64
144 )
145 for w := range workers {
146 wg.Go(func() {
147 var number int64
148 if w%2 == 0 {
149 got, err := mrs.Create(ctx, newMR(t, n, "t"))
150 if !assert.NoError(t, err) {
151 return
152 }
153 number = got.Number
154 } else {
155 got, err := is.Create(ctx, newIssue(t, n, "t"))
156 if !assert.NoError(t, err) {
157 return
158 }
159 number = got.Number
160 }
161 mu.Lock()
162 numbers = append(numbers, number)
163 mu.Unlock()
164 })
165 }
166 wg.Wait()
167 want := make([]int64, workers)
168 for i := range want {
169 want[i] = int64(i + 1)
170 }
171 assert.ElementsMatch(t, want, numbers, "every number is used once, by an issue or a merge request")
172}
173
174func TestDeleteDropsTheIssuesOfTheRepository(t *testing.T) {
175 ctx := context.Background()
176 s, _, n := repoWithMRs(t)
177 sib := name(t, "proj-2")
178 require.NoError(t, s.Create(ctx, domain.Repo{Name: sib, CreatedAt: created, Visibility: domain.VisibilityPublic}))
179 is := s.Issues()
180
181 for _, repo := range []domain.RepoName{n, sib} {
182 issue, err := is.Create(ctx, newIssue(t, repo, "t"))
183 require.NoError(t, err)
184 issue.CommentSeq = 1
185 require.NoError(t, is.SaveComment(ctx, issue, newIssueComment(t, 1, "note")))
186 }
187 require.NoError(t, s.Delete(ctx, n))
188
189 page, err := is.List(ctx, n, ports.ListIssuesParams{})
190 require.NoError(t, err)
191 assert.Empty(t, page.Issues)
192 comments, err := is.ListComments(ctx, n, 1)
193 require.NoError(t, err)
194 assert.Empty(t, comments)
195 assert.Empty(t, openOf(t, is, n))
196 assert.Zero(t, countKeys(t, s, "is/proj/"))
197 assert.Zero(t, countKeys(t, s, "isc/proj/"))
198 assert.Zero(t, countKeys(t, s, "isopen/proj/"))
199
200 open := openOf(t, is, sib)
201 assert.Len(t, open, 1, "a repository whose name starts like the deleted one keeps its issues")
202 comments, err = is.ListComments(ctx, sib, 1)
203 require.NoError(t, err)
204 assert.Len(t, comments, 1)
205
206 require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
207 again, err := is.Create(ctx, newIssue(t, n, "fresh"))
208 require.NoError(t, err)
209 assert.Equal(t, int64(1), again.Number)
210}
211
212func TestOpenIssueIndexFollowsUpdates(t *testing.T) {
213 ctx := context.Background()
214 s, _, n := repoWithMRs(t)
215 is := s.Issues()
216 a, err := is.Create(ctx, newIssue(t, n, "a"))
217 require.NoError(t, err)
218 b, err := is.Create(ctx, newIssue(t, n, "b"))
219 require.NoError(t, err)
220
221 numbers := func() []int64 {
222 open := openOf(t, is, n)
223 var out []int64
224 for _, i := range open {
225 out = append(out, i.Number)
226 }
227 return out
228 }
229 assert.Equal(t, []int64{2, 1}, numbers())
230 assert.Equal(t, 2, countKeys(t, s, "isopen/proj/"))
231
232 a.State = domain.IssueStateClosed
233 require.NoError(t, is.Update(ctx, a))
234 assert.Equal(t, []int64{2}, numbers())
235 assert.Equal(t, 1, countKeys(t, s, "isopen/proj/"))
236
237 a, err = is.Get(ctx, n, a.Number)
238 require.NoError(t, err)
239 a.State = domain.IssueStateOpen
240 require.NoError(t, is.Update(ctx, a))
241 assert.Equal(t, []int64{2, 1}, numbers())
242
243 // A comment keeps the index as the issue says.
244 b.State = domain.IssueStateClosed
245 b.CommentSeq = 1
246 require.NoError(t, is.SaveComment(ctx, b, newIssueComment(t, 1, "closing")))
247 assert.Equal(t, []int64{1}, numbers())
248 assert.Equal(t, 1, countKeys(t, s, "isopen/proj/"))
249}
250
251func TestRepoSettingsKeepTheIssuesMode(t *testing.T) {
252 ctx := context.Background()
253 s := openStore(t, t.TempDir())
254 n := name(t, "proj")
255 require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
256 got, err := s.Get(ctx, n)
257 require.NoError(t, err)
258 assert.Equal(t, domain.IssuesOff, got.Settings.Issues)
259 for _, m := range []domain.IssuesMode{domain.IssuesOwner, domain.IssuesPublic, domain.IssuesOff} {
260 got.Settings.Issues = m
261 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil })
262 require.NoError(t, err)
263 again, err := s.Get(ctx, n)
264 require.NoError(t, err)
265 assert.Equal(t, m, again.Settings.Issues)
266 }
267 got.Settings.Issues = "everyone"
268 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil })
269 assert.ErrorIs(t, err, domain.ErrInvalid)
270}
271
272// TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every
273// original scope keep their owner rights, gaining mr:review and issue:write
274// (each only if missing), then mr:force-merge in schema 5; no other token changes.
275func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
276 ctx := context.Background()
277 dir := t.TempDir()
278 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 3}))
279
280 all9 := []string{
281 "repo:read", "repo:write", "repo:admin", "mr:write", "mr:merge",
282 "ci:write", "secrets:write", "runner:register", "token:admin",
283 }
284 tokens := map[string]*storagepb.Token{
285 "full": {Id: "full", Scopes: all9},
286 "full10": {Id: "full10", Scopes: append(slices.Clone(all9), "mr:review")},
287 "reposmr": {Id: "reposmr", Scopes: append(slices.Clone(all9), "mr:review"), Repos: []string{"proj"}},
288 "missing": {Id: "missing", Scopes: all9[:8]},
289 "repos": {Id: "repos", Scopes: all9, Repos: []string{"proj"}},
290 "prefix": {Id: "prefix", Scopes: all9, BranchPrefixes: []string{"agent/"}},
291 "small": {Id: "small", Scopes: []string{"repo:read"}},
292 "otherfull": {Id: "otherfull", Scopes: append([]string{"token:admin"}, all9[:8]...)},
293 }
294 for id, tok := range tokens {
295 tok.Hash = make([]byte, 32)
296 tok.Hash[0] = byte(len(id))
297 putRaw(t, dir, "token/"+id, mustProto(t, tok))
298 }
299
300 s, err := kvstore.Open(ctx, dir)
301 require.NoError(t, err)
302 t.Cleanup(func() { _ = s.Close(ctx) })
303 get := func(id string) domain.Token {
304 tok, err := s.Tokens().Get(ctx, id)
305 require.NoError(t, err)
306 return tok
307 }
308
309 full := get("full")
310 assert.Contains(t, full.Scopes, domain.ScopeIssueWrite)
311 assert.Contains(t, full.Scopes, domain.ScopeMRReview)
312 assert.Len(t, full.Scopes, 12)
313 full10 := get("full10")
314 assert.Len(t, full10.Scopes, 12)
315 assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview))
316 assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite)
317 assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights())
318 assert.Len(t, get("reposmr").Scopes, 10)
319 assert.NotContains(t, get("reposmr").Scopes, domain.ScopeIssueWrite)
320 assert.True(t, domain.TokenPrincipal(full).HasOwnerRights())
321 assert.True(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights())
322
323 for _, id := range []string{"missing", "reposmr", "repos", "prefix", "small"} {
324 assert.NotContains(t, get(id).Scopes, domain.ScopeMRForceMerge)
325 }
326 assert.NotContains(t, get("missing").Scopes, domain.ScopeIssueWrite)
327 assert.NotContains(t, get("missing").Scopes, domain.ScopeMRReview)
328 assert.NotContains(t, get("repos").Scopes, domain.ScopeMRReview)
329 assert.NotContains(t, get("prefix").Scopes, domain.ScopeMRReview)
330 assert.False(t, domain.TokenPrincipal(get("missing")).HasOwnerRights())
331 assert.NotContains(t, get("repos").Scopes, domain.ScopeIssueWrite)
332 assert.NotContains(t, get("prefix").Scopes, domain.ScopeIssueWrite)
333 assert.Equal(t, []domain.Scope{domain.ScopeRepoRead}, get("small").Scopes)
334 assert.Len(t, get("missing").Scopes, 8)
335 assert.Len(t, get("repos").Scopes, 9)
336 assert.Len(t, get("prefix").Scopes, 9)
337 assert.Equal(t, []string{"agent/"}, get("prefix").BranchPrefixes)
338 assert.Equal(t, "full", full.ID)
339
340 require.NoError(t, s.Close(ctx))
341 assert.Equal(t, uint32(5), readSchema(t, dir))
342
343 // A second open changes nothing.
344 s2, err := kvstore.Open(ctx, dir)
345 require.NoError(t, err)
346 tok, err := s2.Tokens().Get(ctx, "full")
347 require.NoError(t, err)
348 assert.Equal(t, full.Scopes, tok.Scopes)
349 tok10, err := s2.Tokens().Get(ctx, "full10")
350 require.NoError(t, err)
351 assert.Equal(t, full10.Scopes, tok10.Scopes)
352 require.NoError(t, s2.Close(ctx))
353}
354
355func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
356 ctx := context.Background()
357 dir := t.TempDir()
358 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
359 _, err := kvstore.Open(ctx, dir)
360 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
361 assert.Equal(t, uint32(5), uint32(kvstore.SchemaVersion))
362}
363
364// A reopen racing the delete of the repository either lands before the delete
365// removes it or fails; it never leaves an isopen/ key of a repository that is
366// gone.
367func TestDeleteVersusReopenLeavesNoIssueKeys(t *testing.T) {
368 ctx := context.Background()
369 for range 30 {
370 s, _, n := repoWithMRs(t)
371 is := s.Issues()
372 issue, err := is.Create(ctx, newIssue(t, n, "t"))
373 require.NoError(t, err)
374 issue.State = domain.IssueStateClosed
375 require.NoError(t, is.Update(ctx, issue))
376 closed, err := is.Get(ctx, n, issue.Number)
377 require.NoError(t, err)
378 closed.State = domain.IssueStateOpen
379
380 var wg sync.WaitGroup
381 start := make(chan struct{})
382 wg.Add(2)
383 go func() { defer wg.Done(); <-start; _ = is.Update(ctx, closed) }()
384 go func() { defer wg.Done(); <-start; _ = s.Delete(ctx, n) }()
385 close(start)
386 wg.Wait()
387
388 for _, prefix := range []string{"is/proj/", "isc/proj/", "isopen/proj/"} {
389 assert.Zero(t, countKeys(t, s, prefix), prefix)
390 }
391 assert.ErrorIs(t, is.Update(ctx, closed), domain.ErrNotFound)
392 }
393}
394
395func TestListOpenAllSpansRepositoriesThroughTheIndex(t *testing.T) {
396 ctx := context.Background()
397 s, mrs, n := repoWithMRs(t)
398 other := name(t, "other")
399 require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic}))
400 is := s.Issues()
401
402 i1, err := is.Create(ctx, newIssue(t, n, "one"))
403 require.NoError(t, err)
404 i2, err := is.Create(ctx, newIssue(t, other, "two"))
405 require.NoError(t, err)
406 i3, err := is.Create(ctx, newIssue(t, other, "three"))
407 require.NoError(t, err)
408 i3.State = domain.IssueStateClosed
409 require.NoError(t, is.Update(ctx, i3))
410 _, err = mrs.Create(ctx, newMR(t, n, "mr1"))
411 require.NoError(t, err)
412 m2, err := mrs.Create(ctx, newMR(t, other, "mr2"))
413 require.NoError(t, err)
414 m3, err := mrs.Create(ctx, newMR(t, other, "mr3"))
415 require.NoError(t, err)
416 m3.State = domain.MRStateClosed
417 require.NoError(t, mrs.Update(ctx, m3))
418
419 issues, err := is.ListOpenAll(ctx)
420 require.NoError(t, err)
421 var got []string
422 for _, i := range issues {
423 got = append(got, i.Repo.String()+"#"+strconv.FormatInt(i.Number, 10))
424 }
425 assert.ElementsMatch(t, []string{
426 i1.Repo.String() + "#" + strconv.FormatInt(i1.Number, 10),
427 i2.Repo.String() + "#" + strconv.FormatInt(i2.Number, 10),
428 }, got)
429
430 open, err := mrs.ListOpenAll(ctx)
431 require.NoError(t, err)
432 got = nil
433 for _, m := range open {
434 got = append(got, m.Repo.String()+"#"+strconv.FormatInt(m.Number, 10))
435 }
436 assert.ElementsMatch(t, []string{"proj#2", m2.Repo.String() + "#" + strconv.FormatInt(m2.Number, 10)}, got)
437 assert.Len(t, open, 2)
438}
439
440func TestListOpenAllIsEmptyWithoutOpenItems(t *testing.T) {
441 ctx := context.Background()
442 s, mrs, _ := repoWithMRs(t)
443 issues, err := s.Issues().ListOpenAll(ctx)
444 require.NoError(t, err)
445 assert.Empty(t, issues)
446 open, err := mrs.ListOpenAll(ctx)
447 require.NoError(t, err)
448 assert.Empty(t, open)
449}
450
451func TestListOpenAllSkipsAStaleIndexEntry(t *testing.T) {
452 ctx := context.Background()
453 s, mrs, n := repoWithMRs(t)
454 is := s.Issues()
455 keepI, err := is.Create(ctx, newIssue(t, n, "keep"))
456 require.NoError(t, err)
457 goneI, err := is.Create(ctx, newIssue(t, n, "gone"))
458 require.NoError(t, err)
459 keepM, err := mrs.Create(ctx, newMR(t, n, "keep"))
460 require.NoError(t, err)
461 goneM, err := mrs.Create(ctx, newMR(t, n, "gone"))
462 require.NoError(t, err)
463
464 require.NoError(t, is.DropIssueRecordForTest(ctx, n, goneI.Number))
465 require.NoError(t, mrs.DropMRRecordForTest(ctx, n, goneM.Number))
466 require.Positive(t, countKeys(t, s, "isopen/proj/"), "the index entries are still there")
467 require.Positive(t, countKeys(t, s, "mropen/proj/"))
468
469 issues, err := is.ListOpenAll(ctx)
470 require.NoError(t, err)
471 require.Len(t, issues, 1)
472 assert.Equal(t, keepI.Number, issues[0].Number)
473 open, err := mrs.ListOpenAll(ctx)
474 require.NoError(t, err)
475 require.Len(t, open, 1)
476 assert.Equal(t, keepM.Number, open[0].Number)
477}
478
479func TestIssueListPagesThroughManyIssues(t *testing.T) {
480 ctx := context.Background()
481 s, _, n := repoWithMRs(t)
482 is := s.Issues()
483 const total = 53
484 for i := 1; i <= total; i++ {
485 _, err := is.Create(ctx, newIssue(t, n, "issue "+strconv.Itoa(i)))
486 require.NoError(t, err)
487 }
488 for i := 2; i <= total; i += 3 { // 2, 5, 8, ...
489 is, err := is.Get(ctx, n, int64(i))
490 require.NoError(t, err)
491 is.State = domain.IssueStateClosed
492 require.NoError(t, s.Issues().Update(ctx, is))
493 }
494
495 walk := func(state domain.IssueState, size int) []int64 {
496 var got []int64
497 token := ""
498 for {
499 page, err := s.Issues().List(ctx, n, ports.ListIssuesParams{State: state, PageSize: size, PageToken: token})
500 require.NoError(t, err)
501 require.LessOrEqual(t, len(page.Issues), size)
502 for _, is := range page.Issues {
503 got = append(got, is.Number)
504 }
505 if page.NextPageToken == "" {
506 return got
507 }
508 token = page.NextPageToken
509 }
510 }
511 for _, size := range []int{1, 7, 10, 53, 100} {
512 var all, closed []int64
513 for i := int64(total); i >= 1; i-- {
514 all = append(all, i)
515 if i%3 == 2 {
516 closed = append(closed, i)
517 }
518 }
519 assert.Equal(t, all, walk("", size), "size %d", size)
520 assert.Equal(t, closed, walk(domain.IssueStateClosed, size), "closed, size %d", size)
521 }
522 page, err := s.Issues().List(ctx, n, ports.ListIssuesParams{PageSize: 10, PageToken: "1"})
523 require.NoError(t, err)
524 assert.Empty(t, page.Issues)
525 assert.Empty(t, page.NextPageToken)
526}
527
528// openOf lists the open issues of one repository through the isopen/ index.
529func openOf(t *testing.T, is *kvstore.Issues, repo domain.RepoName) []domain.Issue {
530 t.Helper()
531 all, err := is.ListOpenAll(context.Background())
532 require.NoError(t, err)
533 var out []domain.Issue
534 for _, i := range all {
535 if i.Repo == repo {
536 out = append(out, i)
537 }
538 }
539 sort.Slice(out, func(a, b int) bool { return out[a].Number > out[b].Number })
540 return out
541}
542
543func TestStoredPendingIssueStateIsCorrupt(t *testing.T) {
544 ctx := context.Background()
545 s, _, n := repoWithMRs(t)
546 is := s.Issues()
547 raw, err := proto.Marshal(&storagepb.Issue{Number: 1, Title: "x", State: storagepb.IssueState_ISSUE_STATE_PENDING})
548 require.NoError(t, err)
549 require.NoError(t, is.PutRawIssueForTest(ctx, n, 1, raw))
550 _, err = is.Get(ctx, n, 1)
551 assert.ErrorContains(t, err, "pending")
552 _, err = is.List(ctx, n, ports.ListIssuesParams{})
553 assert.ErrorContains(t, err, "pending")
554 _, err = is.List(ctx, n, ports.ListIssuesParams{State: domain.IssueStateOpen})
555 assert.ErrorContains(t, err, "pending")
556}
557
558func countScope(scopes []domain.Scope, want domain.Scope) int {
559 n := 0
560 for _, s := range scopes {
561 if s == want {
562 n++
563 }
564 }
565 return n
566}
567
568func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
569 ctx := context.Background()
570 dir := t.TempDir()
571 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 4}))
572 var previous []string
573 for _, sc := range domain.AllScopes() {
574 if sc != domain.ScopeMRForceMerge {
575 previous = append(previous, sc.String())
576 }
577 }
578 tokens := map[string]*storagepb.Token{
579 "owner": {Id: "owner", Scopes: previous},
580 "repos": {Id: "repos", Scopes: previous, Repos: []string{"proj"}},
581 "branch": {Id: "branch", Scopes: previous, BranchPrefixes: []string{"agent/"}},
582 "partial": {Id: "partial", Scopes: previous[:10]},
583 }
584 for id, tok := range tokens {
585 tok.Hash = make([]byte, 32)
586 putRaw(t, dir, "token/"+id, mustProto(t, tok))
587 }
588 for range 2 {
589 s, err := kvstore.Open(ctx, dir)
590 require.NoError(t, err)
591 for id, original := range tokens {
592 tok, err := s.Tokens().Get(ctx, id)
593 require.NoError(t, err)
594 if id == "owner" {
595 assert.True(t, domain.TokenPrincipal(tok).HasOwnerRights())
596 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeMRForceMerge))
597 assert.Len(t, tok.Scopes, 12)
598 } else {
599 assert.NotContains(t, tok.Scopes, domain.ScopeMRForceMerge)
600 var got []string
601 for _, sc := range tok.Scopes {
602 got = append(got, sc.String())
603 }
604 assert.Equal(t, original.Scopes, got)
605 }
606 }
607 require.NoError(t, s.Close(ctx))
608 assert.Equal(t, uint32(5), readSchema(t, dir))
609 }
610}