internal/httpd/issuecreate_test.go
333 lines · 9816 bytes
1package httpd
2
3import (
4 "html/template"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "testing"
10 "time"
11
12 "gitbay.org/gitbay/internal/config"
13 "gitbay.org/gitbay/internal/store"
14 "gitbay.org/gitbay/internal/web"
15)
16
17// A heading precedes the issue's comment thread, matching the merge
18// request page, so a screen-reader user skimming by heading has a
19// landmark before the first comment rather than falling straight from
20// the edit box into the body (#271).
21func TestIssuePageHasDiscussionHeading(t *testing.T) {
22 var sb strings.Builder
23 if err := web.Render(&sb, "issue.html", struct {
24 repoPage
25 Issue store.Issue
26 BodyHTML template.HTML
27 Comments []renderedComment
28 CanEdit bool
29 CanWrite bool
30 Milestones []store.Milestone
31 Notice string
32 LabelColors map[string]template.CSS
33 Draft *draft
34 }{repoPage: testRepoPage(), Issue: store.Issue{Number: 1, Title: "bug", Author: "cmc", State: "open"}}); err != nil {
35 t.Fatalf("render: %v", err)
36 }
37 if !strings.Contains(sb.String(), "<h2>Discussion</h2>") {
38 t.Error("no Discussion heading")
39 }
40}
41
42// sessionCookieFor gives uid a real web session, the way canWriteRepo's
43// call to s.viewer(r) needs (internal/httpd/accounts.go:37-47), since
44// issueCreateForm gates the milestone/assignee fields on it rather than
45// on the handler's own user parameter.
46func sessionCookieFor(t *testing.T, s *Server, st *store.Store, uid int64) *http.Cookie {
47 t.Helper()
48 tok, hash, err := store.NewToken()
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := st.CreateWebSession(hash, uid, time.Hour); err != nil {
53 t.Fatal(err)
54 }
55 return s.sessionCookieFor(tok)
56}
57
58// The new-issue form takes milestone and assignee, resolved on the same
59// issue create dispatch as the title and labels, so a typo in either
60// creates nothing and the label/milestone/assign code paths still run
61// notifications and events (#271).
62func TestIssueCreateFormHasMilestoneAndAssigneeForWriter(t *testing.T) {
63 st, err := store.Open(":memory:")
64 if err != nil {
65 t.Fatal(err)
66 }
67 defer st.Close()
68 if err := st.MigrateUp(); err != nil {
69 t.Fatal(err)
70 }
71 uid, err := st.CreateUser("alice", false)
72 if err != nil {
73 t.Fatal(err)
74 }
75 u := store.User{ID: uid, Username: "alice"}
76 if _, err := st.CreateRepo("user", uid, "app", "public"); err != nil {
77 t.Fatal(err)
78 }
79
80 cfg := config.Default()
81 cfg.Web.Mode = "accounts"
82 s := New(cfg, st)
83 req := httptest.NewRequest("GET", "/alice/app/issues/new", nil)
84 req.SetPathValue("owner", "alice")
85 req.SetPathValue("repo", "app")
86 req.AddCookie(sessionCookieFor(t, s, st, uid))
87 rr := httptest.NewRecorder()
88 s.issueCreateForm(rr, req, u)
89
90 body := rr.Body.String()
91 if !strings.Contains(body, `name="labels"`) {
92 t.Error("no labels field for a writer")
93 }
94 if !strings.Contains(body, `name="milestone"`) {
95 t.Error("no milestone field for a writer")
96 }
97 if !strings.Contains(body, `name="assignee"`) {
98 t.Error("no assignee field for a writer")
99 }
100}
101
102// A reader (no write access) sees no milestone/assignee fields, and can
103// still create an issue with title and body alone.
104func TestIssueCreateFormHidesMilestoneAndAssigneeForReader(t *testing.T) {
105 st, err := store.Open(":memory:")
106 if err != nil {
107 t.Fatal(err)
108 }
109 defer st.Close()
110 if err := st.MigrateUp(); err != nil {
111 t.Fatal(err)
112 }
113 ownerID, err := st.CreateUser("alice", false)
114 if err != nil {
115 t.Fatal(err)
116 }
117 readerID, err := st.CreateUser("bob", false)
118 if err != nil {
119 t.Fatal(err)
120 }
121 reader := store.User{ID: readerID, Username: "bob"}
122 if _, err := st.CreateRepo("user", ownerID, "app", "public"); err != nil {
123 t.Fatal(err)
124 }
125
126 cfg := config.Default()
127 cfg.Web.Mode = "accounts"
128 s := New(cfg, st)
129 req := httptest.NewRequest("GET", "/alice/app/issues/new", nil)
130 req.SetPathValue("owner", "alice")
131 req.SetPathValue("repo", "app")
132 req.AddCookie(sessionCookieFor(t, s, st, readerID))
133 rr := httptest.NewRecorder()
134 s.issueCreateForm(rr, req, reader)
135
136 body := rr.Body.String()
137 if strings.Contains(body, `name="labels"`) {
138 t.Error("reader should not see a labels field")
139 }
140 if strings.Contains(body, `name="milestone"`) {
141 t.Error("reader should not see a milestone field")
142 }
143 if strings.Contains(body, `name="assignee"`) {
144 t.Error("reader should not see an assignee field")
145 }
146
147 form := url.Values{"title": {"a bug"}, "body": {"steps"}}
148 submit := httptest.NewRequest("POST", "/alice/app/issues/new", strings.NewReader(form.Encode()))
149 submit.Header.Set("Content-Type", "application/x-www-form-urlencoded")
150 submit.SetPathValue("owner", "alice")
151 submit.SetPathValue("repo", "app")
152 rr2 := httptest.NewRecorder()
153 s.issueCreateSubmit(rr2, submit, reader)
154 if rr2.Code != http.StatusSeeOther {
155 t.Fatalf("reader create: status %d, body %q", rr2.Code, rr2.Body.String())
156 }
157
158 repo, err := st.RepoByPath("alice/app")
159 if err != nil {
160 t.Fatal(err)
161 }
162 issue, err := st.IssueByNumber(repo.ID, 1)
163 if err != nil {
164 t.Fatalf("issue not created: %v", err)
165 }
166 if issue.Title != "a bug" {
167 t.Fatalf("got title %q", issue.Title)
168 }
169}
170
171// A reader's hand-crafted POST carrying a labels value still creates a
172// plain issue: issue create requires write access for --label, so
173// issueCreateSubmit drops labels/milestone/assignee from the argv for a
174// non-writer rather than sending them and failing the whole create.
175func TestIssueCreateSubmitReaderLabelIsDropped(t *testing.T) {
176 st, err := store.Open(":memory:")
177 if err != nil {
178 t.Fatal(err)
179 }
180 defer st.Close()
181 if err := st.MigrateUp(); err != nil {
182 t.Fatal(err)
183 }
184 ownerID, err := st.CreateUser("alice", false)
185 if err != nil {
186 t.Fatal(err)
187 }
188 readerID, err := st.CreateUser("bob", false)
189 if err != nil {
190 t.Fatal(err)
191 }
192 reader := store.User{ID: readerID, Username: "bob"}
193 if _, err := st.CreateRepo("user", ownerID, "app", "public"); err != nil {
194 t.Fatal(err)
195 }
196 repo, err := st.RepoByPath("alice/app")
197 if err != nil {
198 t.Fatal(err)
199 }
200
201 s := New(config.Default(), st)
202 form := url.Values{
203 "title": {"a bug"},
204 "body": {"steps"},
205 "labels": {"bug"},
206 }
207 req := httptest.NewRequest("POST", "/alice/app/issues/new", strings.NewReader(form.Encode()))
208 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
209 req.SetPathValue("owner", "alice")
210 req.SetPathValue("repo", "app")
211 rr := httptest.NewRecorder()
212 s.issueCreateSubmit(rr, req, reader)
213 if rr.Code != http.StatusSeeOther {
214 t.Fatalf("reader create: status %d, body %q", rr.Code, rr.Body.String())
215 }
216
217 issue, err := st.IssueByNumber(repo.ID, 1)
218 if err != nil {
219 t.Fatalf("issue not created: %v", err)
220 }
221 if len(issue.Labels) != 0 {
222 t.Errorf("labels = %v, want none", issue.Labels)
223 }
224}
225
226// A writer creates an issue with a milestone and an assignee in one
227// request; both land on the issue because they go through the same
228// dispatch as the create.
229func TestIssueCreateSubmitSetsMilestoneAndAssignee(t *testing.T) {
230 st, err := store.Open(":memory:")
231 if err != nil {
232 t.Fatal(err)
233 }
234 defer st.Close()
235 if err := st.MigrateUp(); err != nil {
236 t.Fatal(err)
237 }
238 uid, err := st.CreateUser("alice", false)
239 if err != nil {
240 t.Fatal(err)
241 }
242 u := store.User{ID: uid, Username: "alice"}
243 if _, err := st.CreateUser("bob", false); err != nil {
244 t.Fatal(err)
245 }
246 if _, err := st.CreateRepo("user", uid, "app", "public"); err != nil {
247 t.Fatal(err)
248 }
249 repo, err := st.RepoByPath("alice/app")
250 if err != nil {
251 t.Fatal(err)
252 }
253 if _, err := st.CreateMilestone(repo, "v1", "", ""); err != nil {
254 t.Fatal(err)
255 }
256
257 s := New(config.Default(), st)
258 form := url.Values{
259 "title": {"needs a fix"},
260 "body": {"details"},
261 "milestone": {"v1"},
262 "assignee": {"bob"},
263 }
264 req := httptest.NewRequest("POST", "/alice/app/issues/new", strings.NewReader(form.Encode()))
265 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
266 req.SetPathValue("owner", "alice")
267 req.SetPathValue("repo", "app")
268 rr := httptest.NewRecorder()
269 s.issueCreateSubmit(rr, req, u)
270 if rr.Code != http.StatusSeeOther {
271 t.Fatalf("status %d, body %q", rr.Code, rr.Body.String())
272 }
273
274 issue, err := st.IssueByNumber(repo.ID, 1)
275 if err != nil {
276 t.Fatalf("issue not created: %v", err)
277 }
278 if issue.Milestone != "v1" {
279 t.Errorf("milestone = %q, want v1", issue.Milestone)
280 }
281 if len(issue.Assignees) != 1 || issue.Assignees[0] != "bob" {
282 t.Errorf("assignees = %v, want [bob]", issue.Assignees)
283 }
284}
285
286// A bad assignee creates nothing: issue create resolves the assignee
287// before writing the issue, so a typo leaves the repo without a
288// half-created issue (#271).
289func TestIssueCreateSubmitBadAssigneeCreatesNothing(t *testing.T) {
290 st, err := store.Open(":memory:")
291 if err != nil {
292 t.Fatal(err)
293 }
294 defer st.Close()
295 if err := st.MigrateUp(); err != nil {
296 t.Fatal(err)
297 }
298 uid, err := st.CreateUser("alice", false)
299 if err != nil {
300 t.Fatal(err)
301 }
302 u := store.User{ID: uid, Username: "alice"}
303 if _, err := st.CreateRepo("user", uid, "app", "public"); err != nil {
304 t.Fatal(err)
305 }
306 repo, err := st.RepoByPath("alice/app")
307 if err != nil {
308 t.Fatal(err)
309 }
310
311 s := New(config.Default(), st)
312 form := url.Values{
313 "title": {"needs a fix"},
314 "body": {"details"},
315 "assignee": {"nobody-such-user"},
316 }
317 req := httptest.NewRequest("POST", "/alice/app/issues/new", strings.NewReader(form.Encode()))
318 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
319 req.SetPathValue("owner", "alice")
320 req.SetPathValue("repo", "app")
321 rr := httptest.NewRecorder()
322 s.issueCreateSubmit(rr, req, u)
323 if rr.Code == http.StatusSeeOther {
324 t.Fatalf("expected a failure status, got redirect")
325 }
326 if !strings.Contains(rr.Body.String(), "nobody-such-user") {
327 t.Errorf("error body %q does not name the bad assignee", rr.Body.String())
328 }
329
330 if _, err := st.IssueByNumber(repo.ID, 1); err == nil {
331 t.Fatal("issue was created despite the bad assignee")
332 }
333}