e2e/issue_test.go

9df917e73a67d15adecc3f45976690f6fcd4e47a
gitbay/e2e/issue_test.go history · blame · raw

174 lines · 7192 bytes

  1package e2e
  2
  3import (
  4	"encoding/json"
  5	"regexp"
  6	"strings"
  7	"testing"
  8)
  9
 10// normalizeJSON parses JSON and blanks volatile timestamp fields so the
 11// remainder can be compared as a golden value.
 12var tsPat = regexp.MustCompile(`"\d{4}-\d{2}-\d{2}T[0-9:.]+Z?"`)
 13
 14func golden(t *testing.T, raw string) string {
 15	t.Helper()
 16	norm := tsPat.ReplaceAllString(strings.TrimSpace(raw), `"TS"`)
 17	// Re-encode compactly for stable comparison.
 18	var v any
 19	if err := json.Unmarshal([]byte(norm), &v); err != nil {
 20		t.Fatalf("not JSON: %v\n%s", err, raw)
 21	}
 22	out, _ := json.Marshal(v)
 23	return string(out)
 24}
 25
 26func TestIssueLifecycleOverBareSSH(t *testing.T) {
 27	t.Parallel()
 28	inst := startInstance(t)
 29
 30	aliceKey := inst.newKey(t, "alice")
 31	bobKey := inst.newKey(t, "bob")
 32	eveKey := inst.newKey(t, "eve")
 33	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 34	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 35	inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
 36
 37	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/proj"); code != 0 {
 38		t.Fatalf("repo create: %s", errOut)
 39	}
 40
 41	// Create with inline body; numbering starts at 1.
 42	out, errOut, code := inst.ssh(t, aliceKey, "",
 43		"issue", "create", "alice/proj", "--title", "'first bug'", "--body", "'it is broken'", "--json")
 44	if code != 0 {
 45		t.Fatalf("issue create: %s", errOut)
 46	}
 47	if g := golden(t, out); g != `{"data":{"number":1},"protocol_version":1}` {
 48		t.Fatalf("create output: %s", g)
 49	}
 50
 51	// Bob (no grant, public repo) can file an issue too — body over stdin.
 52	_, errOut, code = inst.ssh(t, bobKey, "long body\nfrom stdin\n",
 53		"issue", "create", "alice/proj", "--title", "'from bob'", "--file", "-")
 54	if code != 0 {
 55		t.Fatalf("bob issue create: %s", errOut)
 56	}
 57
 58	// Comment, label, assign, close.
 59	if _, errOut, code = inst.ssh(t, bobKey, "", "issue", "comment", "alice/proj", "1", "--message", "'me too'"); code != 0 {
 60		t.Fatalf("comment: %s", errOut)
 61	}
 62	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "label", "alice/proj", "1", "--add", "bug", "--add", "urgent"); code != 0 {
 63		t.Fatalf("label: %s", errOut)
 64	}
 65	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "assign", "alice/proj", "1", "--add", "bob"); code != 0 {
 66		t.Fatalf("assign: %s", errOut)
 67	}
 68
 69	// Golden check on issue show --json.
 70	out, _, code = inst.ssh(t, aliceKey, "", "issue", "show", "alice/proj", "1", "--json")
 71	if code != 0 {
 72		t.Fatal("issue show failed")
 73	}
 74	// A body reports the markup it was written in; "md" is what a body with no
 75	// --format carries, and what everything written before formats existed has.
 76	wantShow := `{"data":{"assignees":["bob"],"author":"alice","body":"it is broken",` +
 77		`"body_format":"md",` +
 78		`"comments":[{"author":"bob","body":"me too","body_format":"md","created_at":"TS"}],` +
 79		`"created_at":"TS","labels":["bug","urgent"],"number":1,"state":"open",` +
 80		`"title":"first bug"},"protocol_version":1}`
 81	if g := golden(t, out); g != wantShow {
 82		t.Fatalf("issue show golden mismatch:\ngot  %s\nwant %s", g, wantShow)
 83	}
 84
 85	// Permission edges: eve (read-only public) cannot label or close
 86	// someone else's issue; bob can close his own.
 87	_, errOut, code = inst.ssh(t, eveKey, "", "issue", "label", "alice/proj", "1", "--add", "spam")
 88	if code != 4 {
 89		t.Fatalf("eve label: exit %d (want 4), %s", code, errOut)
 90	}
 91	_, errOut, code = inst.ssh(t, eveKey, "", "issue", "close", "alice/proj", "1")
 92	if code != 4 {
 93		t.Fatalf("eve close: exit %d (want 4), %s", code, errOut)
 94	}
 95	if _, errOut, code = inst.ssh(t, bobKey, "", "issue", "close", "alice/proj", "2"); code != 0 {
 96		t.Fatalf("bob closing own issue: %s", errOut)
 97	}
 98
 99	// Close, list filters, reopen.
