| @@ -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) } |