e2e/reactions_test.go
156 lines · 6252 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "net/url"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12// Reactions on issues, merge requests and their comments (#291): the
13// commands, the counts in show, the web buttons, and a private
14// repository staying invisible.
15func TestReactions(t *testing.T) {
16 t.Parallel()
17 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
18 aliceKey := inst.newKey(t, "alice")
19 bobKey := inst.newKey(t, "bob")
20 carolKey := inst.newKey(t, "carol")
21 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
22 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
23 inst.admin(t, "admin", "user", "create", "carol", "--key", carolKey+".pub")
24 for _, args := range [][]string{{"repo", "create", "alice/app"}, {"repo", "create", "alice/secret", "--private"}} {
25 if _, errOut, code := inst.ssh(t, aliceKey, "", args...); code != 0 {
26 t.Fatalf("%v: %s", args, errOut)
27 }
28 }
29 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/secret", "bob", "read"); code != 0 {
30 t.Fatal("grant failed")
31 }
32 env := inst.gitEnv(aliceKey)
33 work := t.TempDir()
34 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
35 dir := filepath.Join(work, "w")
36 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
37 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
38 mustGit(t, dir, env, "add", ".")
39 mustGit(t, dir, env, "commit", "-q", "-m", "base")
40 mustGit(t, dir, env, "push", "-q", "origin", "main")
41 mustGit(t, dir, env, "checkout", "-q", "-b", "feat")
42 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("b\n"), 0o644)
43 mustGit(t, dir, env, "commit", "-q", "-am", "change")
44 mustGit(t, dir, env, "push", "-q", "origin", "feat")
45
46 must := func(key string, args ...string) string {
47 t.Helper()
48 out, errOut, code := inst.ssh(t, key, "", args...)
49 if code != 0 {
50 t.Fatalf("%v: exit %d: %s%s", args, code, out, errOut)
51 }
52 return out
53 }
54 must(aliceKey, "issue", "create", "alice/app", "--title", "bug", "--body", "'it breaks'")
55 must(aliceKey, "issue", "comment", "alice/app", "1", "--message", "first")
56 must(aliceKey, "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "'change'")
57 must(aliceKey, "mr", "comment", "alice/app", "1", "--message", "'looks fine'")
58 must(aliceKey, "issue", "create", "alice/secret", "--title", "hidden")
59
60 type show struct {
61 Data struct {
62 Reactions []struct {
63 Reaction string
64 Count int
65 Me bool
66 }
67 Comments []struct {
68 ID int64
69 Reactions []struct {
70 Reaction string
71 Count int
72 Me bool
73 }
74 }
75 }
76 }
77 read := func(key, noun string) show {
78 t.Helper()
79 var s show
80 if err := json.Unmarshal([]byte(must(key, noun, "show", "alice/app", "1", "--json")), &s); err != nil {
81 t.Fatal(err)
82 }
83 return s
84 }
85
86 for _, noun := range []string{"issue", "mr"} {
87 cid := read(bobKey, noun).Data.Comments[0].ID
88 if cid == 0 {
89 t.Fatalf("%s show carries no comment id", noun)
90 }
91 must(bobKey, noun, "react", "alice/app", "1", "+1")
92 must(bobKey, noun, "react", "alice/app", "1", "+1") // idempotent
93 must(aliceKey, noun, "react", "alice/app", "1", "👍")
94 must(bobKey, noun, "react", "alice/app", "1", "--comment", itoa64(cid), "-1")
95 must(bobKey, noun, "react", "alice/app", "1", "--remove", "rocket") // absent: no-op
96
97 s := read(bobKey, noun)
98 if len(s.Data.Reactions) != 1 || s.Data.Reactions[0].Reaction != "+1" || s.Data.Reactions[0].Count != 2 || !s.Data.Reactions[0].Me {
99 t.Errorf("%s body reactions: %+v", noun, s.Data.Reactions)
100 }
101 if c := s.Data.Comments[0].Reactions; len(c) != 1 || c[0].Reaction != "-1" || c[0].Count != 1 || !c[0].Me {
102 t.Errorf("%s comment reactions: %+v", noun, c)
103 }
104 if a := read(aliceKey, noun); a.Data.Comments[0].Reactions[0].Me {
105 t.Errorf("%s: alice marked on bob's reaction", noun)
106 }
107 if out := must(bobKey, noun, "show", "alice/app", "1"); !strings.Contains(out, "👍 2 (you)") || !strings.Contains(out, "(comment ") {
108 t.Errorf("%s show text:\n%s", noun, out)
109 }
110 if _, _, code := inst.ssh(t, bobKey, "", noun, "react", "alice/app", "1", "nope"); code != 2 {
111 t.Errorf("%s: unknown reaction exited %d, want 2", noun, code)
112 }
113 if _, _, code := inst.ssh(t, bobKey, "", noun, "react", "alice/app", "1", "--comment", "9999", "+1"); code != 3 {
114 t.Errorf("%s: unknown comment exited %d, want 3", noun, code)
115 }
116 }
117
118 // Private: readable by bob (granted), not found for carol.
119 must(bobKey, "issue", "react", "alice/secret", "1", "eyes")
120 if _, _, code := inst.ssh(t, carolKey, "", "issue", "react", "alice/secret", "1", "eyes"); code != 3 {
121 t.Errorf("private repository: exit %d, want 3", code)
122 }
123
124 // Reacting files no activity.
125 if out := must(aliceKey, "feed", "--json"); strings.Contains(out, "react") {
126 t.Errorf("a reaction reached the feed:\n%s", out)
127 }
128
129 // Web: buttons for a signed-in viewer, counts only for anyone else.
130 bob := inst.login(t, bobKey)
131 if status, page := browserGet(t, bob, inst.base()+"/alice/app/issues/1"); status != 200 ||
132 !strings.Contains(page, `class="react mine"`) || !strings.Contains(page, `name="add" value="eyes"`) {
133 t.Errorf("signed-in issue page: %d\n%s", status, page)
134 }
135 if status, page := browserGet(t, bob, inst.base()+"/alice/app/mrs/1"); status != 200 || !strings.Contains(page, `class="react mine"`) {
136 t.Errorf("signed-in MR page: %d", status)
137 }
138 if status, _ := browserPost(t, bob, inst.base()+"/alice/app/issues/1/react", url.Values{"add": {"rocket"}}); status != 200 {
139 t.Fatalf("web react: %d", status)
140 }
141 if status, _ := browserPost(t, bob, inst.base()+"/alice/app/mrs/1/react", url.Values{"remove": {"+1"}}); status != 200 {
142 t.Fatalf("web unreact: %d", status)
143 }
144 if s := read(bobKey, "issue"); len(s.Data.Reactions) != 2 {
145 t.Errorf("web reaction not stored: %+v", s.Data.Reactions)
146 }
147 if s := read(bobKey, "mr"); s.Data.Reactions[0].Count != 1 || s.Data.Reactions[0].Me {
148 t.Errorf("web removal: %+v", s.Data.Reactions)
149 }
150 _, anon := inst.get(t, "/alice/app/issues/1")
151 if strings.Contains(anon, "/issues/1/react") || !strings.Contains(anon, "👍 2") {
152 t.Errorf("signed-out issue page:\n%s", anon)
153 }
154}
155
156func itoa64(n int64) string { b, _ := json.Marshal(n); return string(b) }