internal/store/labels_test.go
297 lines · 9555 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}
19
20func newAcme(t *testing.T) acmeFixture {
21 t.Helper()
22 s := open(t)
23 if err := s.MigrateUp(); err != nil {
24 t.Fatal(err)
25 }
26 var f acmeFixture
27 f.s = s
28 var err error
29 if f.alice, err = s.CreateUser("alice", false); err != nil {
30 t.Fatal(err)
31 }
32 if f.org, err = s.CreateOrg("acme", f.alice); err != nil {
33 t.Fatal(err)
34 }
35 mk := func(kind string, owner int64, name string) Repo {
36 id, err := s.CreateRepo(kind, owner, name, "public")
37 if err != nil {
38 t.Fatal(err)
39 }
40 r, err := s.RepoByID(id)
41 if err != nil {
42 t.Fatal(err)
43 }
44 return r
45 }
46 f.core = mk("org", f.org, "core")
47 f.site = mk("org", f.org, "site")
48 f.app = mk("user", f.alice, "app")
49 // CreateIssue returns the per-repo issue number, not the issues.id row
50 // that issue_labels.issue_id references (and that every production
51 // caller of SetIssueLabel passes); resolve it the same way they do, or
52 // core's and site's both-numbered-1 first issues collide.
53 mkIssue := func(repo Repo, title string) int64 {
54 n, err := s.CreateIssue(repo.ID, f.alice, title, "", "md")
55 if err != nil {
56 t.Fatal(err)
57 }
58 iss, err := s.IssueByNumber(repo.ID, n)
59 if err != nil {
60 t.Fatal(err)
61 }
62 return iss.ID
63 }
64 f.coreIssue = mkIssue(f.core, "c1")
65 f.siteIssue = mkIssue(f.site, "s1")
66 return f
67}
68
69func (f acmeFixture) orgRepos() []int64 { return []int64{f.core.ID, f.site.ID} }
70
71func TestOrgLabelSeenByEveryOrgRepo(t *testing.T) {
72 f := newAcme(t)
73 if _, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000"); err != nil {
74 t.Fatal(err)
75 }
76 if err := f.s.SetLabel(f.site, "docs", ""); err != nil {
77 t.Fatal(err)
78 }
79 // site sees the org's bug first, then its own docs; core sees only bug;
80 // alice/app, user-owned, sees nothing.
81 got, err := f.s.ListLabels(f.site, f.orgRepos())
82 if err != nil || len(got) != 2 || got[0].Name != "bug" || !got[0].Org || got[1].Name != "docs" || got[1].Org {
83 t.Fatalf("site labels = %+v, %v", got, err)
84 }
85 if got, _ := f.s.ListLabels(f.core, f.orgRepos()); len(got) != 1 || got[0].Name != "bug" {
86 t.Fatalf("core labels = %+v", got)
87 }
88 if got, _ := f.s.ListLabels(f.app, []int64{f.app.ID}); len(got) != 0 {
89 t.Fatalf("app labels = %+v", got)
90 }
91 colors, _ := f.s.LabelColors(f.core)
92 if colors["bug"] != "#ff0000" {
93 t.Fatalf("core colours = %v", colors)
94 }
95}
96
97func TestIssueLabelResolvesOrgRowFirst(t *testing.T) {
98 f := newAcme(t)
99 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
100 t.Fatal(err)
101 }
102 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", true); err != nil {
103 t.Fatal(err)
104 }
105 if err := f.s.SetIssueLabel(f.site, f.siteIssue, "bug", true); err != nil {
106 t.Fatal(err)
107 }
108 // One org row, no repo rows were created on the fly.
109 var n int
110 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug'").Scan(&n)
111 if n != 1 {
112 t.Fatalf("labels named bug: %d, want 1", n)
113 }
114 // The count spans the org's readable repos.
115 got, _ := f.s.ListOrgLabels(f.org, f.orgRepos())
116 if len(got) != 1 || got[0].Issues != 2 {
117 t.Fatalf("org labels = %+v", got)
118 }
119 got, _ = f.s.ListOrgLabels(f.org, []int64{f.core.ID})
120 if got[0].Issues != 1 {
121 t.Fatalf("org labels over core only = %+v", got)
122 }
123 // A label neither scope has is still created on the fly in the repo.
124 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "adhoc", true); err != nil {
125 t.Fatal(err)
126 }
127 if l, err := f.s.LabelByName(f.core, "adhoc"); err != nil || l.Org {
128 t.Fatalf("adhoc = %+v, %v", l, err)
129 }
130 // Removing by name works for the org row too.
131 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", false); err != nil {
132 t.Fatal(err)
133 }
134 got, _ = f.s.ListOrgLabels(f.org, f.orgRepos())
135 if got[0].Issues != 1 {
136 t.Fatalf("after detach: %+v", got)
137 }
138}
139
140// The web issue list reads labels per repository; an org label attached
141// to an issue has to come back from there like the repository's own.
142func TestListIssueLabelsIncludesOrgRows(t *testing.T) {
143 f := newAcme(t)
144 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
145 t.Fatal(err)
146 }
147 if err := f.s.SetLabel(f.core, "docs", ""); err != nil {
148 t.Fatal(err)
149 }
150 for _, name := range []string{"bug", "docs"} {
151 if err := f.s.SetIssueLabel(f.core, f.coreIssue, name, true); err != nil {
152 t.Fatal(err)
153 }
154 }
155 got, err := f.s.ListIssueLabels(f.core)
156 if err != nil || len(got[f.coreIssue]) != 2 || got[f.coreIssue][0] != "bug" || got[f.coreIssue][1] != "docs" {
157 t.Fatalf("core issue labels = %v, %v", got, err)
158 }
159 // Another repository under the org does not pick up core's attachment.
160 if got, _ := f.s.ListIssueLabels(f.site); len(got) != 0 {
161 t.Fatalf("site issue labels = %v", got)
162 }
163}
164
165func TestRepoLabelRefusedWhenOrgHoldsName(t *testing.T) {
166 f := newAcme(t)
167 if _, err := f.s.SetOrgLabel(f.org, "bug", ""); err != nil {
168 t.Fatal(err)
169 }
170 if err := f.s.SetLabel(f.core, "bug", "#00ff00"); !errors.Is(err, ErrOrgScoped) {
171 t.Fatalf("SetLabel over org name: %v, want ErrOrgScoped", err)
172 }
173 if err := f.s.DeleteLabel(f.core, "bug"); !errors.Is(err, ErrOrgScoped) {
174 t.Fatalf("DeleteLabel of org row: %v, want ErrOrgScoped", err)
175 }
176 if err := f.s.DeleteLabel(f.core, "nope"); !errors.Is(err, ErrNotFound) {
177 t.Fatalf("DeleteLabel of nothing: %v, want ErrNotFound", err)
178 }
179 // A user-owned repo is unaffected by any org.
180 if err := f.s.SetLabel(f.app, "bug", ""); err != nil {
181 t.Fatal(err)
182 }
183}
184
185func TestSetOrgLabelPromotesRepoLabels(t *testing.T) {
186 f := newAcme(t)
187 if err := f.s.SetIssueLabel(f.core, f.coreIssue, "bug", true); err != nil {
188 t.Fatal(err)
189 }
190 if err := f.s.SetIssueLabel(f.site, f.siteIssue, "bug", true); err != nil {
191 t.Fatal(err)
192 }
193 if err := f.s.SetLabel(f.app, "bug", "#123456"); err != nil {
194 t.Fatal(err)
195 }
196 folded, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000")
197 if err != nil || folded != 2 {
198 t.Fatalf("SetOrgLabel folded %d, %v; want 2", folded, err)
199 }
200 var n int
201 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug' AND org_id = ?", f.org).Scan(&n)
202 if n != 1 {
203 t.Fatalf("org rows named bug: %d", n)
204 }
205 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE name = 'bug' AND repo_id IN (?, ?)", f.core.ID, f.site.ID).Scan(&n)
206 if n != 0 {
207 t.Fatalf("repo rows named bug left under the org: %d", n)
208 }
209 got, _ := f.s.ListOrgLabels(f.org, f.orgRepos())
210 if len(got) != 1 || got[0].Issues != 2 || got[0].Color != "#ff0000" {
211 t.Fatalf("after promote: %+v", got)
212 }
213 // alice/app's own bug is another owner's and stays.
214 if l, err := f.s.LabelByName(f.app, "bug"); err != nil || l.Color != "#123456" {
215 t.Fatalf("app bug = %+v, %v", l, err)
216 }
217 // A second set only recolours.
218 if folded, err := f.s.SetOrgLabel(f.org, "bug", "#0000ff"); err != nil || folded != 0 {
219 t.Fatalf("second set folded %d, %v", folded, err)
220 }
221 if err := f.s.DeleteOrgLabel(f.org, "bug"); err != nil {
222 t.Fatal(err)
223 }
224 if err := f.s.DeleteOrgLabel(f.org, "bug"); !errors.Is(err, ErrNotFound) {
225 t.Fatalf("second delete: %v", err)
226 }
227 f.s.DB.QueryRow("SELECT COUNT(*) FROM issue_labels").Scan(&n)
228 if n != 0 {
229 t.Fatalf("memberships after org delete: %d", n)
230 }
231}
232
233// A repository moving into an org brings its own labels and milestones;
234// the names the org already holds fold into the org's rows rather than
235// leaving the repository seeing two of each.
236func TestTransferIntoOrgFoldsDuplicateNames(t *testing.T) {
237 f := newAcme(t)
238 n, err := f.s.CreateIssue(f.app.ID, f.alice, "a1", "", "md")
239 if err != nil {
240 t.Fatal(err)
241 }
242 issue, err := f.s.IssueByNumber(f.app.ID, n)
243 if err != nil {
244 t.Fatal(err)
245 }
246 if err := f.s.SetLabel(f.app, "bug", "#123456"); err != nil {
247 t.Fatal(err)
248 }
249 if err := f.s.SetIssueLabel(f.app, issue.ID, "bug", true); err != nil {
250 t.Fatal(err)
251 }
252 repoMS, err := f.s.CreateMilestone(f.app, "v1", "", "")
253 if err != nil {
254 t.Fatal(err)
255 }
256 if err := f.s.SetIssueMilestone(issue.ID, repoMS); err != nil {
257 t.Fatal(err)
258 }
259 if _, err := f.s.SetOrgLabel(f.org, "bug", "#ff0000"); err != nil {
260 t.Fatal(err)
261 }
262 orgMS, _, err := f.s.CreateOrgMilestone(f.org, "v1", "", "")
263 if err != nil {
264 t.Fatal(err)
265 }
266 if err := f.s.TransferRepo(f.app.ID, "org", f.org); err != nil {
267 t.Fatal(err)
268 }
269 app, err := f.s.RepoByID(f.app.ID)
270 if err != nil {
271 t.Fatal(err)
272 }
273 labels, err := f.s.ListLabels(app, []int64{app.ID})
274 if err != nil || len(labels) != 1 || !labels[0].Org || labels[0].Color != "#ff0000" || labels[0].Issues != 1 {
275 t.Fatalf("labels after transfer = %+v, %v", labels, err)
276 }
277 ms, err := f.s.ListMilestones(app, "all", []int64{app.ID})
278 if err != nil || len(ms) != 1 || ms[0].ID != orgMS || ms[0].OrgID != f.org || ms[0].OpenItems != 1 {
279 t.Fatalf("milestones after transfer = %+v, %v", ms, err)
280 }
281 // The issue keeps both, pointing at the org's rows; the repository's
282 // rows are gone.
283 var count int
284 f.s.DB.QueryRow(`SELECT COUNT(*) FROM issue_labels il JOIN labels l ON l.id = il.label_id
285 WHERE il.issue_id = ? AND l.org_id = ?`, issue.ID, f.org).Scan(&count)
286 if count != 1 {
287 t.Fatalf("label membership after transfer: %d", count)
288 }
289 f.s.DB.QueryRow("SELECT COUNT(*) FROM labels WHERE repo_id = ?", app.ID).Scan(&count)
290 if count != 0 {
291 t.Fatalf("repo label rows left: %d", count)
292 }
293 f.s.DB.QueryRow("SELECT COUNT(*) FROM milestones WHERE id = ?", repoMS).Scan(&count)
294 if count != 0 {
295 t.Fatalf("repo milestone row left: %d", count)
296 }
297}