internal/store/labels_test.go

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

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}