e2e/diffcomment_test.go
134 lines · 5599 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestDiffComments(t *testing.T) {
13 t.Parallel()
14 inst := startInstance(t)
15 aliceKey := inst.newKey(t, "alice")
16 bobKey := inst.newKey(t, "bob")
17 eveKey := inst.newKey(t, "eve")
18 inst.admin(t, "admin", "user", "create", "alice",
19 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
20 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
21 inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
22
23 // MR with a real multi-line diff.
24 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/lib"); code != 0 {
25 t.Fatalf("repo create: %s", errOut)
26 }
27 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/lib", "bob", "write"); code != 0 {
28 t.Fatal("grant failed")
29 }
30 work := t.TempDir()
31 env := inst.gitEnv(aliceKey)
32 mustGit(t, work, env, "clone", inst.sshURL("alice/lib"), "w")
33 dir := filepath.Join(work, "w")
34 os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nfunc main() {\n}\n"), 0o644)
35 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
36 mustGit(t, dir, env, "add", ".")
37 mustGit(t, dir, env, "commit", "-q", "-m", "base")
38 mustGit(t, dir, env, "push", "-q", "origin", "main")
39 mustGit(t, dir, env, "checkout", "-q", "-b", "feat")
40 os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hi\")\n}\n"), 0o644)
41 mustGit(t, dir, env, "add", ".")
42 mustGit(t, dir, env, "commit", "-q", "-m", "add greeting")
43 mustGit(t, dir, env, "push", "-q", "origin", "feat")
44 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/lib",
45 "--source", "feat", "--target", "main", "--title", "'greeting'"); code != 0 {
46 t.Fatalf("mr create: %s", errOut)
47 }
48
49 // A thread on a real diff line; a path outside the diff is refused.
50 if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "diff-comment", "alice/lib", "1",
51 "--path", "nope.go", "--line", "1", "--message", "'x'"); code != 2 || !strings.Contains(errOut, "not part of") {
52 t.Fatalf("off-diff path: exit %d, %s", code, errOut)
53 }
54 out, errOut, code := inst.ssh(t, bobKey, "", "mr", "diff-comment", "alice/lib", "1",
55 "--path", "main.go", "--line", "6", "--message", "'use log instead of fmt'", "--json")
56 if code != 0 {
57 t.Fatalf("diff-comment: %s", errOut)
58 }
59 var env2 struct {
60 Data struct {
61 Thread int64 `json:"thread"`
62 } `json:"data"`
63 }
64 json.Unmarshal([]byte(out), &env2)
65 thread := env2.Data.Thread
66
67 // Reply joins the thread; replying to a reply is refused.
68 out, _, code = inst.ssh(t, aliceKey, "", "mr", "diff-comment", "alice/lib", "1",
69 "--reply", fmt.Sprint(thread), "--message", "'will do'", "--json")
70 if code != 0 {
71 t.Fatalf("reply failed: %s", out)
72 }
73 var env3 struct {
74 Data struct {
75 ID int64 `json:"id"`
76 } `json:"data"`
77 }
78 json.Unmarshal([]byte(out), &env3)
79 if _, errOut, code = inst.ssh(t, bobKey, "", "mr", "diff-comment", "alice/lib", "1",
80 "--reply", fmt.Sprint(env3.Data.ID), "--message", "'nested'"); code != 1 || !strings.Contains(errOut, "thread root") {
81 t.Fatalf("nested reply: exit %d, %s", code, errOut)
82 }
83
84 // Threads listing shows the thread, both comments, fresh.
85 out, _, _ = inst.ssh(t, aliceKey, "", "mr", "threads", "alice/lib", "1", "--json")
86 if !strings.Contains(out, `"path":"main.go"`) || !strings.Contains(out, `"line":6`) ||
87 !strings.Contains(out, "will do") || strings.Contains(out, `"stale":true`) {
88 t.Fatalf("threads: %s", out)
89 }
90
91 // mr show counts the unresolved thread; the web renders it inline.
92 out, _, _ = inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1", "--json")
93 if !strings.Contains(out, `"unresolved_threads":1`) {
94 t.Fatalf("mr show count: %s", out)
95 }
96 status, body := inst.get(t, "/alice/lib/mrs/1?view=diff")
97 if status != 200 || !strings.Contains(body, "use log instead of fmt") ||
98 !strings.Contains(body, `class="thread`) {
99 t.Fatalf("web thread: %d", status)
100 }
101 // Detached threads belong to the conversation, not the current diff.
102 if _, conv := inst.get(t, "/alice/lib/mrs/1"); strings.Contains(conv, "Threads on earlier revisions") {
103 t.Fatal("fresh thread rendered as detached")
104 }
105
106 // Resolution: eve (read-only outsider) cannot; the thread author can;
107 // count drops; unresolve restores it.
108 if _, _, code = inst.ssh(t, eveKey, "", "mr", "resolve", "alice/lib", "1", fmt.Sprint(thread)); code != 4 {
109 t.Fatal("outsider resolved a thread")
110 }
111 if _, errOut, code = inst.ssh(t, bobKey, "", "mr", "resolve", "alice/lib", "1", fmt.Sprint(thread)); code != 0 {
112 t.Fatalf("resolve: %s", errOut)
113 }
114 out, _, _ = inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1", "--json")
115 if strings.Contains(out, `"unresolved_threads"`) {
116 t.Fatalf("resolved thread still counted: %s", out)
117 }
118 if _, _, code = inst.ssh(t, bobKey, "", "mr", "unresolve", "alice/lib", "1", fmt.Sprint(thread)); code != 0 {
119 t.Fatal("unresolve failed")
120 }
121
122 // Force-push moves the head: the thread goes stale and the web moves it
123 // to the earlier-revisions section.
124 mustGit(t, dir, env, "commit", "-q", "--amend", "-m", "add greeting (amended)")
125 mustGit(t, dir, env, "push", "-q", "--force", "origin", "feat")
126 out, _, _ = inst.ssh(t, aliceKey, "", "mr", "threads", "alice/lib", "1", "--json")
127 if !strings.Contains(out, `"stale":true`) {
128 t.Fatalf("thread not stale after force-push: %s", out)
129 }
130 _, body = inst.get(t, "/alice/lib/mrs/1")
131 if !strings.Contains(body, "Threads on earlier revisions") {
132 t.Fatal("stale thread not moved to detached section")
133 }
134}