internal/store/mrs_test.go
330 lines · 9920 bytes
1package store
2
3import "testing"
4
5func mrFixture(t *testing.T) (*Store, int64, int64) {
6 t.Helper()
7 s := open(t)
8 if err := s.MigrateUp(); err != nil {
9 t.Fatal(err)
10 }
11 uid, err := s.CreateUser("cmc", true)
12 if err != nil {
13 t.Fatal(err)
14 }
15 repoID, err := s.CreateRepo("user", uid, "lib", "public")
16 if err != nil {
17 t.Fatal(err)
18 }
19 if _, err := s.CreateMR(repoID, uid, repoID, "feature", "main", "t", "", "abc123", "md", false); err != nil {
20 t.Fatal(err)
21 }
22 return s, repoID, uid
23}
24
25// A merged or closed MR records who resolved it and when: the state alone
26// cannot say it, and updated_at moves for every edit.
27func TestResolutionStamps(t *testing.T) {
28 s, repoID, uid := mrFixture(t)
29 mr, err := s.MRByNumber(repoID, 1)
30 if err != nil {
31 t.Fatal(err)
32 }
33 if mr.MergedAt != "" || mr.ClosedAt != "" {
34 t.Fatalf("open MR carries a stamp: %+v", mr)
35 }
36 if err := s.MarkMerged(mr.ID, "base1", uid, ""); err != nil {
37 t.Fatal(err)
38 }
39 mr, _ = s.MRByNumber(repoID, 1)
40 if mr.State != "merged" || mr.MergedAt == "" || mr.MergedBy != "cmc" {
41 t.Fatalf("merge stamp: %+v", mr)
42 }
43
44 // Reopening — a source branch that came back — clears the stamp.
45 if err := s.SetMRState(mr.ID, "open"); err != nil {
46 t.Fatal(err)
47 }
48 mr, _ = s.MRByNumber(repoID, 1)
49 if mr.MergedAt != "" || mr.MergedBy != "" {
50 t.Fatalf("reopen kept the merge stamp: %+v", mr)
51 }
52
53 if err := s.MarkClosed(mr.ID, uid, ""); err != nil {
54 t.Fatal(err)
55 }
56 mr, _ = s.MRByNumber(repoID, 1)
57 if mr.State != "closed" || mr.ClosedAt == "" || mr.ClosedBy != "cmc" {
58 t.Fatalf("close stamp: %+v", mr)
59 }
60}
61
62// A merge request closed without merging can record the request that
63// carried its change forward; MRsSuperseding is the reverse lookup, and
64// 0 clears the field (#223).
65func TestSupersededBy(t *testing.T) {
66 s, repoID, uid := mrFixture(t)
67 if _, err := s.CreateMR(repoID, uid, repoID, "feature2", "main", "t2", "", "def456", "md", false); err != nil {
68 t.Fatal(err)
69 }
70 mr1, _ := s.MRByNumber(repoID, 1)
71 if err := s.MarkClosed(mr1.ID, uid, ""); err != nil {
72 t.Fatal(err)
73 }
74 if err := s.SetSupersededBy(mr1.ID, 2); err != nil {
75 t.Fatal(err)
76 }
77 mr1, _ = s.MRByNumber(repoID, 1)
78 if mr1.SupersededBy != 2 {
79 t.Fatalf("SupersededBy = %d, want 2", mr1.SupersededBy)
80 }
81 superseding, err := s.MRsSuperseding(repoID, 2)
82 if err != nil {
83 t.Fatal(err)
84 }
85 if len(superseding) != 1 || superseding[0].Number != 1 {
86 t.Fatalf("MRsSuperseding(repoID, 2) = %+v", superseding)
87 }
88 if err := s.SetSupersededBy(mr1.ID, 0); err != nil {
89 t.Fatal(err)
90 }
91 mr1, _ = s.MRByNumber(repoID, 1)
92 if mr1.SupersededBy != 0 {
93 t.Fatalf("SupersededBy after clear = %d, want 0", mr1.SupersededBy)
94 }
95 superseding, err = s.MRsSuperseding(repoID, 2)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if len(superseding) != 0 {
100 t.Fatalf("MRsSuperseding(repoID, 2) after clear = %+v", superseding)
101 }
102}
103
104// An import carries the upstream time but no local account for the actor.
105func TestResolutionStampImported(t *testing.T) {
106 s, repoID, _ := mrFixture(t)
107 mr, _ := s.MRByNumber(repoID, 1)
108 if err := s.MarkMerged(mr.ID, "base1", 0, "2024-03-02T10:00:00Z"); err != nil {
109 t.Fatal(err)
110 }
111 mr, _ = s.MRByNumber(repoID, 1)
112 if mr.MergedAt != "2024-03-02T10:00:00Z" || mr.MergedBy != "" {
113 t.Fatalf("imported merge stamp: %+v", mr)
114 }
115}
116
117// PreferredVerifiedEmail falls back to a verified secondary when the
118// primary is not verified, unlike PrimaryVerifiedEmail (#158).
119func TestPreferredVerifiedEmail(t *testing.T) {
120 s := open(t)
121 if err := s.MigrateUp(); err != nil {
122 t.Fatal(err)
123 }
124 uid, err := s.CreateUser("gus", false)
125 if err != nil {
126 t.Fatal(err)
127 }
128
129 if addr, err := s.PreferredVerifiedEmail(uid); err != nil || addr != "" {
130 t.Fatalf("no addresses at all: %q, %v", addr, err)
131 }
132
133 if err := s.AddEmail(uid, "primary@example.test", "", true); err != nil {
134 t.Fatal(err)
135 }
136 if addr, err := s.PreferredVerifiedEmail(uid); err != nil || addr != "" {
137 t.Fatalf("unverified primary only: %q, %v", addr, err)
138 }
139 if addr, err := s.PrimaryVerifiedEmail(uid); err != nil || addr != "" {
140 t.Fatalf("PrimaryVerifiedEmail on an unverified primary: %q, %v", addr, err)
141 }
142
143 if err := s.AddEmail(uid, "secondary@example.test", "smtp", false); err != nil {
144 t.Fatal(err)
145 }
146 if addr, err := s.PreferredVerifiedEmail(uid); err != nil || addr != "secondary@example.test" {
147 t.Fatalf("unverified primary, verified secondary: %q, %v", addr, err)
148 }
149 // PrimaryVerifiedEmail keeps meaning exactly what it says: still "",
150 // because the primary itself is still unverified.
151 if addr, err := s.PrimaryVerifiedEmail(uid); err != nil || addr != "" {
152 t.Fatalf("PrimaryVerifiedEmail with only the secondary verified: %q, %v", addr, err)
153 }
154
155 if err := s.VerifyEmail(uid, "primary@example.test", "admin"); err != nil {
156 t.Fatal(err)
157 }
158 if addr, err := s.PreferredVerifiedEmail(uid); err != nil || addr != "primary@example.test" {
159 t.Fatalf("both verified, primary should win: %q, %v", addr, err)
160 }
161}
162
163// With no verified primary, the choice among verified secondaries must not
164// depend on insertion or row order.
165func TestPreferredVerifiedEmailDeterministicTiebreak(t *testing.T) {
166 s := open(t)
167 if err := s.MigrateUp(); err != nil {
168 t.Fatal(err)
169 }
170 uid, err := s.CreateUser("gus", false)
171 if err != nil {
172 t.Fatal(err)
173 }
174 if err := s.AddEmail(uid, "primary@example.test", "", true); err != nil {
175 t.Fatal(err)
176 }
177 if err := s.AddEmail(uid, "zzz@example.test", "smtp", false); err != nil {
178 t.Fatal(err)
179 }
180 if err := s.AddEmail(uid, "aaa@example.test", "smtp", false); err != nil {
181 t.Fatal(err)
182 }
183 if addr, err := s.PreferredVerifiedEmail(uid); err != nil || addr != "aaa@example.test" {
184 t.Fatalf("tiebreak should be alphabetical: %q, %v", addr, err)
185 }
186}
187
188// MRCommentCounts folds conversation comments and diff-thread roots into
189// one count per MR, for the list page. System comments, diff-thread
190// replies, and pending (unpublished) diff comments do not count.
191func TestMRCommentCounts(t *testing.T) {
192 s, repoID, uid := mrFixture(t)
193 mr1, err := s.MRByNumber(repoID, 1)
194 if err != nil {
195 t.Fatal(err)
196 }
197 if _, err := s.CreateMR(repoID, uid, repoID, "feature2", "main", "t2", "", "def456", "md", false); err != nil {
198 t.Fatal(err)
199 }
200 mr2, err := s.MRByNumber(repoID, 2)
201 if err != nil {
202 t.Fatal(err)
203 }
204
205 if err := s.AddMRComment(mr1.ID, uid, "hi", "md"); err != nil {
206 t.Fatal(err)
207 }
208 if err := s.AddMRSystemComment(mr1.ID, uid, "merged"); err != nil {
209 t.Fatal(err)
210 }
211 rootID, err := s.AddDiffComment(mr1.ID, uid, "abc123", "file.txt", "new", 1, 0, "root", 0, false)
212 if err != nil {
213 t.Fatal(err)
214 }
215 if _, err := s.AddDiffComment(mr1.ID, uid, "abc123", "file.txt", "new", 1, 0, "reply", rootID, false); err != nil {
216 t.Fatal(err)
217 }
218 if _, err := s.AddDiffComment(mr1.ID, uid, "abc123", "file.txt", "new", 2, 0, "pending root", 0, true); err != nil {
219 t.Fatal(err)
220 }
221
222 counts, err := s.MRCommentCounts(repoID, []int64{mr1.ID, mr2.ID})
223 if err != nil {
224 t.Fatal(err)
225 }
226 if counts[mr1.ID] != 2 {
227 t.Fatalf("mr1 count = %d, want 2 (1 comment + 1 diff root)", counts[mr1.ID])
228 }
229 if counts[mr2.ID] != 0 {
230 t.Fatalf("mr2 count = %d, want 0", counts[mr2.ID])
231 }
232
233 if empty, err := s.MRCommentCounts(repoID, nil); err != nil || len(empty) != 0 {
234 t.Fatalf("MRCommentCounts(nil) = %v, %v", empty, err)
235 }
236}
237
238// The list batches return what the per-item calls return, keyed by item.
239func TestListPageBatches(t *testing.T) {
240 s, repoID, uid := mrFixture(t)
241 mr1, err := s.MRByNumber(repoID, 1)
242 if err != nil {
243 t.Fatal(err)
244 }
245 if err := s.AddMRReview(mr1.ID, uid, "approve", "abc123"); err != nil {
246 t.Fatal(err)
247 }
248 reviews, err := s.MRReviewsFor([]int64{mr1.ID, mr1.ID + 99})
249 if err != nil {
250 t.Fatal(err)
251 }
252 if got := reviews[mr1.ID]; len(got) != 1 || got[0].Reviewer != "cmc" || got[0].Verdict != "approve" {
253 t.Errorf("reviews = %+v", got)
254 }
255 if len(reviews[mr1.ID+99]) != 0 {
256 t.Errorf("reviews for a missing MR: %+v", reviews[mr1.ID+99])
257 }
258
259 if err := s.SetCommitStatus(repoID, "abc123", "ci/test", "failure", "", "", uid); err != nil {
260 t.Fatal(err)
261 }
262 if err := s.SetCommitStatus(repoID, "abc123", "ci/build", "success", "", "", uid); err != nil {
263 t.Fatal(err)
264 }
265 statuses, err := s.CommitStatusesFor(repoID, []string{"abc123", "def456"})
266 if err != nil {
267 t.Fatal(err)
268 }
269 if got := statuses["abc123"]; len(got) != 2 || got[0].Context != "ci/build" || got[1].State != "failure" {
270 t.Errorf("statuses = %+v", got)
271 }
272 if len(statuses["def456"]) != 0 {
273 t.Errorf("statuses for a commit with none: %+v", statuses["def456"])
274 }
275
276 n, err := s.CreateIssue(repoID, uid, "t", "", "md")
277 if err != nil {
278 t.Fatal(err)
279 }
280 issue, err := s.IssueByNumber(repoID, n)
281 if err != nil {
282 t.Fatal(err)
283 }
284 if err := s.AddIssueComment(issue.ID, uid, "hi", "md"); err != nil {
285 t.Fatal(err)
286 }
287 if err := s.AddIssueSystemComment(issue.ID, uid, "closed"); err != nil {
288 t.Fatal(err)
289 }
290 counts, err := s.IssueCommentCounts([]int64{issue.ID})
291 if err != nil {
292 t.Fatal(err)
293 }
294 if counts[issue.ID] != 1 {
295 t.Errorf("comment count = %d, want 1", counts[issue.ID])
296 }
297}
298
299func TestLabelAndRequestBatches(t *testing.T) {
300 s, repoID, uid := mrFixture(t)
301 mr, err := s.MRByNumber(repoID, 1)
302 if err != nil {
303 t.Fatal(err)
304 }
305 if err := s.SetMRReviewRequest(mr.ID, uid, true); err != nil {
306 t.Fatal(err)
307 }
308 reqs, err := s.MRReviewRequestsFor([]int64{mr.ID})
309 if err != nil {
310 t.Fatal(err)
311 }
312 if got := reqs[mr.ID]; len(got) != 1 || got[0] != "cmc" {
313 t.Errorf("review requests = %v", got)
314 }
315 n, err := s.CreateIssue(repoID, uid, "t", "", "md")
316 if err != nil {
317 t.Fatal(err)
318 }
319 issue, err := s.IssueByNumber(repoID, n)
320 if err != nil {
321 t.Fatal(err)
322 }
323 labels, assignees, err := s.IssueLabelsFor([]int64{issue.ID})
324 if err != nil {
325 t.Fatal(err)
326 }
327 if len(labels[issue.ID]) != 0 || len(assignees[issue.ID]) != 0 {
328 t.Errorf("fresh issue: labels %v, assignees %v", labels[issue.ID], assignees[issue.ID])
329 }
330}