internal/control/query_test.go

v1.40.1
gitbay/internal/control/query_test.go history · blame · raw

258 lines · 8868 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"strings"
  8	"testing"
  9
 10	"gitbay.org/gitbay/internal/config"
 11	"gitbay.org/gitbay/internal/protocol"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15type queryEnv struct {
 16	t     *testing.T
 17	st    *store.Store
 18	users map[string]store.User
 19}
 20
 21func newQueryEnv(t *testing.T) queryEnv {
 22	st, err := store.Open(":memory:")
 23	if err != nil {
 24		t.Fatal(err)
 25	}
 26	t.Cleanup(func() { st.Close() })
 27	if err := st.MigrateUp(); err != nil {
 28		t.Fatal(err)
 29	}
 30	e := queryEnv{t, st, map[string]store.User{}}
 31	for _, name := range []string{"alice", "bob"} {
 32		id, err := st.CreateUser(name, false)
 33		if err != nil {
 34			t.Fatal(err)
 35		}
 36		e.users[name] = store.User{ID: id, Username: name}
 37	}
 38	return e
 39}
 40
 41// run dispatches argv as user with --json and returns the exit code,
 42// the envelope's data and stderr.
 43func (e queryEnv) run(user string, argv ...string) (int, json.RawMessage, string) {
 44	var out, errOut bytes.Buffer
 45	c := &Ctx{User: e.users[user], Scope: "full", Store: e.st, Cfg: config.Config{Limits: config.Limits{WriteRate: -1}},
 46		Stdin: strings.NewReader(""), Stdout: &out, Stderr: &errOut}
 47	code := Dispatch(c, append(argv, "--json"))
 48	var env struct {
 49		Data json.RawMessage `json:"data"`
 50	}
 51	json.Unmarshal(out.Bytes(), &env)
 52	return code, env.Data, errOut.String() + out.String()
 53}
 54
 55func (e queryEnv) repo(owner, name, vis string) store.Repo {
 56	id, err := e.st.CreateRepo("user", e.users[owner].ID, name, vis)
 57	if err != nil {
 58		e.t.Fatal(err)
 59	}
 60	r, err := e.st.RepoByID(id)
 61	if err != nil {
 62		e.t.Fatal(err)
 63	}
 64	return r
 65}
 66
 67func TestQueryCommands(t *testing.T) {
 68	e := newQueryEnv(t)
 69	pub := e.repo("alice", "pub", "public")
 70	secret := e.repo("bob", "secret", "private")
 71	for i, r := range []store.Repo{pub, secret, pub} {
 72		owner := e.users[r.OwnerName]
 73		if _, err := e.st.CreateIssue(r.ID, owner.ID, "issue "+string(rune('a'+i)), "", "md"); err != nil {
 74			t.Fatal(err)
 75		}
 76	}
 77	if _, err := e.st.CreateMR(pub.ID, e.users["alice"].ID, pub.ID, "topic", "main", "an mr", "", "", "md", false); err != nil {
 78		t.Fatal(err)
 79	}
 80
 81	if code, _, msg := e.run("alice", "query", "save", "Bad Name", "is:open"); code != protocol.ExitUsage {
 82		t.Fatalf("bad name: exit %d %s", code, msg)
 83	}
 84	if code, _, msg := e.run("alice", "query", "save", "open", "is:open", "bogus:x"); code != protocol.ExitUsage || !strings.Contains(msg, "bogus:x") {
 85		t.Fatalf("bad term: exit %d %s; want 2 naming the term", code, msg)
 86	}
 87	code, data, msg := e.run("alice", "query", "save", "open", "is:open is:open", "label:needs review")
 88	if code != 0 {
 89		t.Fatalf("save: exit %d %s", code, msg)
 90	}
 91	var saved SavedQueryOut
 92	json.Unmarshal(data, &saved)
 93	if saved.Query != "is:open label:needs review" {
 94		t.Errorf("stored %q: want the canonical text", saved.Query)
 95	}
 96	if code, _, msg := e.run("alice", "query", "save", "open", "is:open"); code != protocol.ExitFailure || !strings.Contains(msg, "--force") {
 97		t.Fatalf("save over an existing name: exit %d %s", code, msg)
 98	}
 99	if code, _, msg := e.run("alice", "query", "save", "open", "is:open", "--force"); code != 0 {
100		t.Fatalf("save --force: exit %d %s", code, msg)
101	}
102
103	// bob's saved query sees his private repository; the same text run
104	// by alice does not, and her count does not include it.
105	for _, u := range []string{"alice", "bob"} {
106		if code, _, msg := e.run(u, "query", "save", "all", "is:open"); code != 0 {
107			t.Fatalf("save as %s: exit %d %s", u, code, msg)
108		}
109	}
110	count := func(user string) int {
111		code, data, msg := e.run(user, "query", "show", "all")
112		if code != 0 {
113			t.Fatalf("show as %s: exit %d %s", user, code, msg)
114		}
115		var d SavedQueryOut
116		json.Unmarshal(data, &d)
117		return *d.Count
118	}
119	if a, b := count("alice"), count("bob"); a != 3 || b != 4 {
120		t.Errorf("counts alice %d bob %d, want 3 and 4", a, b)
121	}
122
123	type pageOut struct {
124		Items []QueryItem `json:"items"`
125		Next  string      `json:"next"`
126	}
127	var seen []string
128	cursor := ""
129	for i := 0; i < 5; i++ {
130		argv := []string{"query", "run", "all", "--limit", "1"}
131		if cursor != "" {
132			argv = append(argv, "--cursor", cursor)
133		}
134		code, data, msg := e.run("alice", argv...)
135		if code != 0 {
136			t.Fatalf("run: exit %d %s", code, msg)
137		}
138		var p pageOut
139		if err := json.Unmarshal(data, &p); err != nil {
140			t.Fatal(err)
141		}
142		for _, it := range p.Items {
143			seen = append(seen, it.Ref())
144		}
145		if cursor = p.Next; cursor == "" {
146			break
147		}
148	}
149	if got := strings.Join(seen, " "); got != "alice/pub!1 alice/pub#2 alice/pub#1" {
150		t.Errorf("paged run = %s", got)
151	}
152
153	// issue list and mr list narrow to their own kind.
154	code, data, msg = e.run("alice", "issue", "list", "--query", "all")
155	var p pageOut
156	json.Unmarshal(data, &p)
157	if code != 0 || len(p.Items) != 2 || p.Items[0].Kind != "issue" {
158		t.Errorf("issue list --query: exit %d %s", code, msg)
159	}
160	code, data, msg = e.run("alice", "mr", "list", "--q", "repo:alice/* is:open")
161	p = pageOut{}
162	json.Unmarshal(data, &p)
163	if code != 0 || len(p.Items) != 1 || p.Items[0].Repo != "alice/pub" {
164		t.Errorf("mr list --q: exit %d %s", code, msg)
165	}
166	for _, argv := range [][]string{
167		{"issue", "list", "alice/pub", "--query", "all"},
168		{"issue", "list", "--query", "all", "--state", "closed"},
169		{"issue", "list", "--query", "all", "--q", "is:open"},
170		{"issue", "list", "--q", "is:mr"},
171		{"mr", "list", "--q", "assignee:@me"},
172		{"mr", "list", "--q", "nope:x"},
173		{"issue", "list", "--query", "all", "--cursor", "bad"},
174		{"issue", "list", "--query", "all", "--cursor", encodeCursor("issue", "3")},
175	} {
176		if code, _, msg := e.run("alice", argv...); code != protocol.ExitUsage {
177			t.Errorf("%q: exit %d %s, want usage", argv, code, msg)
178		}
179	}
180	if code, _, _ := e.run("alice", "issue", "list", "--query", "nosuch"); code != protocol.ExitNotFound {
181		t.Errorf("unknown saved query: exit %d, want not found", code)
182	}
183
184	// Pinned queries reach the dashboard with their count and first rows.
185	if code, _, msg := e.run("alice", "query", "pin", "all"); code != 0 {
186		t.Fatalf("pin: exit %d %s", code, msg)
187	}
188	code, data, msg = e.run("alice", "dashboard")
189	if code != 0 {
190		t.Fatalf("dashboard: exit %d %s", code, msg)
191	}
192	var d DashboardOut
193	json.Unmarshal(data, &d)
194	if len(d.Queries) != 1 || d.Queries[0].Name != "all" || d.Queries[0].Count != 3 || len(d.Queries[0].Items) != 3 {
195		t.Errorf("dashboard queries = %+v", d.Queries)
196	}
197	if code, _, _ := e.run("alice", "query", "unpin", "all"); code != 0 {
198		t.Fatal("unpin")
199	}
200	_, data, _ = e.run("alice", "dashboard")
201	d = DashboardOut{}
202	json.Unmarshal(data, &d)
203	if d.Queries == nil || len(d.Queries) != 0 {
204		t.Errorf("after unpin, dashboard queries = %#v, want []", d.Queries)
205	}
206
207	code, data, _ = e.run("alice", "query", "list")
208	var list []SavedQueryOut
209	json.Unmarshal(data, &list)
210	if code != 0 || len(list) != 2 || list[0].Name != "all" || list[1].Name != "open" {
211		t.Errorf("query list = %+v", list)
212	}
213	if code, _, _ := e.run("alice", "query", "remove", "all"); code != 0 {
214		t.Fatal("remove")
215	}
216	if code, _, _ := e.run("alice", "query", "show", "all"); code != protocol.ExitNotFound {
217		t.Errorf("show after remove: exit %d", code)
218	}
219	// bob's query of the same name is his own.
220	if code, _, _ := e.run("bob", "query", "show", "all"); code != 0 {
221		t.Errorf("bob lost his query when alice removed hers")
222	}
223}
224
225// An account keeps at most maxSavedQueries, maxPinnedQueries of them
226// pinned; replacing or re-pinning one it has is not a new one.
227func TestQueryCaps(t *testing.T) {
228	e := newQueryEnv(t)
229	for i := 0; i < maxSavedQueries; i++ {
230		if code, _, msg := e.run("alice", "query", "save", fmt.Sprintf("q%d", i), "is:open"); code != 0 {
231			t.Fatalf("save %d: exit %d %s", i, code, msg)
232		}
233	}
234	if code, _, msg := e.run("alice", "query", "save", "one-more", "is:open"); code != protocol.ExitUsage || !strings.Contains(msg, "limit") {
235		t.Fatalf("save past the cap: exit %d %s", code, msg)
236	}
237	if code, _, msg := e.run("alice", "query", "save", "q0", "is:closed", "--force"); code != 0 {
238		t.Fatalf("replace at the cap: exit %d %s", code, msg)
239	}
240	if code, _, _ := e.run("bob", "query", "save", "mine", "is:open"); code != 0 {
241		t.Fatal("the cap is per account")
242	}
243	for i := 0; i < maxPinnedQueries; i++ {
244		if code, _, msg := e.run("alice", "query", "pin", fmt.Sprintf("q%d", i)); code != 0 {
245			t.Fatalf("pin %d: exit %d %s", i, code, msg)
246		}
247	}
248	if code, _, msg := e.run("alice", "query", "pin", "q0"); code != 0 {
249		t.Fatalf("re-pin at the cap: exit %d %s", code, msg)
250	}
251	if code, _, msg := e.run("alice", "query", "pin", fmt.Sprintf("q%d", maxPinnedQueries)); code != protocol.ExitUsage || !strings.Contains(msg, "limit") {
252		t.Fatalf("pin past the cap: exit %d %s", code, msg)
253	}
254	e.run("alice", "query", "unpin", "q0")
255	if code, _, msg := e.run("alice", "query", "pin", fmt.Sprintf("q%d", maxPinnedQueries)); code != 0 {
256		t.Fatalf("pin after an unpin: exit %d %s", code, msg)
257	}
258}