e2e/dashboard_test.go
194 lines · 7867 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func TestDashboard(t *testing.T) {
12 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
13 aliceKey := inst.newKey(t, "alice")
14 bobKey := inst.newKey(t, "bob")
15 inst.admin(t, "admin", "user", "create", "alice",
16 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
17 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
18
19 // Alice's repo with an open issue and an open MR authored by bob.
20 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
21 t.Fatalf("repo create: %s", errOut)
22 }
23 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/app", "bob", "write"); code != 0 {
24 t.Fatal("grant failed")
25 }
26 work := t.TempDir()
27 env := inst.gitEnv(aliceKey)
28 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
29 dir := filepath.Join(work, "w")
30 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
31 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
32 mustGit(t, dir, env, "add", ".")
33 mustGit(t, dir, env, "commit", "-q", "-m", "base")
34 mustGit(t, dir, env, "push", "-q", "origin", "main")
35 mustGit(t, dir, env, "checkout", "-q", "-b", "feat")
36 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\nb\n"), 0o644)
37 mustGit(t, dir, env, "add", ".")
38 mustGit(t, dir, env, "commit", "-q", "-m", "feat")
39 mustGit(t, dir, env, "push", "-q", "origin", "feat")
40 if _, _, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/app",
41 "--source", "feat", "--target", "main", "--title", "'from bob'"); code != 0 {
42 t.Fatal("mr create failed")
43 }
44 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", "'todo one'"); code != 0 {
45 t.Fatal("issue create failed")
46 }
47
48 // Pins: read access required; unpinning something never pinned 404s.
49 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "pin", "alice/app"); code != 0 {
50 t.Fatal("pin failed")
51 }
52 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "unpin", "alice/app"); code != 0 {
53 t.Fatal("unpin failed")
54 }
55 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "unpin", "alice/app"); code != 3 {
56 t.Fatal("double unpin should be not-found")
57 }
58 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "pin", "alice/app"); code != 0 {
59 t.Fatal("re-pin failed")
60 }
61
62 // Anonymous homepage: landing with explore link, repos on /explore.
63 status, body := inst.get(t, "/")
64 if status != 200 || !strings.Contains(body, `href="/explore"`) || !strings.Contains(body, "CLI-first") {
65 t.Fatalf("landing: %d", status)
66 }
67 if strings.Contains(body, "alice/app") {
68 t.Fatal("landing lists repositories")
69 }
70 _, body = inst.get(t, "/explore")
71 if !strings.Contains(body, "alice/app") {
72 t.Fatal("explore missing public repo")
73 }
74
75 // Logged-in homepage: dashboard with pinned repo and open items.
76 out, errOut, code := inst.ssh(t, aliceKey, "", "web", "login", "--json")
77 if code != 0 {
78 t.Fatalf("web login: %s", errOut)
79 }
80 var env2 struct {
81 Data struct {
82 URL string `json:"url"`
83 } `json:"data"`
84 }
85 json.Unmarshal([]byte(out), &env2)
86 loginPath := env2.Data.URL[strings.Index(env2.Data.URL, "/login"):]
87 browser := newBrowser(t)
88 if status, _ := browserGet(t, browser, inst.base()+loginPath); status != 200 {
89 t.Fatalf("login: %d", status)
90 }
91 status, body = browserGet(t, browser, inst.base()+"/")
92 if status != 200 || !strings.Contains(body, ">Dashboard</h1>") {
93 t.Fatalf("dashboard: %d", status)
94 }
95 for _, want := range []string{
96 ">Pinned</h2>", ">app<", // pinned group in the aside
97 "Waiting on your review", "Assigned to you", "Recent activity",
98 "from bob", "alice/app!1", "todo one", "alice/app#1",
99 `href="/alice/app/mrs/1"`, `href="/alice/app/issues/1"`,
100 } {
101 if !strings.Contains(body, want) {
102 t.Errorf("dashboard missing %q", want)
103 }
104 }
105
106 // The diff has its own view rather than a fold at the foot of the
107 // conversation: the default view offers it, and asking for it renders
108 // the stat line and the patch.
109 _, body = inst.get(t, "/alice/app/mrs/1")
110 if !strings.Contains(body, `href="/alice/app/mrs/1?view=diff"`) {
111 t.Fatal("merge request missing the files-changed view")
112 }
113 if strings.Contains(body, `class="difftable"`) {
114 t.Fatal("diff rendered on the conversation view")
115 }
116 _, body = inst.get(t, "/alice/app/mrs/1?view=diff")
117 if !strings.Contains(body, "1 file changed") || !strings.Contains(body, `class="difftable"`) {
118 t.Fatalf("diff view missing stat or patch:\n%s", body)
119 }
120}
121
122// TestDashboardQueues covers the parts of the dashboard that answer "what
123// needs me": the review queue, assigned issues, and the activity feed.
124func TestDashboardQueues(t *testing.T) {
125 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
126 aliceKey := inst.newKey(t, "alice")
127 bobKey := inst.newKey(t, "bob")
128 inst.admin(t, "admin", "user", "create", "alice",
129 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
130 inst.admin(t, "admin", "user", "create", "bob",
131 "--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
132
133 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
134 t.Fatalf("repo create: %s", errOut)
135 }
136 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/app", "bob", "write"); code != 0 {
137 t.Fatalf("grant: %s", errOut)
138 }
139 env := inst.gitEnv(aliceKey)
140 work := t.TempDir()
141 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
142 dir := filepath.Join(work, "w")
143 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
144 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
145 mustGit(t, dir, env, "add", ".")
146 mustGit(t, dir, env, "commit", "-q", "-m", "base")
147 mustGit(t, dir, env, "push", "-q", "origin", "main")
148
149 // Bob opens a merge request: it lands in alice's review queue.
150 bobEnv := inst.gitEnv(bobKey)
151 bobWork := t.TempDir()
152 mustGit(t, bobWork, bobEnv, "clone", inst.sshURL("alice/app"), "w")
153 bobDir := filepath.Join(bobWork, "w")
154 mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "fix", "origin/main")
155 os.WriteFile(filepath.Join(bobDir, "b.txt"), []byte("b\n"), 0o644)
156 mustGit(t, bobDir, bobEnv, "add", ".")
157 mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "the fix")
158 mustGit(t, bobDir, bobEnv, "push", "-q", "origin", "fix")
159 if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/app",
160 "--source", "fix", "--target", "main", "--title", "'needs a look'"); code != 0 {
161 t.Fatalf("mr create: %s", errOut)
162 }
163 // And an issue assigned to alice.
164 if _, errOut, code := inst.ssh(t, bobKey, "", "issue", "create", "alice/app", "--title", "'please handle'"); code != 0 {
165 t.Fatalf("issue create: %s", errOut)
166 }
167 if _, errOut, code := inst.ssh(t, aliceKey, "", "issue", "assign", "alice/app", "1", "--add", "alice"); code != 0 {
168 t.Fatalf("assign: %s", errOut)
169 }
170
171 _, body := browserGet(t, inst.login(t, aliceKey), inst.base()+"/")
172 if !strings.Contains(body, "needs a look") {
173 t.Fatalf("review queue missing the MR:\n%s", body)
174 }
175 if !strings.Contains(body, "please handle") {
176 t.Fatalf("assigned issues missing the issue:\n%s", body)
177 }
178 // The feed reports what happened, phrased and linked.
179 for _, want := range []string{"opened merge request", "opened issue", `href="/alice/app/mrs/1"`} {
180 if !strings.Contains(body, want) {
181 t.Fatalf("feed missing %q:\n%s", want, body)
182 }
183 }
184
185 // Once alice reviews, the merge request leaves her queue.
186 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "review", "alice/app", "1", "--approve"); code != 0 {
187 t.Fatalf("review: %s", errOut)
188 }
189 _, after := browserGet(t, inst.login(t, aliceKey), inst.base()+"/")
190 queue := after[strings.Index(after, "Waiting on your review"):strings.Index(after, "Assigned to you")]
191 if strings.Contains(queue, "needs a look") {
192 t.Fatalf("reviewed MR still waiting:\n%s", queue)
193 }
194}