internal/store/savedqueries_test.go

main
gitbay/internal/store/savedqueries_test.go history · blame · raw

283 lines · 8858 bytes

9 symbols in this file
  1package store
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"testing"
  7)
  8
  9type queryFixture struct {
 10	s                  *Store
 11	alice, bob, carol  int64
 12	pub, priv, bobPriv Repo
 13}
 14
 15// newQueryFixture: alice owns a public and a private repository, bob a
 16// private one; carol owns nothing and is granted nothing.
 17func newQueryFixture(t *testing.T) queryFixture {
 18	t.Helper()
 19	s := open(t)
 20	if err := s.MigrateUp(); err != nil {
 21		t.Fatal(err)
 22	}
 23	var f queryFixture
 24	f.s = s
 25	must := func(id int64, err error) int64 {
 26		t.Helper()
 27		if err != nil {
 28			t.Fatal(err)
 29		}
 30		return id
 31	}
 32	f.alice = must(s.CreateUser("alice", false))
 33	f.bob = must(s.CreateUser("bob", false))
 34	f.carol = must(s.CreateUser("carol", false))
 35	repo := func(owner int64, name, vis string) Repo {
 36		r, err := s.RepoByID(must(s.CreateRepo("user", owner, name, vis)))
 37		if err != nil {
 38			t.Fatal(err)
 39		}
 40		return r
 41	}
 42	f.pub = repo(f.alice, "pub", "public")
 43	f.priv = repo(f.alice, "priv", "private")
 44	f.bobPriv = repo(f.bob, "secret", "private")
 45	return f
 46}
 47
 48func (f queryFixture) issue(t *testing.T, r Repo, author int64, title string) Issue {
 49	t.Helper()
 50	n, err := f.s.CreateIssue(r.ID, author, title, "", "md")
 51	if err != nil {
 52		t.Fatal(err)
 53	}
 54	i, err := f.s.IssueByNumber(r.ID, n)
 55	if err != nil {
 56		t.Fatal(err)
 57	}
 58	return i
 59}
 60
 61func (f queryFixture) mr(t *testing.T, r Repo, author int64, title string) MR {
 62	t.Helper()
 63	n, err := f.s.CreateMR(r.ID, author, r.ID, "topic", "main", title, "", "", "md", false)
 64	if err != nil {
 65		t.Fatal(err)
 66	}
 67	m, err := f.s.MRByNumber(r.ID, n)
 68	if err != nil {
 69		t.Fatal(err)
 70	}
 71	return m
 72}
 73
 74func titles(items []Item) []string {
 75	var out []string
 76	for _, it := range items {
 77		out = append(out, it.Title)
 78	}
 79	return out
 80}
 81
 82// A query reaches only what the caller may read: someone else's private
 83// repository is absent from the rows and from the count, even when the
 84// query names it.
 85func TestQueryItemsSkipsUnreadableRepositories(t *testing.T) {
 86	f := newQueryFixture(t)
 87	f.issue(t, f.pub, f.alice, "public bug")
 88	f.issue(t, f.priv, f.alice, "alice private bug")
 89	f.issue(t, f.bobPriv, f.bob, "bob private bug")
 90	f.mr(t, f.bobPriv, f.bob, "bob private mr")
 91	f.mr(t, f.pub, f.alice, "public mr")
 92
 93	all := ItemFilter{Issues: true, MRs: true}
 94	for _, tc := range []struct {
 95		name string
 96		user int64
 97		f    ItemFilter
 98		want int
 99	}{
100		{"carol, everything", f.carol, all, 2},
101		{"alice, everything", f.alice, all, 3},
102		{"bob, everything", f.bob, all, 4},
103		{"carol naming bob's repository", f.carol, ItemFilter{Issues: true, MRs: true, Scopes: []RepoScope{{Owner: "bob", Name: "secret"}}}, 0},
104		{"carol naming bob", f.carol, ItemFilter{Issues: true, MRs: true, Scopes: []RepoScope{{Owner: "bob"}}}, 0},
105		{"carol globbing alice", f.carol, ItemFilter{Issues: true, MRs: true, Scopes: []RepoScope{{Owner: "alice", Name: "p*"}}}, 2},
106		{"alice globbing herself", f.alice, ItemFilter{Issues: true, MRs: true, Scopes: []RepoScope{{Owner: "alice", Name: "p*"}}}, 3},
107		{"carol by bob's text", f.carol, ItemFilter{Issues: true, MRs: true, Text: "bob"}, 0},
108	} {
109		items, err := f.s.QueryItems(tc.user, tc.f, nil, 0)
110		if err != nil {
111			t.Fatal(err)
112		}
113		n, err := f.s.CountItems(tc.user, tc.f)
114		if err != nil {
115			t.Fatal(err)
116		}
117		if len(items) != tc.want || n != tc.want {
118			t.Errorf("%s: %d rows %q, count %d; want %d", tc.name, len(items), titles(items), n, tc.want)
119		}
120	}
121
122	// A grant opens bob's repository to carol.
123	if err := f.s.GrantAccess(f.bobPriv.ID, f.carol, "read"); err != nil {
124		t.Fatal(err)
125	}
126	if n, _ := f.s.CountItems(f.carol, all); n != 4 {
127		t.Errorf("carol with a grant counts %d, want 4", n)
128	}
129}
130
131func TestQueryItemsFilters(t *testing.T) {
132	f := newQueryFixture(t)
133	bug := f.issue(t, f.pub, f.alice, "labelled bug")
134	if err := f.s.SetIssueLabel(f.pub, bug.ID, "bug", true); err != nil {
135		t.Fatal(err)
136	}
137	if err := f.s.SetIssueLabel(f.pub, bug.ID, "ui", true); err != nil {
138		t.Fatal(err)
139	}
140	both := f.issue(t, f.pub, f.bob, "assigned in milestone")
141	ms, err := f.s.CreateMilestone(f.pub, "v2", "", "")
142	if err != nil {
143		t.Fatal(err)
144	}
145	if err := f.s.SetIssueMilestone(both.ID, ms); err != nil {
146		t.Fatal(err)
147	}
148	if err := f.s.SetIssueAssignee(both.ID, f.carol, true); err != nil {
149		t.Fatal(err)
150	}
151	closed := f.issue(t, f.pub, f.alice, "closed one")
152	if err := f.s.SetIssueState(closed.ID, "closed"); err != nil {
153		t.Fatal(err)
154	}
155	merged := f.mr(t, f.pub, f.alice, "merged mr")
156	if err := f.s.MarkMerged(merged.ID, "", f.alice, ""); err != nil {
157		t.Fatal(err)
158	}
159	f.mr(t, f.pub, f.bob, "open mr")
160
161	for _, tc := range []struct {
162		f    ItemFilter
163		want string
164	}{
165		{ItemFilter{Issues: true, MRs: true, State: "open"}, "[open mr assigned in milestone labelled bug]"},
166		{ItemFilter{Issues: true, MRs: true, State: "merged"}, "[merged mr]"},
167		{ItemFilter{Issues: true, State: "closed"}, "[closed one]"},
168		{ItemFilter{Issues: true, MRs: true, Labels: []string{"bug", "ui"}}, "[labelled bug]"},
169		{ItemFilter{Issues: true, MRs: true, Labels: []string{"bug", "nope"}}, "[]"},
170		{ItemFilter{Issues: true, NoLabel: true}, "[closed one assigned in milestone]"},
171		{ItemFilter{Issues: true, Milestone: "v2"}, "[assigned in milestone]"},
172		{ItemFilter{Issues: true, NoMilestone: true}, "[closed one labelled bug]"},
173		{ItemFilter{Issues: true, Assignee: "carol"}, "[assigned in milestone]"},
174		{ItemFilter{Issues: true, MRs: true, Author: "bob"}, "[open mr assigned in milestone]"},
175		{ItemFilter{MRs: true}, "[open mr merged mr]"},
176		{ItemFilter{}, "[]"},
177	} {
178		items, err := f.s.QueryItems(f.alice, tc.f, nil, 0)
179		if err != nil {
180			t.Fatal(err)
181		}
182		if got := fmt.Sprint(titles(items)); got != tc.want && !(got == "[]" && tc.want == "[]") {
183			t.Errorf("%+v = %s, want %s", tc.f, got, tc.want)
184		}
185	}
186}
187
188// Paging walks every row once across repositories and both tables, even
189// when rows share a creation time: the cursor carries the tie-breakers.
190func TestQueryItemsPagesAcrossRepositories(t *testing.T) {
191	f := newQueryFixture(t)
192	var want []string
193	for i := 0; i < 5; i++ {
194		f.issue(t, f.pub, f.alice, fmt.Sprintf("pub issue %d", i))
195		f.issue(t, f.priv, f.alice, fmt.Sprintf("priv issue %d", i))
196		f.mr(t, f.pub, f.alice, fmt.Sprintf("pub mr %d", i))
197		f.issue(t, f.bobPriv, f.bob, fmt.Sprintf("bob issue %d", i))
198	}
199	// Half the rows at one instant, so ordering falls to kind and id.
200	if _, err := f.s.DB.Exec("UPDATE issues SET created_at = '2026-01-01T00:00:00.000Z' WHERE id % 2 = 0"); err != nil {
201		t.Fatal(err)
202	}
203	if _, err := f.s.DB.Exec("UPDATE merge_requests SET created_at = '2026-01-01T00:00:00.000Z' WHERE id % 2 = 1"); err != nil {
204		t.Fatal(err)
205	}
206	all := ItemFilter{Issues: true, MRs: true}
207	full, err := f.s.QueryItems(f.alice, all, nil, 0)
208	if err != nil {
209		t.Fatal(err)
210	}
211	if len(full) != 15 {
212		t.Fatalf("alice reads %d rows, want 15", len(full))
213	}
214	for _, it := range full {
215		want = append(want, it.Kind+it.Title)
216	}
217	var got []string
218	var after *ItemCursor
219	for pages := 0; ; pages++ {
220		if pages > 20 {
221			t.Fatal("paging does not end")
222		}
223		page, err := f.s.QueryItems(f.alice, all, after, 4)
224		if err != nil {
225			t.Fatal(err)
226		}
227		for _, it := range page {
228			got = append(got, it.Kind+it.Title)
229		}
230		if len(page) < 4 {
231			break
232		}
233		c := page[len(page)-1].Cursor()
234		after = &c
235	}
236	if fmt.Sprint(got) != fmt.Sprint(want) {
237		t.Errorf("paged\n%v\nwant\n%v", got, want)
238	}
239}
240
241func TestSavedQueries(t *testing.T) {
242	f := newQueryFixture(t)
243	s := f.s
244	if err := s.SaveQuery(f.alice, "mine", "is:open", false); err != nil {
245		t.Fatal(err)
246	}
247	if err := s.SaveQuery(f.alice, "mine", "is:closed", false); !errors.Is(err, ErrExists) {
248		t.Fatalf("second save without replace: %v, want ErrExists", err)
249	}
250	if err := s.PinSavedQuery(f.alice, "mine", true); err != nil {
251		t.Fatal(err)
252	}
253	if err := s.SaveQuery(f.alice, "mine", "is:closed", true); err != nil {
254		t.Fatal(err)
255	}
256	q, err := s.SavedQueryByName(f.alice, "mine")
257	if err != nil || q.Query != "is:closed" || !q.Pinned {
258		t.Fatalf("after replace: %+v %v; want the new text, still pinned", q, err)
259	}
260	// Names are per user.
261	if err := s.SaveQuery(f.bob, "mine", "is:open", false); err != nil {
262		t.Fatal(err)
263	}
264	if _, err := s.SavedQueryByName(f.carol, "mine"); !errors.Is(err, ErrNotFound) {
265		t.Fatalf("carol sees alice's query: %v", err)
266	}
267	if err := s.SaveQuery(f.alice, "other", "is:open", false); err != nil {
268		t.Fatal(err)
269	}
270	pinned, err := s.SavedQueries(f.alice, true)
271	if err != nil || len(pinned) != 1 || pinned[0].Name != "mine" {
272		t.Fatalf("pinned = %+v %v", pinned, err)
273	}
274	if err := s.RemoveSavedQuery(f.alice, "mine"); err != nil {
275		t.Fatal(err)
276	}
277	if err := s.RemoveSavedQuery(f.alice, "mine"); !errors.Is(err, ErrNotFound) {
278		t.Fatalf("second remove: %v", err)
279	}
280	if err := s.PinSavedQuery(f.alice, "gone", true); !errors.Is(err, ErrNotFound) {
281		t.Fatalf("pin of a missing query: %v", err)
282	}
283}