internal/control/query_test.go
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}