internal/control/react_test.go
269 lines · 9371 bytes
1package control
2
3import (
4 "bytes"
5 "encoding/json"
6 "testing"
7
8 "gitbay.org/gitbay/internal/protocol"
9 "gitbay.org/gitbay/internal/store"
10)
11
12// reactFixture is alice's repository with issue #1 and one comment on
13// it, and a second user acting on the same store.
14func reactFixture(t *testing.T, visibility string) (alice, bob *Ctx, repo store.Repo, commentID int64) {
15 t.Helper()
16 alice = notifTestCtx(t, "alice")
17 repoID, err := alice.Store.CreateRepo("user", alice.User.ID, "app", visibility)
18 if err != nil {
19 t.Fatal(err)
20 }
21 repo, _ = alice.Store.RepoByID(repoID)
22 if code := runIssueCreate(alice, []string{repo.Path(), "--title", "t", "--body", "b"}); code != 0 {
23 t.Fatalf("create: %d", code)
24 }
25 if code := runIssueComment(alice, []string{repo.Path(), "1", "--message", "hi"}); code != 0 {
26 t.Fatalf("comment: %d", code)
27 }
28 iss, _ := alice.Store.IssueByNumber(repo.ID, 1)
29 cs, _ := alice.Store.ListIssueComments(iss.ID)
30 commentID = cs[0].ID
31 bobID, err := alice.Store.CreateUser("bob", false)
32 if err != nil {
33 t.Fatal(err)
34 }
35 b := *alice
36 b.User = store.User{ID: bobID, Username: "bob"}
37 b.Stdout, b.Stderr = &bytes.Buffer{}, &bytes.Buffer{}
38 return alice, &b, repo, commentID
39}
40
41func showReactIssue(t *testing.T, c *Ctx, repo store.Repo) IssueShow {
42 t.Helper()
43 var out bytes.Buffer
44 c.Stdout, c.JSON = &out, true
45 defer func() { c.JSON = false }()
46 if code := runIssueShow(c, []string{repo.Path(), "1"}); code != 0 {
47 t.Fatalf("show: %d", code)
48 }
49 var env struct{ Data IssueShow }
50 if err := json.Unmarshal(out.Bytes(), &env); err != nil {
51 t.Fatal(err)
52 }
53 return env.Data
54}
55
56func TestIssueReact(t *testing.T) {
57 alice, bob, repo, cid := reactFixture(t, "public")
58 p := repo.Path()
59 cmt := itoa(cid)
60
61 for i := 0; i < 2; i++ { // twice is a no-op
62 if code := runReact(bob, []string{p, "1", "+1"}, "issue"); code != 0 {
63 t.Fatalf("react: %d", code)
64 }
65 }
66 if code := runReact(alice, []string{p, "1", "👍"}, "issue"); code != 0 { // the emoji itself
67 t.Fatalf("react emoji: %d", code)
68 }
69 if code := runReact(bob, []string{p, "1", "--comment", cmt, "hooray"}, "issue"); code != 0 {
70 t.Fatalf("react comment: %d", code)
71 }
72
73 d := showReactIssue(t, bob, repo)
74 if len(d.Reactions) != 1 || d.Reactions[0] != (ReactionOut{"+1", 2, true}) {
75 t.Errorf("issue reactions as bob: %+v", d.Reactions)
76 }
77 if len(d.Comments) != 1 || d.Comments[0].ID != cid ||
78 len(d.Comments[0].Reactions) != 1 || d.Comments[0].Reactions[0] != (ReactionOut{"hooray", 1, true}) {
79 t.Errorf("comment reactions: %+v", d.Comments)
80 }
81 if d := showReactIssue(t, alice, repo); d.Comments[0].Reactions[0].Me || !d.Reactions[0].Me {
82 t.Errorf("me flags as alice: %+v", d)
83 }
84
85 for i := 0; i < 2; i++ { // removing twice is a no-op
86 if code := runReact(bob, []string{p, "1", "--remove", "+1"}, "issue"); code != 0 {
87 t.Fatalf("remove: %d", code)
88 }
89 }
90 if d := showReactIssue(t, alice, repo); len(d.Reactions) != 1 || d.Reactions[0].Count != 1 {
91 t.Errorf("after remove: %+v", d.Reactions)
92 }
93 if code := runReact(bob, []string{p, "1", "--remove", "rocket"}, "issue"); code != 0 {
94 t.Errorf("removing an absent reaction: %d", code)
95 }
96
97 // Reacting files nothing.
98 var n int
99 alice.Store.DB.QueryRow("SELECT COUNT(*) FROM events WHERE kind LIKE '%react%'").Scan(&n)
100 if n != 0 {
101 t.Errorf("%d events for reactions", n)
102 }
103 rows, _ := alice.Store.Inbox(alice.User.ID, false, 20, 0)
104 if len(rows) != 0 {
105 t.Errorf("alice notified: %+v", rows)
106 }
107}
108
109func TestIssueReactRefusals(t *testing.T) {
110 _, bob, repo, cid := reactFixture(t, "public")
111 p := repo.Path()
112 for name, tc := range map[string]struct {
113 args []string
114 want int
115 }{
116 "unknown reaction": {[]string{p, "1", "thumbsup"}, protocol.ExitUsage},
117 "missing reaction": {[]string{p, "1"}, protocol.ExitUsage},
118 "unknown comment": {[]string{p, "1", "--comment", "999", "+1"}, protocol.ExitNotFound},
119 "bad comment id": {[]string{p, "1", "--comment", "x", "+1"}, protocol.ExitUsage},
120 "unknown issue": {[]string{p, "9", "+1"}, protocol.ExitNotFound},
121 "unknown repository": {[]string{"alice/nope", "1", "+1"}, protocol.ExitNotFound},
122 } {
123 if code := runReact(bob, tc.args, "issue"); code != tc.want {
124 t.Errorf("%s: exit %d, want %d", name, code, tc.want)
125 }
126 }
127 _ = cid
128}
129
130func TestIssueReactPrivateIsNotFound(t *testing.T) {
131 _, bob, repo, cid := reactFixture(t, "private")
132 for _, args := range [][]string{
133 {repo.Path(), "1", "+1"},
134 {repo.Path(), "1", "--comment", itoa(cid), "+1"},
135 } {
136 if code := runReact(bob, args, "issue"); code != protocol.ExitNotFound {
137 t.Errorf("%v: exit %d, want %d", args, code, protocol.ExitNotFound)
138 }
139 }
140 if code := runIssueShow(bob, []string{repo.Path(), "1"}); code != protocol.ExitNotFound {
141 t.Errorf("show: exit %d", code)
142 }
143}
144
145func TestReactRefusedOnArchived(t *testing.T) {
146 alice, bob, repo, _ := reactFixture(t, "public")
147 if _, err := alice.Store.UpdateRepoSettings(repo.ID, func(s *store.RepoSettings) { s.Archived = true }); err != nil {
148 t.Fatal(err)
149 }
150 if code := runReact(bob, []string{repo.Path(), "1", "+1"}, "issue"); code != protocol.ExitDenied {
151 t.Errorf("exit %d, want %d", code, protocol.ExitDenied)
152 }
153}
154
155func TestReactOnSystemCommentRefused(t *testing.T) {
156 alice, _, repo, _ := reactFixture(t, "public")
157 iss, _ := alice.Store.IssueByNumber(repo.ID, 1)
158 alice.Store.AddIssueSystemComment(iss.ID, alice.User.ID, "closed")
159 cs, _ := alice.Store.ListIssueComments(iss.ID)
160 if code := runReact(alice, []string{repo.Path(), "1", "--comment", itoa(cs[1].ID), "+1"}, "issue"); code != protocol.ExitNotFound {
161 t.Errorf("exit %d", code)
162 }
163}
164
165func itoa(n int64) string { b, _ := json.Marshal(n); return string(b) }
166
167func TestMRReact(t *testing.T) {
168 alice, bob, repo, _ := reactFixture(t, "public")
169 if _, err := alice.Store.CreateMR(repo.ID, alice.User.ID, repo.ID, "f", "main", "t", "", "abc", "md", false); err != nil {
170 t.Fatal(err)
171 }
172 mr, _ := alice.Store.MRByNumber(repo.ID, 1)
173 if err := alice.Store.AddMRComment(mr.ID, alice.User.ID, "hi", "md"); err != nil {
174 t.Fatal(err)
175 }
176 cs, _ := alice.Store.ListMRComments(mr.ID)
177 p := repo.Path()
178 for _, args := range [][]string{{p, "1", "rocket"}, {p, "1", "--comment", itoa(cs[0].ID), "eyes"}} {
179 if code := runReact(bob, args, "mr"); code != 0 {
180 t.Fatalf("%v: %d", args, code)
181 }
182 }
183 if code := runReact(bob, []string{p, "1", "--comment", "999", "eyes"}, "mr"); code != protocol.ExitNotFound {
184 t.Errorf("unknown comment: %d", code)
185 }
186 got, _ := alice.Store.ReactionCounts("mr", mr.ID, bob.User.ID)
187 if len(got[0]) != 1 || got[0][0].Reaction != "rocket" || !got[0][0].Me || got[cs[0].ID][0].Reaction != "eyes" {
188 t.Errorf("counts: %+v", got)
189 }
190}
191
192// A comment id that belongs to another thread, or to a repository the
193// caller cannot read, is not found under a readable repository and
194// issue or merge request, and nothing is stored.
195func TestReactCommentFromElsewhereNotFound(t *testing.T) {
196 alice, bob, repo, _ := reactFixture(t, "public")
197 st := alice.Store
198
199 // Another issue and MR in the same repository, each with a comment.
200 if code := runIssueCreate(alice, []string{repo.Path(), "--title", "two"}); code != 0 {
201 t.Fatal(code)
202 }
203 if code := runIssueComment(alice, []string{repo.Path(), "2", "--message", "other"}); code != 0 {
204 t.Fatal(code)
205 }
206 iss2, _ := st.IssueByNumber(repo.ID, 2)
207 ic2, _ := st.ListIssueComments(iss2.ID)
208 for i := 0; i < 2; i++ {
209 if _, err := st.CreateMR(repo.ID, alice.User.ID, repo.ID, "f", "main", "t", "", "abc", "md", false); err != nil {
210 t.Fatal(err)
211 }
212 }
213 mr1, _ := st.MRByNumber(repo.ID, 1)
214 mr2, _ := st.MRByNumber(repo.ID, 2)
215 st.AddMRComment(mr1.ID, alice.User.ID, "one", "md")
216 st.AddMRComment(mr2.ID, alice.User.ID, "two", "md")
217 mc1, _ := st.ListMRComments(mr1.ID)
218 mc2, _ := st.ListMRComments(mr2.ID)
219
220 // Another user's private repository, with an issue comment and an MR comment.
221 carolID, _ := st.CreateUser("carol", false)
222 pid, err := st.CreateRepo("user", carolID, "secret", "private")
223 if err != nil {
224 t.Fatal(err)
225 }
226 priv, _ := st.RepoByID(pid)
227 carol := *alice
228 carol.User = store.User{ID: carolID, Username: "carol"}
229 carol.Stdout, carol.Stderr = &bytes.Buffer{}, &bytes.Buffer{}
230 if code := runIssueCreate(&carol, []string{priv.Path(), "--title", "s"}); code != 0 {
231 t.Fatal(code)
232 }
233 if code := runIssueComment(&carol, []string{priv.Path(), "1", "--message", "s"}); code != 0 {
234 t.Fatal(code)
235 }
236 pi, _ := st.IssueByNumber(priv.ID, 1)
237 pic, _ := st.ListIssueComments(pi.ID)
238 if _, err := st.CreateMR(priv.ID, carolID, priv.ID, "f", "main", "t", "", "abc", "md", false); err != nil {
239 t.Fatal(err)
240 }
241 pm, _ := st.MRByNumber(priv.ID, 1)
242 st.AddMRComment(pm.ID, carolID, "s", "md")
243 pmc, _ := st.ListMRComments(pm.ID)
244
245 p := repo.Path()
246 cases := []struct {
247 noun string
248 n string
249 id int64
250 }{
251 {"issue", "1", ic2[0].ID},
252 {"issue", "1", pic[0].ID},
253 {"mr", "1", mc2[0].ID},
254 {"mr", "1", pmc[0].ID},
255 }
256 _ = mc1
257 for _, tc := range cases {
258 if code := runReact(bob, []string{p, tc.n, "--comment", itoa(tc.id), "+1"}, tc.noun); code != protocol.ExitNotFound {
259 t.Errorf("%s comment %d: exit %d, want %d", tc.noun, tc.id, code, protocol.ExitNotFound)
260 }
261 }
262 for _, table := range []string{"issue_reactions", "mr_reactions"} {
263 var n int
264 st.DB.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n)
265 if n != 0 {
266 t.Errorf("%s holds %d rows", table, n)
267 }
268 }
269}