e2e/reactions_test.go

v1.40.0
gitbay/e2e/reactions_test.go history · blame · raw

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