internal/store/mrs_test.go
236 lines · 7300 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}