internal/store/labels_test.go
407 lines · 13572 bytes
1package store
2
3import (
4 "errors"
5 "testing"
6)
7
8// acmeFixture: org acme owned by alice with repos acme/core and
9// acme/site, an issue in each, and alice's own alice/app.
10type acmeFixture struct {
11 s *Store
12 alice int64
13 org int64
14 core, site Repo
15 app Repo
16 coreIssue int64
17 siteIssue int64
18 coreMR int64
19 siteMR int64
20}
21
22func newAcme(t *testing.T) acmeFixture {
23 t.Helper()
24 s := open(t)
25 if err := s.MigrateUp(); err != nil {
26 t.Fatal(err)
27 }
28 var f acmeFixture
29 f.s = s
30 var err error
31 if f.alice, err = s.CreateUser("alice", false); err != nil {
32 t.Fatal(err)
33 }
34 if f.org, err = s.CreateOrg("acme", f.alice); err != nil {
35 t.Fatal(err)
36 }
37 mk := func(kind string, owner int64, name string) Repo {
38 id, err := s.CreateRepo(kind, owner, name, "public")
39 if err != nil {
40 t.Fatal(err)
41 }
42 r, err := s.RepoByID(id)
43 if err != nil {
44 t.Fatal(err)
45 }
46 return r
47 }
48 f.core = mk("org", f.org, "core")
49 f.site = mk("org", f.org, "site")
50 f.app = mk("user", f.alice, "app")
51 // CreateIssue returns the per-repo issue number, not the issues.id row
52 // that issue_labels.issue_id references (and that every production
53 // caller of SetIssueLabel passes); resolve it the same way they do, or
54 // core's and site's both-numbered-1 first issues collide.
55 mkIssue := func(repo Repo, title string) int64 {
56 n, err := s.CreateIssue(repo.ID, f.alice, title, "", "md")
57 if err != nil {
58 t.Fatal(err)
59 }
60 iss, err := s.IssueByNumber(repo.ID, n)
61 if err != nil {
62 t.Fatal(err)
63 }
64 return iss.ID
65 }
66 f.coreIssue = mkIssue(f.core, "c1")
67 f.siteIssue = mkIssue(f.site, "s1")
68 // Same resolution for merge requests: CreateMR returns the per-repo
69 // number, mr_labels.mr_id references merge_requests.id.
70 mkMR := func(repo Repo, title string) int64 {
71 n, err := s.CreateMR(repo.ID, f.alice, repo.ID, "topic", "main", title, "", "deadbeef", "md", false)
72 if err != nil {
73 t.Fatal(err)
74 }
75 m, err := s.MRByNumber(repo.ID, n)
76 if err != nil {
77 t.Fatal(err)
78 }
79 return m.ID
80 }
81 f.coreMR = mkMR(f.core, "c!1")
82 f.siteMR = mkMR(f.site, "s!1")
83 return f
84}
85
86func (f acmeFixture) orgRepos() []int64 { return []int64{f.core.ID, f.site.ID} }
87
88func TestOrgLabelSeenByEveryOrgRepo(t *testing.T) {
89 f := newAcme(t)
90 if _, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000"); err != nil {
91 t.Fatal(err)
92 }
93 if err := f.s.SetLabel(f.site, "docs", ""); err != nil {
94 t.Fatal(err)
95 }
96 // site sees the org's bug first, then its own docs; core sees only bug;
97 // alice/app, user-owned, sees nothing.
98 got, err := f.s.ListLabels(f.site, f.orgRepos())
99 if err != nil || len(got) != 2 || got[0].Name != "bug" || !got[0].Org || got[1].Name != "docs" || got[1].Org {
100 t.Fatalf("site labels = %+v, %v", got, err)
101 }
102 if got, _ := f.s.ListLabels(f.core, f.orgRepos()); len(got) != 1 || got[0].Name != "bug" {
103 t.Fatalf("core labels = %+v", got)
104 }
105 if got, _ := f.s.ListLabels(f.app, []int64{f.app.ID}); len(got) != 0 {
106 t.Fatalf("app labels = %+v", got)
107 }
108 colors, _ := f.s.LabelColors(f.core)
109 if colors["bug"] != "#ff0000" {
110 t.Fatalf("core colours = %v", colors)
111 }
112}
113
114func TestIssueLabelResolvesOrgRowFirst(t *testing.T) {
115 f := newAcme(t)
116 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
117 t.Fatal(err)
118 }
119 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", true); err != nil {
120 t.Fatal(err)
121 }
122 if err := f.s.SetIssueLabel(f.site, f.siteIssue, "bug", true); err != nil {
123 t.Fatal(err)
124 }
125 // One org row, no repo rows were created on the fly.
126 var n int
127 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug'").Scan(&n)
128 if n != 1 {
129 t.Fatalf("labels named bug: %d, want 1", n)
130 }
131 // The count spans the org's readable repos.
132 got, _ := f.s.ListOrgLabels(f.org, f.orgRepos())
133 if len(got) != 1 || got[0].Issues != 2 {
134 t.Fatalf("org labels = %+v", got)
135 }
136 got, _ = f.s.ListOrgLabels(f.org, []int64{f.core.ID})
137 if got[0].Issues != 1 {
138 t.Fatalf("org labels over core only = %+v", got)
139 }
140 // A label neither scope has is still created on the fly in the repo.
141 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "adhoc", true); err != nil {
142 t.Fatal(err)
143 }
144 if l, err := f.s.LabelByName(f.core, "adhoc"); err != nil || l.Org {
145 t.Fatalf("adhoc = %+v, %v", l, err)
146 }
147 // Removing by name works for the org row too.
148 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", false); err != nil {
149 t.Fatal(err)
150 }
151 got, _ = f.s.ListOrgLabels(f.org, f.orgRepos())
152 if got[0].Issues != 1 {
153 t.Fatalf("after detach: %+v", got)
154 }
155}
156
157// The web issue list reads labels per repository; an org label attached
158// to an issue has to come back from there like the repository's own.
159func TestListIssueLabelsIncludesOrgRows(t *testing.T) {
160 f := newAcme(t)
161 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
162 t.Fatal(err)
163 }
164 if err := f.s.SetLabel(f.core, "docs", ""); err != nil {
165 t.Fatal(err)
166 }
167 for _, name := range []string{"bug", "docs"} {
168 if err := f.s.SetIssueLabel(f.core, f.coreIssue, name, true); err != nil {
169 t.Fatal(err)
170 }
171 }
172 got, err := f.s.ListIssueLabels(f.core)
173 if err != nil || len(got[f.coreIssue]) != 2 || got[f.coreIssue][0] != "bug" || got[f.coreIssue][1] != "docs" {
174 t.Fatalf("core issue labels = %v, %v", got, err)
175 }
176 // Another repository under the org does not pick up core's attachment.
177 if got, _ := f.s.ListIssueLabels(f.site); len(got) != 0 {
178 t.Fatalf("site issue labels = %v", got)
179 }
180}
181
182// The web merge request list reads labels per repository, the same shape
183// the issue list reads them in; an org label attached to a merge request
184// comes back from there like the repository's own (#231).
185func TestListMRLabelsIncludesOrgRows(t *testing.T) {
186 f := newAcme(t)
187 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
188 t.Fatal(err)
189 }
190 if err := f.s.SetLabel(f.core, "docs", ""); err != nil {
191 t.Fatal(err)
192 }
193 for _, name := range []string{"bug", "docs"} {
194 if err := f.s.SetMRLabel(f.core, f.coreMR, name, true); err != nil {
195 t.Fatal(err)
196 }
197 }
198 got, err := f.s.ListMRLabels(f.core)
199 if err != nil || len(got[f.coreMR]) != 2 || got[f.coreMR][0] != "bug" || got[f.coreMR][1] != "docs" {
200 t.Fatalf("core MR labels = %v, %v", got, err)
201 }
202 // Another repository under the org does not pick up core's attachment.
203 if got, _ := f.s.ListMRLabels(f.site); len(got) != 0 {
204 t.Fatalf("site MR labels = %v", got)
205 }
206 // MRByNumber carries them, and the label listing counts them apart
207 // from issues.
208 m, err := f.s.MRByNumber(f.core.ID, 1)
209 if err != nil || len(m.Labels) != 2 || m.Labels[0] != "bug" {
210 t.Fatalf("MRByNumber labels = %v, %v", m.Labels, err)
211 }
212 rows, _ := f.s.ListLabels(f.core, f.orgRepos())
213 if len(rows) != 2 || rows[0].Name != "bug" || rows[0].MRs != 1 || rows[0].Issues != 0 {
214 t.Fatalf("label rows = %+v", rows)
215 }
216 // The filter narrows to the merge requests carrying the name.
217 mrs, err := f.s.QueryMRs(f.core.ID, MRFilter{State: "all", Label: "bug"})
218 if err != nil || len(mrs) != 1 || mrs[0].ID != f.coreMR {
219 t.Fatalf("QueryMRs by label = %+v, %v", mrs, err)
220 }
221 if mrs, _ := f.s.QueryMRs(f.core.ID, MRFilter{State: "all", Label: "nope"}); len(mrs) != 0 {
222 t.Fatalf("QueryMRs by absent label = %+v", mrs)
223 }
224 // Removing a name nothing carries is not found.
225 if err := f.s.SetMRLabel(f.core, f.coreMR, "nope", false); !errors.Is(err, ErrNotFound) {
226 t.Fatalf("remove of absent label: %v, want ErrNotFound", err)
227 }
228}
229
230// Folding a repository label onto its org's row moves the merge requests
231// carrying it, not only the issues.
232func TestSetOrgLabelFoldsMRLabels(t *testing.T) {
233 f := newAcme(t)
234 if err := f.s.SetMRLabel(f.core, f.coreMR, "bug", true); err != nil {
235 t.Fatal(err)
236 }
237 if err := f.s.SetMRLabel(f.site, f.siteMR, "bug", true); err != nil {
238 t.Fatal(err)
239 }
240 folded, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000")
241 if err != nil || folded != 2 {
242 t.Fatalf("SetOrgLabel folded %d, %v; want 2", folded, err)
243 }
244 var n int
245 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug'").Scan(&n)
246 if n != 1 {
247 t.Fatalf("labels named bug after folding: %d, want 1", n)
248 }
249 // Both merge requests still carry it, now through the org's row.
250 for _, c := range []struct {
251 repo Repo
252 mr int64
253 }{{f.core, f.coreMR}, {f.site, f.siteMR}} {
254 got, _ := f.s.ListMRLabels(c.repo)
255 if len(got[c.mr]) != 1 || got[c.mr][0] != "bug" {
256 t.Fatalf("%s MR labels after folding = %v", c.repo.Name, got)
257 }
258 }
259 rows, _ := f.s.ListOrgLabels(f.org, f.orgRepos())
260 if len(rows) != 1 || rows[0].MRs != 2 {
261 t.Fatalf("org label rows = %+v", rows)
262 }
263 // Deleting the label takes it off every merge request: mr_labels
264 // cascades from labels, nothing unlinks them by hand.
265 if err := f.s.DeleteOrgLabel(f.org, "bug"); err != nil {
266 t.Fatal(err)
267 }
268 for _, repo := range []Repo{f.core, f.site} {
269 if got, err := f.s.ListMRLabels(repo); err != nil || len(got) != 0 {
270 t.Fatalf("%s MR labels after delete = %v, %v", repo.Name, got, err)
271 }
272 }
273}
274
275func TestRepoLabelRefusedWhenOrgHoldsName(t *testing.T) {
276 f := newAcme(t)
277 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
278 t.Fatal(err)
279 }
280 if err := f.s.SetLabel(f.core, "bug", "#00ff00"); !errors.Is(err, ErrOrgScoped) {
281 t.Fatalf("SetLabel over org name: %v, want ErrOrgScoped", err)
282 }
283 if err := f.s.DeleteLabel(f.core, "bug"); !errors.Is(err, ErrOrgScoped) {
284 t.Fatalf("DeleteLabel of org row: %v, want ErrOrgScoped", err)
285 }
286 if err := f.s.DeleteLabel(f.core, "nope"); !errors.Is(err, ErrNotFound) {
287 t.Fatalf("DeleteLabel of nothing: %v, want ErrNotFound", err)
288 }
289 // A user-owned repo is unaffected by any org.
290 if err := f.s.SetLabel(f.app, "bug", ""); err != nil {
291 t.Fatal(err)
292 }
293}
294
295func TestSetOrgLabelPromotesRepoLabels(t *testing.T) {
296 f := newAcme(t)
297 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", true); err != nil {
298 t.Fatal(err)
299 }
300 if err := f.s.SetIssueLabel(f.site, f.siteIssue, "bug", true); err != nil {
301 t.Fatal(err)
302 }
303 if err := f.s.SetLabel(f.app, "bug", "#123456"); err != nil {
304 t.Fatal(err)
305 }
306 folded, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000")
307 if err != nil || folded != 2 {
308 t.Fatalf("SetOrgLabel folded %d, %v; want 2", folded, err)
309 }
310 var n int
311 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug' AND org_id = ?", f.org).Scan(&n)
312 if n != 1 {
313 t.Fatalf("org rows named bug: %d", n)
314 }
315 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug' AND repo_id IN (?, ?)", f.core.ID, f.site.ID).Scan(&n)
316 if n != 0 {
317 t.Fatalf("repo rows named bug left under the org: %d", n)
318 }
319 got, _ := f.s.ListOrgLabels(f.org, f.orgRepos())
320 if len(got) != 1 || got[0].Issues != 2 || got[0].Color != "#ff0000" {
321 t.Fatalf("after promote: %+v", got)
322 }
323 // alice/app's own bug is another owner's and stays.
324 if l, err := f.s.LabelByName(f.app, "bug"); err != nil || l.Color != "#123456" {
325 t.Fatalf("app bug = %+v, %v", l, err)
326 }
327 // A second set only recolours.
328 if folded, err := f.s.SetOrgLabel(f.org, "bug", "#0000ff"); err != nil || folded != 0 {
329 t.Fatalf("second set folded %d, %v", folded, err)
330 }
331 if err := f.s.DeleteOrgLabel(f.org, "bug"); err != nil {
332 t.Fatal(err)
333 }
334 if err := f.s.DeleteOrgLabel(f.org, "bug"); !errors.Is(err, ErrNotFound) {
335 t.Fatalf("second delete: %v", err)
336 }
337 f.s.DB.QueryRow("SELECT COUNT(*) FROM issue_labels").Scan(&n)
338 if n != 0 {
339 t.Fatalf("memberships after org delete: %d", n)
340 }
341}
342
343// A repository moving into an org brings its own labels and milestones;
344// the names the org already holds fold into the org's rows rather than
345// leaving the repository seeing two of each.
346func TestTransferIntoOrgFoldsDuplicateNames(t *testing.T) {
347 f := newAcme(t)
348 n, err := f.s.CreateIssue(f.app.ID, f.alice, "a1", "", "md")
349 if err != nil {
350 t.Fatal(err)
351 }
352 issue, err := f.s.IssueByNumber(f.app.ID, n)
353 if err != nil {
354 t.Fatal(err)
355 }
356 if err := f.s.SetLabel(f.app, "bug", "#123456"); err != nil {
357 t.Fatal(err)
358 }
359 if err := f.s.SetIssueLabel(f.app, issue.ID, "bug", true); err != nil {
360 t.Fatal(err)
361 }
362 repoMS, err := f.s.CreateMilestone(f.app, "v1", "", "")
363 if err != nil {
364 t.Fatal(err)
365 }
366 if err := f.s.SetIssueMilestone(issue.ID, repoMS); err != nil {
367 t.Fatal(err)
368 }
369 if _, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000"); err != nil {
370 t.Fatal(err)
371 }
372 orgMS, _, err := f.s.CreateOrgMilestone(f.org, "v1", "", "")
373 if err != nil {
374 t.Fatal(err)
375 }
376 if err := f.s.TransferRepo(f.app.ID, "org", f.org); err != nil {
377 t.Fatal(err)
378 }
379 app, err := f.s.RepoByID(f.app.ID)
380 if err != nil {
381 t.Fatal(err)
382 }
383 labels, err := f.s.ListLabels(app, []int64{app.ID})
384 if err != nil || len(labels) != 1 || !labels[0].Org || labels[0].Color != "#ff0000" || labels[0].Issues != 1 {
385 t.Fatalf("labels after transfer = %+v, %v", labels, err)
386 }
387 ms, err := f.s.ListMilestones(app, "all", []int64{app.ID})
388 if err != nil || len(ms) != 1 || ms[0].ID != orgMS || ms[0].OrgID != f.org || ms[0].OpenItems != 1 {
389 t.Fatalf("milestones after transfer = %+v, %v", ms, err)
390 }
391 // The issue keeps both, pointing at the org's rows; the repository's
392 // rows are gone.
393 var count int
394 f.s.DB.QueryRow(`SELECT COUNT(*) FROM issue_labels il JOIN labels l ON l.id = il.label_id
395 WHERE il.issue_id = ? AND l.org_id = ?`, issue.ID, f.org).Scan(&count)
396 if count != 1 {
397 t.Fatalf("label membership after transfer: %d", count)
398 }
399 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE repo_id = ?", app.ID).Scan(&count)
400 if count != 0 {
401 t.Fatalf("repo label rows left: %d", count)
402 }
403 f.s.DB.QueryRow("SELECT COUNT(*) FROM milestones WHERE id = ?", repoMS).Scan(&count)
404 if count != 0 {
405 t.Fatalf("repo milestone row left: %d", count)
406 }
407}