internal/store/labels_test.go

e9566eed86ebcd185c4b85f63d667b5e671fe787
gitbay/internal/store/labels_test.go history · blame · raw

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}