internal/httpd/issuecreate_test.go

9df917e73a67d15adecc3f45976690f6fcd4e47a
gitbay/internal/httpd/issuecreate_test.go history · blame · raw

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}