100	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "close", "alice/proj", "1"); code != 0 {
101		t.Fatalf("close: %s", errOut)
102	}
103	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "list", "alice/proj", "--json")
104	if g := golden(t, out); g != `{"data":[],"protocol_version":1}` { // empty list is [], never null
105		t.Fatalf("open list after close: %s", g)
106	}
107	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "list", "alice/proj", "--state", "closed", "--json")
108	if !strings.Contains(out, `"number":2`) || !strings.Contains(out, `"number":1`) {
109		t.Fatalf("closed list: %s", out)
110	}
111	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "reopen", "alice/proj", "1"); code != 0 {
112		t.Fatalf("reopen: %s", errOut)
113	}
114	// Double-reopen is a usage error.
115	if _, _, code = inst.ssh(t, aliceKey, "", "issue", "reopen", "alice/proj", "1"); code != 2 {
116		t.Fatalf("double reopen: exit %d, want 2", code)
117	}
118
119	// Label removal and assignee removal.
120	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "label", "alice/proj", "1", "--remove", "urgent"); code != 0 {
121		t.Fatalf("label remove: %s", errOut)
122	}
123	if _, errOut, code = inst.ssh(t, aliceKey, "", "issue", "assign", "alice/proj", "1", "--remove", "bob"); code != 0 {
124		t.Fatalf("assign remove: %s", errOut)
125	}
126	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "show", "alice/proj", "1", "--json")
127	if strings.Contains(out, "urgent") || strings.Contains(out, "assignees") {
128		t.Fatalf("removal not reflected: %s", out)
129	}
130
131	// Missing issue and missing repo produce not-found exit codes.
132	if _, _, code = inst.ssh(t, aliceKey, "", "issue", "show", "alice/proj", "99"); code != 3 {
133		t.Fatalf("missing issue: exit %d, want 3", code)
134	}
135	if _, _, code = inst.ssh(t, aliceKey, "", "issue", "list", "alice/nope"); code != 3 {
136		t.Fatalf("missing repo: exit %d, want 3", code)
137	}
138
139	// Web read views: list shows the issue, detail shows the comment, and
140	// bodies render as markdown (goldmark drops raw HTML).
141	status, body := inst.get(t, "/alice/proj/issues")
142	if status != 200 || !strings.Contains(body, "first bug") {
143		t.Fatalf("issues page: %d", status)
144	}
145	status, body = inst.get(t, "/alice/proj/issues/1")
146	if status != 200 || !strings.Contains(body, "me too") || !strings.Contains(body, "bug") {
147		t.Fatalf("issue detail: %d\n%s", status, body)
148	}
149	if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/proj",
150		"--title", "'md body'", "--body", "'has **bold** and <script>x</script>'"); code != 0 {
151		t.Fatal("md issue create failed")
152	}
153	status, body = inst.get(t, "/alice/proj/issues/3")
154	if status != 200 || !strings.Contains(body, "<strong>bold</strong>") {
155		t.Fatalf("markdown body not rendered: %d\n%s", status, body)
156	}
157	if strings.Contains(body, "<script>x</script>") {
158		t.Fatal("raw HTML survived in issue body")
159	}
160
161	// Private repos hide their issues from non-readers, as not-found.
162	if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private"); code != 0 {
163		t.Fatal("create private failed")
164	}
165	if _, _, code = inst.ssh(t, aliceKey, "", "issue", "create", "alice/secret", "--title", "hidden"); code != 0 {
166		t.Fatal("issue in private repo failed")
167	}
168	if _, _, code = inst.ssh(t, eveKey, "", "issue", "show", "alice/secret", "1"); code != 3 {
169		t.Fatalf("eve sees private issue: exit %d, want 3", code)
170	}
171	if status, _ := inst.get(t, "/alice/secret/issues"); status != 404 {
172		t.Fatalf("anonymous issues page on private repo: %d, want 404", status)
173	}
174}