internal/control/react_test.go

v1.42.0
gitbay/internal/control/react_test.go history · blame · raw

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}