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 and release:write in
275// schema 6; no other token changes.
276func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
277 ctx := context.Background()
278 dir := t.TempDir()
279 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 3}))
280
281 all9 := []string{
282 "repo:read", "repo:write", "repo:admin", "mr:write", "mr:merge",
283 "ci:write", "secrets:write", "runner:register", "token:admin",
284 }
285 tokens := map[string]*storagepb.Token{
286 "full": {Id: "full", Scopes: all9},
287 "full10": {Id: "full10", Scopes: append(slices.Clone(all9), "mr:review")},
288 "reposmr": {Id: "reposmr", Scopes: append(slices.Clone(all9), "mr:review"), Repos: []string{"proj"}},
289 "missing": {Id: "missing", Scopes: all9[:8]},
290 "repos": {Id: "repos", Scopes: all9, Repos: []string{"proj"}},
291 "prefix": {Id: "prefix", Scopes: all9, BranchPrefixes: []string{"agent/"}},
292 "small": {Id: "small", Scopes: []string{"repo:read"}},
293 "otherfull": {Id: "otherfull", Scopes: append([]string{"token:admin"}, all9[:8]...)},
294 }
295 for id, tok := range tokens {
296 tok.Hash = make([]byte, 32)
297 tok.Hash[0] = byte(len(id))
298 putRaw(t, dir, "token/"+id, mustProto(t, tok))
299 }
300
301 s, err := kvstore.Open(ctx, dir)
302 require.NoError(t, err)
303 t.Cleanup(func() { _ = s.Close(ctx) })
304 get := func(id string) domain.Token {
305 tok, err := s.Tokens().Get(ctx, id)
306 require.NoError(t, err)
307 return tok
308 }
309
310 full := get("full")
311 assert.Contains(t, full.Scopes, domain.ScopeIssueWrite)
312 assert.Contains(t, full.Scopes, domain.ScopeMRReview)
313 assert.Len(t, full.Scopes, 13)
314 assert.Contains(t, full.Scopes, domain.ScopeReleaseWrite)
315 full10 := get("full10")
316 assert.Len(t, full10.Scopes, 13)
317 assert.Contains(t, full10.Scopes, domain.ScopeReleaseWrite)
318 for _, id := range []string{"repos", "prefix", "missing", "reposmr", "small"} {
319 assert.NotContains(t, get(id).Scopes, domain.ScopeReleaseWrite)
320 }
321 assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview))
322 assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite)
323 assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights())
324 assert.Len(t, get("reposmr").Scopes, 10)
325 assert.NotContains(t, get("reposmr").Scopes, domain.ScopeIssueWrite)
326 assert.True(t, domain.TokenPrincipal(full).HasOwnerRights())
327 assert.True(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights())
328
329 for _, id := range []string{"missing", "reposmr", "repos", "prefix", "small"} {
330 assert.NotContains(t, get(id).Scopes, domain.ScopeMRForceMerge)
331 }
332 assert.NotContains(t, get("missing").Scopes, domain.ScopeIssueWrite)
333 assert.NotContains(t, get("missing").Scopes, domain.ScopeMRReview)
334 assert.NotContains(t, get("repos").Scopes, domain.ScopeMRReview)
335 assert.NotContains(t, get("prefix").Scopes, domain.ScopeMRReview)
336 assert.False(t, domain.TokenPrincipal(get("missing")).HasOwnerRights())
337 assert.NotContains(t, get("repos").Scopes, domain.ScopeIssueWrite)
338 assert.NotContains(t, get("prefix").Scopes, domain.ScopeIssueWrite)
339 assert.Equal(t, []domain.Scope{domain.ScopeRepoRead}, get("small").Scopes)
340 assert.Len(t, get("missing").Scopes, 8)
341 assert.Len(t, get("repos").Scopes, 9)
342 assert.Len(t, get("prefix").Scopes, 9)
343 assert.Equal(t, []string{"agent/"}, get("prefix").BranchPrefixes)
344 assert.Equal(t, "full", full.ID)
345
346 require.NoError(t, s.Close(ctx))
347 assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
348
349 // A second open changes nothing.
350 s2, err := kvstore.Open(ctx, dir)
351 require.NoError(t, err)
352 tok, err := s2.Tokens().Get(ctx, "full")
353 require.NoError(t, err)
354 assert.Equal(t, full.Scopes, tok.Scopes)
355 tok10, err := s2.Tokens().Get(ctx, "full10")
356 require.NoError(t, err)
357 assert.Equal(t, full10.Scopes, tok10.Scopes)
358 require.NoError(t, s2.Close(ctx))
359}
360
361func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
362 ctx := context.Background()
363 dir := t.TempDir()
364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
365 _, err := kvstore.Open(ctx, dir)
366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
367 assert.Equal(t, uint32(7), uint32(kvstore.SchemaVersion))
368}
369
370// A reopen racing the delete of the repository either lands before the delete
371// removes it or fails; it never leaves an isopen/ key of a repository that is
372// gone.
373func TestDeleteVersusReopenLeavesNoIssueKeys(t *testing.T) {
374 ctx := context.Background()
375 for range 30 {
376 s, _, n := repoWithMRs(t)
377 is := s.Issues()
378 issue, err := is.Create(ctx, newIssue(t, n, "t"))
379 require.NoError(t, err)
380 issue.State = domain.IssueStateClosed
381 require.NoError(t, is.Update(ctx, issue))
382 closed, err := is.Get(ctx, n, issue.Number)
383 require.NoError(t, err)
384 closed.State = domain.IssueStateOpen
385
386 var wg sync.WaitGroup
387 start := make(chan struct{})
388 wg.Add(2)
389 go func() { defer wg.Done(); <-start; _ = is.Update(ctx, closed) }()
390 go func() { defer wg.Done(); <-start; _ = s.Delete(ctx, n) }()
391 close(start)
392 wg.Wait()
393
394 for _, prefix := range []string{"is/proj/", "isc/proj/", "isopen/proj/"} {
395 assert.Zero(t, countKeys(t, s, prefix), prefix)
396 }
397 assert.ErrorIs(t, is.Update(ctx, closed), domain.ErrNotFound)
398 }
399}
400
401func TestListOpenAllSpansRepositoriesThroughTheIndex(t *testing.T) {
402 ctx := context.Background()
403 s, mrs, n := repoWithMRs(t)
404 other := name(t, "other")
405 require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic}))
406 is := s.Issues()
407
408 i1, err := is.Create(ctx, newIssue(t, n, "one"))
409 require.NoError(t, err)
410 i2, err := is.Create(ctx, newIssue(t, other, "two"))
411 require.NoError(t, err)
412 i3, err := is.Create(ctx, newIssue(t, other, "three"))
413 require.NoError(t, err)
414 i3.State = domain.IssueStateClosed
415 require.NoError(t, is.Update(ctx, i3))
416 _, err = mrs.Create(ctx, newMR(t, n, "mr1"))
417 require.NoError(t, err)
418 m2, err := mrs.Create(ctx, newMR(t, other, "mr2"))
419 require.NoError(t, err)
420 m3, err := mrs.Create(ctx, newMR(t, other, "mr3"))
421 require.NoError(t, err)
422 m3.State = domain.MRStateClosed
423 require.NoError(t, mrs.Update(ctx, m3))
424
425 issues, err := is.ListOpenAll(ctx)
426 require.NoError(t, err)
427 var got []string
428 for _, i := range issues {
429 got = append(got, i.Repo.String()+"#"+strconv.FormatInt(i.Number, 10))
430 }
431 assert.ElementsMatch(t, []string{
432 i1.Repo.String() + "#" + strconv.FormatInt(i1.Number, 10),
433 i2.Repo.String() + "#" + strconv.FormatInt(i2.Number, 10),
434 }, got)
435
436 open, err := mrs.ListOpenAll(ctx)
437 require.NoError(t, err)
438 got = nil
439 for _, m := range open {
440 got = append(got, m.Repo.String()+"#"+strconv.FormatInt(m.Number, 10))
441 }
442 assert.ElementsMatch(t, []string{"proj#2", m2.Repo.String() + "#" + strconv.FormatInt(m2.Number, 10)}, got)
443 assert.Len(t, open, 2)
444}
445
446func TestListOpenAllIsEmptyWithoutOpenItems(t *testing.T) {
447 ctx := context.Background()
448 s, mrs, _ := repoWithMRs(t)
449 issues, err := s.Issues().ListOpenAll(ctx)
450 require.NoError(t, err)
451 assert.Empty(t, issues)
452 open, err := mrs.ListOpenAll(ctx)
453 require.NoError(t, err)
454 assert.Empty(t, open)
455}
456
457func TestListOpenAllSkipsAStaleIndexEntry(t *testing.T) {
458 ctx := context.Background()
459 s, mrs, n := repoWithMRs(t)
460 is := s.Issues()
461 keepI, err := is.Create(ctx, newIssue(t, n, "keep"))
462 require.NoError(t, err)
463 goneI, err := is.Create(ctx, newIssue(t, n, "gone"))
464 require.NoError(t, err)
465 keepM, err := mrs.Create(ctx, newMR(t, n, "keep"))
466 require.NoError(t, err)
467 goneM, err := mrs.Create(ctx, newMR(t, n, "gone"))
468 require.NoError(t, err)
469
470 require.NoError(t, is.DropIssueRecordForTest(ctx, n, goneI.Number))
471 require.NoError(t, mrs.DropMRRecordForTest(ctx, n, goneM.Number))
472 require.Positive(t, countKeys(t, s, "isopen/proj/"), "the index entries are still there")
473 require.Positive(t, countKeys(t, s, "mropen/proj/"))
474
475 issues, err := is.ListOpenAll(ctx)
476 require.NoError(t, err)
477 require.Len(t, issues, 1)
478 assert.Equal(t, keepI.Number, issues[0].Number)
479 open, err := mrs.ListOpenAll(ctx)
480 require.NoError(t, err)
481 require.Len(t, open, 1)
482 assert.Equal(t, keepM.Number, open[0].Number)
483}
484
485func TestIssueListPagesThroughManyIssues(t *testing.T) {
486 ctx := context.Background()
487 s, _, n := repoWithMRs(t)
488 is := s.Issues()
489 const total = 53
490 for i := 1; i <= total; i++ {
491 _, err := is.Create(ctx, newIssue(t, n, "issue "+strconv.Itoa(i)))
492 require.NoError(t, err)
493 }
494 for i := 2; i <= total; i += 3 { // 2, 5, 8, ...
495 is, err := is.Get(ctx, n, int64(i))
496 require.NoError(t, err)
497 is.State = domain.IssueStateClosed
498 require.NoError(t, s.Issues().Update(ctx, is))
499 }
500
501 walk := func(state domain.IssueState, size int) []int64 {
502 var got []int64
503 token := ""
504 for {
505 page, err := s.Issues().List(ctx, n, ports.ListIssuesParams{State: state, PageSize: size, PageToken: token})
506 require.NoError(t, err)
507 require.LessOrEqual(t, len(page.Issues), size)
508 for _, is := range page.Issues {
509 got = append(got, is.Number)
510 }
511 if page.NextPageToken == "" {
512 return got
513 }
514 token = page.NextPageToken
515 }
516 }
517 for _, size := range []int{1, 7, 10, 53, 100} {
518 var all, closed []int64
519 for i := int64(total); i >= 1; i-- {
520 all = append(all, i)
521 if i%3 == 2 {
522 closed = append(closed, i)
523 }
524 }
525 assert.Equal(t, all, walk("", size), "size %d", size)
526 assert.Equal(t, closed, walk(domain.IssueStateClosed, size), "closed, size %d", size)
527 }
528 page, err := s.Issues().List(ctx, n, ports.ListIssuesParams{PageSize: 10, PageToken: "1"})
529 require.NoError(t, err)
530 assert.Empty(t, page.Issues)
531 assert.Empty(t, page.NextPageToken)
532}
533
534// openOf lists the open issues of one repository through the isopen/ index.
535func openOf(t *testing.T, is *kvstore.Issues, repo domain.RepoName) []domain.Issue {
536 t.Helper()
537 all, err := is.ListOpenAll(context.Background())
538 require.NoError(t, err)
539 var out []domain.Issue
540 for _, i := range all {
541 if i.Repo == repo {
542 out = append(out, i)
543 }
544 }
545 sort.Slice(out, func(a, b int) bool { return out[a].Number > out[b].Number })
546 return out
547}
548
549func TestStoredPendingIssueStateIsCorrupt(t *testing.T) {
550 ctx := context.Background()
551 s, _, n := repoWithMRs(t)
552 is := s.Issues()
553 raw, err := proto.Marshal(&storagepb.Issue{Number: 1, Title: "x", State: storagepb.IssueState_ISSUE_STATE_PENDING})
554 require.NoError(t, err)
555 require.NoError(t, is.PutRawIssueForTest(ctx, n, 1, raw))
556 _, err = is.Get(ctx, n, 1)
557 assert.ErrorContains(t, err, "pending")
558 _, err = is.List(ctx, n, ports.ListIssuesParams{})
559 assert.ErrorContains(t, err, "pending")
560 _, err = is.List(ctx, n, ports.ListIssuesParams{State: domain.IssueStateOpen})
561 assert.ErrorContains(t, err, "pending")
562}
563
564func countScope(scopes []domain.Scope, want domain.Scope) int {
565 n := 0
566 for _, s := range scopes {
567 if s == want {
568 n++
569 }
570 }
571 return n
572}
573
574func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
575 ctx := context.Background()
576 dir := t.TempDir()
577 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 4}))
578 var previous []string
579 for _, sc := range domain.AllScopes() {
580 if sc != domain.ScopeMRForceMerge && sc != domain.ScopeReleaseWrite {
581 previous = append(previous, sc.String())
582 }
583 }
584 tokens := map[string]*storagepb.Token{
585 "owner": {Id: "owner", Scopes: previous},
586 "repos": {Id: "repos", Scopes: previous, Repos: []string{"proj"}},
587 "branch": {Id: "branch", Scopes: previous, BranchPrefixes: []string{"agent/"}},
588 "partial": {Id: "partial", Scopes: previous[:10]},
589 }
590 for id, tok := range tokens {
591 tok.Hash = make([]byte, 32)
592 putRaw(t, dir, "token/"+id, mustProto(t, tok))
593 }
594 for range 2 {
595 s, err := kvstore.Open(ctx, dir)
596 require.NoError(t, err)
597 for id, original := range tokens {
598 tok, err := s.Tokens().Get(ctx, id)
599 require.NoError(t, err)
600 if id == "owner" {
601 assert.True(t, domain.TokenPrincipal(tok).HasOwnerRights())
602 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeMRForceMerge))
603 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeReleaseWrite))
604 assert.Len(t, tok.Scopes, 13)
605 } else {
606 assert.NotContains(t, tok.Scopes, domain.ScopeMRForceMerge)
607 assert.NotContains(t, tok.Scopes, domain.ScopeReleaseWrite)
608 var got []string
609 for _, sc := range tok.Scopes {
610 got = append(got, sc.String())
611 }
612 assert.Equal(t, original.Scopes, got)
613 }
614 }
615 require.NoError(t, s.Close(ctx))
616 assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
617 }
618}