| @@ -0,0 +1,156 @@ |
| |
1 | package e2e |
| |
2 | |
| |
3 | import ( |
| |
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. |
| |
15 | func 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 | |
| |
156 | func itoa64(n int64) string { b, _ := json.Marshal(n); return string(b) } |