1// SPDX-License-Identifier: AGPL-3.0-or-later
2package kvstore_test
3
4import (
5 "context"
6 "fmt"
7 "testing"
8 "time"
9
10 storagepb "bnema.dev/ermite/gen/ermite/storage"
11 "bnema.dev/ermite/internal/adapters/kvstore"
12 "bnema.dev/ermite/internal/domain"
13 "bnema.dev/ermite/internal/ports"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18func TestIssueHistoryAtomicAndCapped(t *testing.T) {
19 ctx := context.Background()
20 s, _, repo := repoWithMRs(t)
21 is := s.Issues()
22 issue, err := is.Create(ctx, newIssue(t, repo, "history"))
23 require.NoError(t, err)
24 stale := issue
25 issue.State = domain.IssueStateResolved
26 ev := domain.IssueEvent{Kind: domain.IssueEventResolved, Actor: "merger", MergeRequest: 9, CreatedAt: created}
27 require.NoError(t, is.Update(ctx, issue, ev, domain.IssueEvent{Kind: domain.IssueEventReopened, CreatedAt: created.Add(time.Second)}))
28 assert.ErrorIs(t, is.Update(ctx, stale, ev), domain.ErrConflict)
29 events, err := is.ListEvents(ctx, repo, issue.Number)
30 require.NoError(t, err)
31 require.Len(t, events, 2)
32 assert.Equal(t, int64(1), events[0].ID)
33 assert.Equal(t, int64(2), events[1].ID)
34 assert.Equal(t, issue.Number, events[0].Issue)
35 assert.Equal(t, "merger", events[0].Actor)
36 open, err := is.ListOpenAll(ctx)
37 require.NoError(t, err)
38 assert.Empty(t, open)
39 page, err := is.List(ctx, repo, ports.ListIssuesParams{State: domain.IssueStateResolved})
40 require.NoError(t, err)
41 require.Len(t, page.Issues, 1)
42 issue = page.Issues[0]
43 batch := make([]domain.IssueEvent, kvstore.MaxIssueEvents)
44 for j := range batch {
45 batch[j] = ev
46 }
47 require.NoError(t, is.Update(ctx, issue, batch...))
48 issue, err = is.Get(ctx, repo, issue.Number)
49 require.NoError(t, err)
50 issue.State = domain.IssueStateOpen
51 require.NoError(t, is.Update(ctx, issue, ev))
52 events, err = is.ListEvents(ctx, repo, issue.Number)
53 require.NoError(t, err)
54 require.Len(t, events, kvstore.MaxIssueEvents)
55 for j, e := range events {
56 assert.Equal(t, int64(j+1), e.ID)
57 }
58 issue, err = is.Get(ctx, repo, issue.Number)
59 require.NoError(t, err)
60 assert.Equal(t, domain.IssueStateOpen, issue.State)
61 require.NoError(t, s.Delete(ctx, repo))
62 for _, prefix := range []string{"is/", "isc/", "isopen/", "isev/", "mrev/"} {
63 assert.Zero(t, countKeys(t, s, prefix+repo.String()+"/"))
64 }
65}
66
67func TestReferenceHistoryDiff(t *testing.T) {
68 ctx := context.Background()
69 s, _, repo := repoWithMRs(t)
70 links := s.IssueLinks()
71 ref := ports.ReferenceEvent{Actor: "writer", At: created}
72 replace := func(issues, closes []int64) {
73 t.Helper()
74 _, _, err := links.ReplaceLinks(ctx, repo, 9, issues, closes, ref)
75 require.NoError(t, err)
76 }
77 replace([]int64{3}, nil)
78 replace([]int64{3}, nil)
79 ref.At = created.Add(time.Second)
80 replace([]int64{3}, []int64{3})
81 replace([]int64{3}, []int64{3})
82 replace([]int64{3}, nil) // downgrade has no event
83 replace(nil, nil) // removal retains history
84 events, err := s.Issues().ListEvents(ctx, repo, 3)
85 require.NoError(t, err)
86 require.Len(t, events, 2)
87 assert.False(t, events[0].Closes)
88 assert.True(t, events[1].Closes)
89 assert.Equal(t, "writer", events[0].Actor)
90 assert.Equal(t, domain.IssueEventReferenced, events[0].Kind)
91 mrEvents, err := links.EventsOfMergeRequest(ctx, repo, 9)
92 require.NoError(t, err)
93 assert.Equal(t, events, mrEvents)
94 replace([]int64{3}, nil) // reappearance is another event
95 events, err = s.Issues().ListEvents(ctx, repo, 3)
96 require.NoError(t, err)
97 require.Len(t, events, 3)
98 assert.Equal(t, int64(3), events[2].ID)
99 require.NoError(t, s.Delete(ctx, repo))
100 assert.Zero(t, countKeys(t, s, "isev/"+repo.String()+"/"))
101 assert.Zero(t, countKeys(t, s, "mrev/"+repo.String()+"/"))
102}
103
104func TestMRHistoryOrderAndCap(t *testing.T) {
105 ctx := context.Background()
106 s, _, repo := repoWithMRs(t)
107 // Insert in reverse timestamp order; issue and seq break timestamp ties.
108 for n := int64(1); n <= 3; n++ {
109 issue, err := s.Issues().Create(ctx, newIssue(t, repo, "many"))
110 require.NoError(t, err)
111 events := make([]domain.IssueEvent, 201)
112 for j := range events {
113 events[j] = domain.IssueEvent{Kind: domain.IssueEventResolved, MergeRequest: 9, CreatedAt: created.Add(time.Duration(200-j) * time.Second)}
114 }
115 require.NoError(t, s.Issues().Update(ctx, issue, events...))
116 }
117 events, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
118 require.NoError(t, err)
119 require.Len(t, events, 500)
120 for j := 1; j < len(events); j++ {
121 a, b := events[j-1], events[j]
122 assert.False(t, a.CreatedAt.After(b.CreatedAt))
123 if a.CreatedAt.Equal(b.CreatedAt) {
124 assert.Less(t, a.Issue, b.Issue)
125 }
126 }
127}
128
129func TestResolvedMigrationAndRestart(t *testing.T) {
130 ctx := context.Background()
131 for _, version := range []uint32{5, 7} {
132 t.Run(fmt.Sprint(version), func(t *testing.T) {
133 dir := t.TempDir()
134 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: version}))
135 for n := int64(1); n <= 3; n++ {
136 state := storagepb.IssueState_ISSUE_STATE_CLOSED
137 mr := int64(0)
138 if n == 1 {
139 mr = 9
140 }
141 if n == 3 {
142 state = storagepb.IssueState_ISSUE_STATE_OPEN
143 }
144 putRaw(t, dir, fmt.Sprintf("is/proj/%010d", n), mustProto(t, &storagepb.Issue{Number: n, Title: "old", State: state, ClosedByMergeRequest: mr}))
145 }
146 putRaw(t, dir, "isopen/proj/0000000003", []byte{1})
147 repo := name(t, "proj")
148 var migratedVersion uint64
149 for pass := 0; pass < 2; pass++ {
150 s, err := kvstore.Open(ctx, dir)
151 require.NoError(t, err)
152 for n, state := range []domain.IssueState{domain.IssueStateResolved, domain.IssueStateClosed, domain.IssueStateOpen} {
153 issue, err := s.Issues().Get(ctx, repo, int64(n+1))
154 require.NoError(t, err)
155 assert.Equal(t, state, issue.State)
156 assert.True(t, issue.UpdatedAt.IsZero())
157 if n == 0 {
158 assert.Equal(t, int64(9), issue.ClosedByMergeRequest)
159 if pass == 0 {
160 migratedVersion = issue.Version
161 } else {
162 assert.Equal(t, migratedVersion, issue.Version)
163 }
164 }
165 }
166 assert.Equal(t, 1, countKeys(t, s, "isopen/proj/"))
167 assert.Zero(t, countKeys(t, s, "isev/"))
168 assert.Zero(t, countKeys(t, s, "mrev/"))
169 require.NoError(t, s.Close(ctx))
170 assert.Equal(t, uint32(8), readSchema(t, dir))
171 }
172 })
173 }
174}
175
176func TestIssueEventSequenceSurvivesRestart(t *testing.T) {
177 ctx := context.Background()
178 dir := t.TempDir()
179 repo := name(t, "proj")
180 for pass := 0; pass < 2; pass++ {
181 s, err := kvstore.Open(ctx, dir)
182 require.NoError(t, err)
183 if pass == 0 {
184 require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPrivate}))
185 _, err := s.Issues().Create(ctx, newIssue(t, repo, "restart"))
186 require.NoError(t, err)
187 }
188 issue, err := s.Issues().Get(ctx, repo, 1)
189 require.NoError(t, err)
190 require.NoError(t, s.Issues().Update(ctx, issue, domain.IssueEvent{Kind: domain.IssueEventClosed, MergeRequest: 9, CreatedAt: created}))
191 events, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
192 require.NoError(t, err)
193 require.Len(t, events, pass+1)
194 assert.Equal(t, int64(pass+1), events[pass].ID)
195 require.NoError(t, s.Close(ctx))
196 }
197}
198
199func TestLinkEventsDropAtCapWithoutChangingIssueVersion(t *testing.T) {
200 ctx := context.Background()
201 s, _, repo := repoWithMRs(t)
202 issue, err := s.Issues().Create(ctx, newIssue(t, repo, "full"))
203 require.NoError(t, err)
204 events := make([]domain.IssueEvent, kvstore.MaxIssueEvents)
205 for j := range events {
206 events[j].Kind = domain.IssueEventClosed
207 }
208 require.NoError(t, s.Issues().Update(ctx, issue, events...))
209 before, err := s.Issues().Get(ctx, repo, issue.Number)
210 require.NoError(t, err)
211 added, _, err := s.IssueLinks().ReplaceLinks(ctx, repo, 9, []int64{issue.Number}, []int64{issue.Number}, ports.ReferenceEvent{Actor: "writer", At: created})
212 require.NoError(t, err)
213 require.Len(t, added, 1)
214 after, err := s.Issues().Get(ctx, repo, issue.Number)
215 require.NoError(t, err)
216 assert.Equal(t, before.Version, after.Version)
217 history, err := s.Issues().ListEvents(ctx, repo, issue.Number)
218 require.NoError(t, err)
219 require.Len(t, history, kvstore.MaxIssueEvents)
220 mrHistory, err := s.IssueLinks().EventsOfMergeRequest(ctx, repo, 9)
221 require.NoError(t, err)
222 assert.Empty(t, mrHistory)
223 links, err := s.IssueLinks().LinksOfMergeRequest(ctx, repo, 9)
224 require.NoError(t, err)
225 assert.Equal(t, added, links)
226}