labels_test.go
⎇
Raw
1package e2e
2
3import (
4 "net/http"
5 "net/url"
6 "strings"
7 "testing"
8
9 "github.com/PuerkitoBio/goquery"
10)
11
12// labelsPatch is a minimal but complete format-patch file.
13func labelsPatch(file string) string {
14 return strings.Join([]string{
15 "From a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 Mon Sep 17 00:00:00 2001",
16 "From: Test User <test@example.com>",
17 "Date: Mon, 01 Jan 2024 12:00:00 +0000",
18 "Subject: [PATCH] Add " + file,
19 "",
20 "---",
21 "diff --git a/" + file + " b/" + file,
22 "new file mode 100644",
23 "index 0000000..9daeafb",
24 "--- /dev/null",
25 "+++ b/" + file,
26 "@@ -0,0 +1 @@",
27 "+x",
28 "",
29 }, "\n")
30}
31
32// labelsCreate posts the label form on the repo settings page.
33func labelsCreate(s *session, repo, name, color string) *response {
34 return s.post("/"+repo+"/settings/labels", url.Values{"name": {name}, "color": {color}})
35}
36
37// labelsSettingsID returns the id of the named label, read from its delete
38// form on the settings page.
39func labelsSettingsID(t *testing.T, s *session, repo, name string) string {
40 t.Helper()
41 id := ""
42 s.get("/" + repo + "/settings").Find(".label-settings-item").Each(func(_ int, sel *goquery.Selection) {
43 if id == "" && strings.TrimSpace(sel.Find(".label-settings-name").Text()) == name {
44 id, _ = sel.Find("input[name=id]").Attr("value")
45 }
46 })
47 if id == "" {
48 t.Fatalf("label %q not found in settings", name)
49 }
50 return id
51}
52
53// labelsFilterID returns the label id of the named filter checkbox on a list
54// page such as /repo/issues.
55func labelsFilterID(t *testing.T, s *session, path, name string) string {
56 t.Helper()
57 id := ""
58 s.get(path).Find(".label-filter-item").Each(func(_ int, sel *goquery.Selection) {
59 if id == "" && strings.Contains(sel.Text(), name) {
60 id, _ = sel.Find("input[name=labels]").Attr("value")
61 }
62 })
63 if id == "" {
64 t.Fatalf("label %q not found in filter popup of %s", name, path)
65 }
66 return id
67}
68
69// labelsItemWith returns the list item whose text contains want.
70func labelsItemWith(t *testing.T, r *response, want string) *goquery.Selection {
71 t.Helper()
72 var found *goquery.Selection
73 r.Find(".issue-item").Each(func(_ int, s *goquery.Selection) {
74 if found == nil && strings.Contains(s.Text(), want) {
75 found = s
76 }
77 })
78 if found == nil {
79 t.Fatalf("no list item containing %q", want)
80 }
81 return found
82}
83
84// labelsTexts returns the trimmed text of every match inside a selection.
85func labelsTexts(sel *goquery.Selection, css string) []string {
86 var out []string
87 sel.Find(css).Each(func(_ int, s *goquery.Selection) {
88 out = append(out, strings.TrimSpace(s.Text()))
89 })
90 return out
91}
92
93func labelsHas(list []string, want string) bool {
94 for _, s := range list {
95 if s == want {
96 return true
97 }
98 }
99 return false
100}
101
102func TestLabels(t *testing.T) {
103 e := newEnv(t)
104 e.register("alice", "password123")
105 admin := e.admin()
106
107 e.createRepo(admin, "label-repo")
108 issuePath := issuesCreate(admin, "label-repo", "Labelled issue", "")
109 patchPath := admin.postMultipart("/label-repo/patches",
110 url.Values{"title": {"Labelled patch"}},
111 file{Field: "patch_file", Name: "label-test.patch", Content: []byte(labelsPatch("label-test.txt"))},
112 ).mustRedirect("/label-repo/patches/")
113
114 t.Run("create label in repo settings", func(t *testing.T) {
115 r := admin.follow(labelsCreate(admin, "label-repo", "bug", "#ff0000"))
116 if !contains(r.Texts(".label-settings-name"), "bug") {
117 t.Error("label not listed")
118 }
119 })
120
121 t.Run("create a second label", func(t *testing.T) {
122 r := admin.follow(labelsCreate(admin, "label-repo", "enhancement", "#00aa00"))
123 names := r.Texts(".label-settings-name")
124 if !labelsHas(names, "bug") || !labelsHas(names, "enhancement") {
125 t.Errorf("labels = %q", names)
126 }
127 })
128
129 t.Run("duplicate label name is rejected", func(t *testing.T) {
130 if !admin.follow(labelsCreate(admin, "label-repo", "bug", "#0000ff")).Has(".form-error") {
131 t.Error("no error message")
132 }
133 })
134
135 t.Run("non-admin cannot create labels", func(t *testing.T) {
136 r := e.anon().post("/label-repo/settings/labels",
137 url.Values{"name": {"nope"}, "color": {"#123456"}})
138 r.mustStatus(http.StatusFound)
139 if !strings.Contains(r.Location(), "/login") {
140 t.Errorf("location = %q", r.Location())
141 }
142 })
143
144 t.Run("assign label to issue", func(t *testing.T) {
145 bug := labelsSettingsID(t, admin, "label-repo", "bug")
146 admin.post(issuePath+"/labels/add", url.Values{"label_id": {bug}}).mustRedirect(issuePath)
147 if !labelsHas(admin.get(issuePath).Texts(".label-badge"), "bug") {
148 t.Error("label badge missing")
149 }
150 })
151
152 t.Run("label appears on issue list", func(t *testing.T) {
153 item := labelsItemWith(t, admin.get("/label-repo/issues"), "Labelled issue")
154 if !labelsHas(labelsTexts(item, ".label-badge"), "bug") {
155 t.Error("label badge missing on list")
156 }
157 })
158
159 t.Run("filter issues by label shows only matching issues", func(t *testing.T) {
160 issuesCreate(admin, "label-repo", "Unlabelled issue", "")
161 id := labelsFilterID(t, admin, "/label-repo/issues", "bug")
162 titles := admin.get("/label-repo/issues?labels=" + id).Texts(".issue-title")
163 if !contains(titles, "Labelled issue") || contains(titles, "Unlabelled issue") {
164 t.Errorf("titles = %q", titles)
165 }
166 })
167
168 t.Run("filter popup is visible without JS", func(t *testing.T) {
169 r := admin.get("/label-repo/issues")
170 if !r.Has("details.label-filter") || !r.Has("details.label-filter summary") {
171 t.Error("filter popup not rendered")
172 }
173 })
174
175 t.Run("remove label from issue", func(t *testing.T) {
176 id := admin.get(issuePath).Attr(".issue-labels-row .label-remove-form input[name=label_id]", "value")
177 admin.post(issuePath+"/labels/remove", url.Values{"label_id": {id}}).mustRedirect(issuePath)
178 if labelsHas(admin.get(issuePath).Texts(".issue-labels-row .label-badge"), "bug") {
179 t.Error("label still attached")
180 }
181 })
182
183 t.Run("assign label to patch", func(t *testing.T) {
184 enh := labelsSettingsID(t, admin, "label-repo", "enhancement")
185 admin.post(patchPath+"/labels/add", url.Values{"label_id": {enh}}).mustRedirect(patchPath)
186 if !labelsHas(admin.get(patchPath).Texts(".label-badge"), "enhancement") {
187 t.Error("label badge missing")
188 }
189 })
190
191 t.Run("label appears on patch list", func(t *testing.T) {
192 item := labelsItemWith(t, admin.get("/label-repo/patches"), "Labelled patch")
193 if !labelsHas(labelsTexts(item, ".label-badge"), "enhancement") {
194 t.Error("label badge missing on list")
195 }
196 })
197
198 t.Run("filter patches by label", func(t *testing.T) {
199 id := labelsFilterID(t, admin, "/label-repo/patches", "enhancement")
200 titles := admin.get("/label-repo/patches?labels=" + id).Texts(".issue-title")
201 if !contains(titles, "Labelled patch") {
202 t.Errorf("titles = %q", titles)
203 }
204 })
205
206 t.Run("remove label from patch", func(t *testing.T) {
207 id := admin.get(patchPath).Attr(".issue-labels-row .label-remove-form input[name=label_id]", "value")
208 admin.post(patchPath+"/labels/remove", url.Values{"label_id": {id}}).mustRedirect(patchPath)
209 if labelsHas(admin.get(patchPath).Texts(".issue-labels-row .label-badge"), "enhancement") {
210 t.Error("label still attached")
211 }
212 })
213
214 t.Run("delete label removes it from settings list", func(t *testing.T) {
215 bug := labelsSettingsID(t, admin, "label-repo", "bug")
216 r := admin.follow(admin.post("/label-repo/settings/labels/delete", url.Values{"id": {bug}}))
217 if labelsHas(r.Texts(".label-settings-name"), "bug") {
218 t.Error("label still listed")
219 }
220 })
221
222 t.Run("deleted label no longer appears in filter popup", func(t *testing.T) {
223 if contains(admin.get("/label-repo/issues").Texts(".label-filter-item"), "bug") {
224 t.Error("deleted label still in filter popup")
225 }
226 })
227}
228
229func TestUserLabelManagement(t *testing.T) {
230 e := newEnv(t)
231 alice := e.register("alice", "password123")
232 admin := e.admin()
233
234 e.createRepo(admin, "ulm-repo")
235 for _, name := range []string{"bug", "feature"} {
236 labelsCreate(admin, "ulm-repo", name, "#808080").mustRedirect("/ulm-repo/settings")
237 }
238 adminIssuePath := issuesCreate(admin, "ulm-repo", "Admin's issue", "")
239 aliceIssuePath := issuesCreate(alice, "ulm-repo", "Alice's issue", "")
240 bugLabelID := labelsFilterID(t, admin, "/ulm-repo/issues", "bug")
241
242 checked := "input[name=allow_user_labels][checked]"
243
244 t.Run("allow_user_labels checkbox is present in repo settings", func(t *testing.T) {
245 if n := admin.get("/ulm-repo/settings").Count("input[name=allow_user_labels]"); n != 1 {
246 t.Errorf("checkbox count = %d", n)
247 }
248 })
249
250 t.Run("allow_user_labels is off by default", func(t *testing.T) {
251 if admin.get("/ulm-repo/settings").Has(checked) {
252 t.Error("checkbox is checked")
253 }
254 })
255
256 t.Run("label checkboxes not shown to non-admin on new issue form when allow_user_labels is off", func(t *testing.T) {
257 if n := alice.get("/ulm-repo/issues/new").Count(".label-checkbox-list"); n != 0 {
258 t.Errorf("label lists = %d", n)
259 }
260 })
261
262 t.Run("label checkboxes not shown to non-admin on new patch form when allow_user_labels is off", func(t *testing.T) {
263 if n := alice.get("/ulm-repo/patches/new").Count(".label-checkbox-list"); n != 0 {
264 t.Errorf("label lists = %d", n)
265 }
266 })
267
268 t.Run("label checkboxes shown to admin on new issue form regardless of setting", func(t *testing.T) {
269 if !admin.get("/ulm-repo/issues/new").Has(".label-checkbox-list") {
270 t.Error("label list missing")
271 }
272 })
273
274 t.Run("admin can enable allow_user_labels", func(t *testing.T) {
275 issuesSaveSettings(admin, "ulm-repo", url.Values{"allow_user_labels": {"1"}}).
276 mustRedirect("/ulm-repo/settings")
277 if !admin.get("/ulm-repo/settings").Has(checked) {
278 t.Error("setting did not persist")
279 }
280 })
281
282 t.Run("label checkboxes shown to non-admin on new issue form when allow_user_labels is on", func(t *testing.T) {
283 r := alice.get("/ulm-repo/issues/new")
284 if !r.Has(".label-checkbox-list") {
285 t.Fatal("label list missing")
286 }
287 labels := r.Texts(".label-checkbox-list .label-badge")
288 if !labelsHas(labels, "bug") || !labelsHas(labels, "feature") {
289 t.Errorf("labels = %q", labels)
290 }
291 })
292
293 t.Run("label checkboxes shown to non-admin on new patch form when allow_user_labels is on", func(t *testing.T) {
294 if !alice.get("/ulm-repo/patches/new").Has(".label-checkbox-list") {
295 t.Error("label list missing")
296 }
297 })
298
299 t.Run("non-admin can create issue with label selected", func(t *testing.T) {
300 p := alice.post("/ulm-repo/issues", url.Values{
301 "title": {"Issue with label"}, "label_ids": {bugLabelID},
302 }).mustRedirect("/ulm-repo/issues/")
303 if !labelsHas(alice.get(p).Texts(".label-badge"), "bug") {
304 t.Error("label not applied")
305 }
306 })
307
308 t.Run("non-admin can create patch with label selected", func(t *testing.T) {
309 featureID := labelsFilterID(t, admin, "/ulm-repo/issues", "feature")
310 p := alice.postMultipart("/ulm-repo/patches",
311 url.Values{"title": {"Patch with label"}, "label_ids": {featureID}},
312 file{Field: "patch_file", Name: "ulm-test.patch", Content: []byte(labelsPatch("ulm-test.txt"))},
313 ).mustRedirect("/ulm-repo/patches/")
314 if !labelsHas(alice.get(p).Texts(".label-badge"), "feature") {
315 t.Error("label not applied")
316 }
317 })
318
319 t.Run("label_ids in POST are ignored for non-admin when allow_user_labels is off (no label applied)", func(t *testing.T) {
320 // Post the settings form without allow_user_labels to switch it off.
321 admin.post("/ulm-repo/settings", url.Values{"description": {""}, "default_branch": {"main"}})
322
323 p := alice.post("/ulm-repo/issues", url.Values{
324 "title": {"Issue sneaking labels"}, "label_ids": {bugLabelID},
325 }).mustRedirect("/ulm-repo/issues/")
326 if labelsHas(alice.get(p).Texts(".label-badge"), "bug") {
327 t.Error("label was applied")
328 }
329
330 admin.post("/ulm-repo/settings", url.Values{
331 "description": {""}, "default_branch": {"main"}, "allow_user_labels": {"1"},
332 })
333 })
334
335 t.Run("non-admin can add label to their own issue", func(t *testing.T) {
336 alice.post(aliceIssuePath+"/labels/add", url.Values{"label_id": {bugLabelID}}).
337 mustRedirect(aliceIssuePath)
338 if !labelsHas(alice.get(aliceIssuePath).Texts(".label-badge"), "bug") {
339 t.Error("label not applied")
340 }
341 })
342
343 t.Run("non-admin can remove label from their own issue", func(t *testing.T) {
344 alice.post(aliceIssuePath+"/labels/remove", url.Values{"label_id": {bugLabelID}}).
345 mustRedirect(aliceIssuePath)
346 if labelsHas(alice.get(aliceIssuePath).Texts(".issue-labels-row .label-badge"), "bug") {
347 t.Error("label still attached")
348 }
349 })
350
351 t.Run("non-admin cannot add label to another user's issue", func(t *testing.T) {
352 alice.post(adminIssuePath+"/labels/add", url.Values{"label_id": {bugLabelID}}).
353 mustStatus(http.StatusForbidden)
354 })
355
356 t.Run("unauthenticated user gets 401 adding a label", func(t *testing.T) {
357 e.anon().post(aliceIssuePath+"/labels/add", url.Values{"label_id": {bugLabelID}}).
358 mustStatus(http.StatusUnauthorized)
359 })
360
361 t.Run("admin can disable allow_user_labels", func(t *testing.T) {
362 issuesSaveSettings(admin, "ulm-repo", url.Values{"allow_user_labels": nil}).
363 mustRedirect("/ulm-repo/settings")
364 if admin.get("/ulm-repo/settings").Has(checked) {
365 t.Error("setting still on")
366 }
367 })
368
369 t.Run("non-admin gets 403 adding label to own issue when allow_user_labels is off", func(t *testing.T) {
370 alice.post(aliceIssuePath+"/labels/add", url.Values{"label_id": {bugLabelID}}).
371 mustStatus(http.StatusForbidden)
372 })
373
374 t.Run("label checkboxes hidden on new issue form after allow_user_labels disabled", func(t *testing.T) {
375 if n := alice.get("/ulm-repo/issues/new").Count(".label-checkbox-list"); n != 0 {
376 t.Errorf("label lists = %d", n)
377 }
378 })
379}
380