vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
c5902451
Keenan Nemetz
16d ago
|
1 | package metadata |
|
37c01e5a
Keenan Nemetz
17d ago
|
2 | |
| 3 | import ( | |
| 4 | "strings" | |
| 5 | "testing" | |
| 6 | "time" | |
|
67dd8854
Keenan Nemetz
17d ago
|
7 | |
|
d37d301f
Keenan Nemetz
16h ago
|
8 | "rodent.software/vole/forge/identity" |
|
37c01e5a
Keenan Nemetz
17d ago
|
9 | ) |
| 10 | ||
| 11 | const ( | |
| 12 | idA = "01M5JR2JK29KKMAA1T9MDNTPBP" | |
| 13 | idB = "01M5JR2JK29KKMAA1T9MDNTPBQ" | |
| 14 | idC = "01M5JR2JK29KKMAA1T9MDNTPBR" | |
| 15 | ) | |
| 16 | ||
| 17 | func validContents() map[string]string { | |
| 18 | return map[string]string{ | |
|
307cbc2a
Keenan Nemetz
17d ago
|
19 | "issues/" + idA + "/issue.md": "---\n" + |
| 20 | `title: "It broke"` + "\n" + | |
| 21 | "state: open\n" + | |
| 22 | "author: keenan\n" + | |
| 23 | "created: 2026-10-22T21:00:00Z\n" + | |
| 24 | "labels:\n" + | |
| 25 | " - bug\n" + | |
| 26 | " - needs-repro\n" + | |
| 27 | "---\n" + | |
| 28 | "Steps to reproduce.\n", | |
|
37c01e5a
Keenan Nemetz
17d ago
|
29 | "issues/" + idA + "/comments/" + idB + ".md": "---\nauthor: alice\ncreated: 2026-10-22T21:01:00Z\n---\nMe too.\n", |
| 30 | "issues/" + idA + "/comments/" + idC + ".md": "---\nauthor: bob\ncreated: 2026-10-22T21:02:00Z\n---\nFixed in main.\n", | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | func TestParseValid(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
35 | t.Parallel() |
|
c5902451
Keenan Nemetz
16d ago
|
36 | list, err := ParseIssues(validContents()) |
|
37c01e5a
Keenan Nemetz
17d ago
|
37 | if err != nil { |
| 38 | t.Fatalf("unexpected error: %v", err) | |
| 39 | } | |
| 40 | if len(list) != 1 { | |
| 41 | t.Fatalf("issues = %d, want 1", len(list)) | |
| 42 | } | |
| 43 | iss := list[0] | |
| 44 | if iss.Title != "It broke" || iss.State != "open" || iss.Author != "keenan" { | |
| 45 | t.Fatalf("bad issue: %+v", iss) | |
| 46 | } | |
| 47 | if len(iss.Labels) != 2 || iss.Labels[0] != "bug" { | |
| 48 | t.Fatalf("labels = %v", iss.Labels) | |
| 49 | } | |
| 50 | if iss.Description != "Steps to reproduce.\n" { | |
| 51 | t.Fatalf("description = %q", iss.Description) | |
| 52 | } | |
| 53 | if len(iss.Comments) != 2 { | |
| 54 | t.Fatalf("comments = %d, want 2", len(iss.Comments)) | |
| 55 | } | |
| 56 | // Sorted by id, which is time-ordered. | |
| 57 | if iss.Comments[0].Author != "alice" || iss.Comments[1].Author != "bob" { | |
| 58 | t.Fatalf("comment order: %s then %s", iss.Comments[0].Author, iss.Comments[1].Author) | |
| 59 | } | |
| 60 | if strings.TrimSpace(iss.Comments[0].Body) != "Me too." { | |
| 61 | t.Fatalf("comment body = %q", iss.Comments[0].Body) | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | func TestParseIgnoresUnrelatedFiles(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
66 | t.Parallel() |
|
37c01e5a
Keenan Nemetz
17d ago
|
67 | c := validContents() |
| 68 | c["README.md"] = "notes about the issues branch\n" | |
|
c5902451
Keenan Nemetz
16d ago
|
69 | if _, err := ParseIssues(c); err != nil { |
|
37c01e5a
Keenan Nemetz
17d ago
|
70 | t.Fatalf("unrelated files should be ignored: %v", err) |
| 71 | } | |
| 72 | } | |
| 73 | ||
|
307cbc2a
Keenan Nemetz
17d ago
|
74 | // TestRenderParseRoundTrip is the format's contract: whatever the writer emits, |
| 75 | // the reader reads back. The titles are the ones that break naive YAML — an | |
| 76 | // unquoted `#` truncates, a leading `-` reads as a sequence, `null` reads as | |
| 77 | // nothing — which is why free text is always quoted. | |
| 78 | func TestRenderParseRoundTrip(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
79 | t.Parallel() |
|
307cbc2a
Keenan Nemetz
17d ago
|
80 | titles := []string{ |
| 81 | "crash on startup", | |
| 82 | "crash on a<b> & c", | |
| 83 | "fix #12 crash", | |
| 84 | "panic: nil map", | |
| 85 | "- not a list", | |
| 86 | "*anchor", | |
| 87 | "null", | |
| 88 | "~", | |
| 89 | "123", | |
| 90 | `it's "quoted"`, | |
| 91 | `path C:\temp`, | |
| 92 | "café ☕ breaks", | |
| 93 | strings.Repeat("long title ", 17) + "end", | |
| 94 | } | |
| 95 | for _, title := range titles { | |
| 96 | iss := &Issue{ | |
| 97 | ID: idA, | |
| 98 | Title: title, | |
| 99 | State: "open", | |
| 100 | Author: "keenan", | |
| 101 | Created: time.Date(2026, 10, 22, 21, 0, 0, 0, time.UTC), | |
| 102 | Labels: []string{"bug", "null"}, | |
| 103 | Description: "It broke.\n", | |
| 104 | } | |
| 105 | raw, err := renderIssue(iss) | |
| 106 | if err != nil { | |
| 107 | t.Fatalf("%q: render: %v", title, err) | |
| 108 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
109 | got, err := ParseIssues(map[string]string{"issues/" + idA + "/issue.md": string(raw)}) |
|
307cbc2a
Keenan Nemetz
17d ago
|
110 | if err != nil { |
| 111 | t.Fatalf("%q: parse:\n%s\nerror: %v", title, raw, err) | |
| 112 | } | |
| 113 | if got[0].Title != title { | |
| 114 | t.Fatalf("title came back as %q, want %q:\n%s", got[0].Title, title, raw) | |
| 115 | } | |
| 116 | if got[0].Description != "It broke.\n" { | |
| 117 | t.Fatalf("%q: description = %q", title, got[0].Description) | |
| 118 | } | |
| 119 | if len(got[0].Labels) != 2 || got[0].Labels[1] != "null" { | |
| 120 | t.Fatalf("%q: labels = %v", title, got[0].Labels) | |
| 121 | } | |
| 122 | } | |
| 123 | } | |
| 124 | ||
|
67dd8854
Keenan Nemetz
17d ago
|
125 | // TestAssigneesAreAccountNames: an assignee names a person, so the value has to |
| 126 | // be something the roster could issue. Anything else — a capital, a dot, a | |
| 127 | // leading @ — is a typo that would render as a filter link to nobody. | |
| 128 | func TestAssigneesAreAccountNames(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
129 | t.Parallel() |
|
67dd8854
Keenan Nemetz
17d ago
|
130 | // Build the files the way a writer would, so the validator is what refuses |
| 131 | // rather than YAML refusing to parse the line first. | |
| 132 | mk := func(assignees ...string) map[string]string { | |
|
c5902451
Keenan Nemetz
16d ago
|
133 | d := NewDoc() |
|
67dd8854
Keenan Nemetz
17d ago
|
134 | d.Text("title", "assigned") |
| 135 | d.Word("state", "open") | |
| 136 | d.Word("author", "keenan") | |
| 137 | d.Word("created", "2026-10-22T21:00:00Z") | |
| 138 | d.List("assignees", assignees) | |
| 139 | raw, err := d.Bytes("body\n") | |
| 140 | if err != nil { | |
| 141 | t.Fatal(err) | |
| 142 | } | |
| 143 | c := validContents() | |
| 144 | c["issues/"+idA+"/issue.md"] = string(raw) | |
| 145 | return c | |
| 146 | } | |
| 147 | ||
|
c5902451
Keenan Nemetz
16d ago
|
148 | list, err := ParseIssues(mk("wilbur", "keenan")) |
|
67dd8854
Keenan Nemetz
17d ago
|
149 | if err != nil { |
| 150 | t.Fatalf("valid assignees refused: %v", err) | |
| 151 | } | |
| 152 | if got := list[0].Assignees; len(got) != 2 || got[0] != "keenan" || got[1] != "wilbur" { | |
| 153 | t.Fatalf("assignees = %v, want sorted [keenan wilbur]", got) | |
| 154 | } | |
| 155 | ||
| 156 | for _, bad := range []string{"Wilbur", "wil.bur", "@wilbur", "w", strings.Repeat("a", 33), "wil bur"} { | |
|
c5902451
Keenan Nemetz
16d ago
|
157 | _, err := ParseIssues(mk(bad)) |
|
67dd8854
Keenan Nemetz
17d ago
|
158 | ve, ok := err.(*ValidationError) |
| 159 | if !ok { | |
| 160 | t.Fatalf("%q was accepted: %v", bad, err) | |
| 161 | } | |
| 162 | if !strings.Contains(ve.Problems[0], "invalid assignee") || !strings.Contains(ve.Problems[0], bad) { | |
| 163 | t.Fatalf("%q: unhelpful refusal: %v", bad, ve.Problems) | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | // TestAssigneeRuleMatchesAccountNames keeps the format's copy of the roster's | |
| 169 | // name rule honest. A name the roster would accept but an issue would reject | |
| 170 | // (or the reverse) is a bug that only shows up as a rejected push. | |
| 171 | func TestAssigneeRuleMatchesAccountNames(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
172 | t.Parallel() |
|
67dd8854
Keenan Nemetz
17d ago
|
173 | for _, s := range []string{"keenan", "wilbur-2", "a_b", "x1", "@keenan", "Keenan", "a.b", |
| 174 | "a", strings.Repeat("a", 32), strings.Repeat("a", 33), "with space"} { | |
| 175 | want := identity.ValidateName(s) == nil | |
| 176 | if got := assigneeRE.MatchString(s); got != want { | |
| 177 | t.Fatalf("%q: issue format says %v, roster says %v", s, got, want) | |
| 178 | } | |
| 179 | } | |
| 180 | } | |
| 181 | ||
| 182 | // TestAssigneeRoundTrip: what the writer emits is what the reader reads, and the | |
| 183 | // list is written sorted so an assignment is one added line. | |
| 184 | func TestAssigneeRoundTrip(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
185 | t.Parallel() |
|
67dd8854
Keenan Nemetz
17d ago
|
186 | iss := &Issue{ |
| 187 | ID: idA, | |
| 188 | Title: "assigned", | |
| 189 | State: "open", | |
| 190 | Author: "keenan", | |
| 191 | Created: time.Date(2026, 10, 22, 21, 0, 0, 0, time.UTC), | |
| 192 | Assignees: []string{"wilbur", "keenan"}, | |
| 193 | Description: "body\n", | |
| 194 | } | |
| 195 | raw, err := renderIssue(iss) | |
| 196 | if err != nil { | |
| 197 | t.Fatal(err) | |
| 198 | } | |
| 199 | if !strings.Contains(string(raw), "assignees:\n - keenan\n - wilbur\n") { | |
| 200 | t.Fatalf("assignees not written sorted, one per line:\n%s", raw) | |
| 201 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
202 | got, err := ParseIssues(map[string]string{"issues/" + idA + "/issue.md": string(raw)}) |
|
67dd8854
Keenan Nemetz
17d ago
|
203 | if err != nil { |
| 204 | t.Fatalf("parse: %v\n%s", raw, err) | |
| 205 | } | |
| 206 | if len(got[0].Assignees) != 2 || got[0].Assignees[0] != "keenan" || got[0].Assignees[1] != "wilbur" { | |
| 207 | t.Fatalf("assignees came back as %v", got[0].Assignees) | |
| 208 | } | |
| 209 | } | |
| 210 | ||
|
9cc2f087
Keenan Nemetz
17d ago
|
211 | // TestAnchorRoundTrips: an anchored comment is four extra fields on the same file |
| 212 | // shape, and the writer and reader have to agree about all of them. | |
| 213 | func TestAnchorRoundTrips(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
214 | t.Parallel() |
|
9cc2f087
Keenan Nemetz
17d ago
|
215 | c := &Comment{ |
| 216 | ID: idB, | |
| 217 | Author: "keenan", | |
| 218 | Created: time.Date(2026, 10, 23, 9, 0, 0, 0, time.UTC), | |
| 219 | Body: "this line is wrong\n", | |
| 220 | Anchor: &Anchor{ | |
|
c5902451
Keenan Nemetz
16d ago
|
221 | Path: "hook/hook.go", |
|
9cc2f087
Keenan Nemetz
17d ago
|
222 | Side: "new", |
| 223 | Line: 412, | |
| 224 | Blob: "0123456789abcdef0123456789abcdef01234567", | |
| 225 | }, | |
| 226 | } | |
| 227 | raw, err := RenderComment(c) | |
| 228 | if err != nil { | |
| 229 | t.Fatal(err) | |
| 230 | } | |
| 231 | got, prob := ParseComment(idB, string(raw), "issues/"+idA+"/comments/"+idB+".md") | |
| 232 | if prob != "" { | |
| 233 | t.Fatalf("parse: %s\n%s", prob, raw) | |
| 234 | } | |
| 235 | if got.Anchor == nil { | |
| 236 | t.Fatalf("the anchor was lost:\n%s", raw) | |
| 237 | } | |
| 238 | if *got.Anchor != *c.Anchor { | |
| 239 | t.Fatalf("anchor = %+v, want %+v", *got.Anchor, *c.Anchor) | |
| 240 | } | |
| 241 | // A comment with no anchor stays a comment with no anchor. | |
| 242 | plain, err := RenderComment(&Comment{ID: idC, Author: "keenan", Created: c.Created, Body: "hi\n"}) | |
| 243 | if err != nil { | |
| 244 | t.Fatal(err) | |
| 245 | } | |
| 246 | if strings.Contains(string(plain), "path:") { | |
| 247 | t.Fatalf("an anchorless comment grew an anchor:\n%s", plain) | |
| 248 | } | |
| 249 | } | |
| 250 | ||
| 251 | // TestAnchorMustBeWhole: half an anchor, or one pointing outside the repository, | |
| 252 | // is a mistake that would render as a link to nowhere. | |
| 253 | func TestAnchorMustBeWhole(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
254 | t.Parallel() |
|
9cc2f087
Keenan Nemetz
17d ago
|
255 | const blob = "0123456789abcdef0123456789abcdef01234567" |
| 256 | cases := []struct{ name, head string }{ | |
| 257 | {"line alone", "author: keenan\ncreated: 2026-10-23T09:00:00Z\nline: 4\n"}, | |
| 258 | {"path without line", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: a.go\nside: new\n"}, | |
| 259 | {"bad side", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: a.go\nside: middle\nline: 4\nblob: " + blob + "\n"}, | |
| 260 | {"line zero", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: a.go\nside: new\nline: 0\nblob: " + blob + "\n"}, | |
| 261 | {"short blob", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: a.go\nside: new\nline: 4\nblob: abc123\n"}, | |
| 262 | {"absolute path", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: /etc/passwd\nside: new\nline: 4\nblob: " + blob + "\n"}, | |
| 263 | {"escaping path", "author: keenan\ncreated: 2026-10-23T09:00:00Z\npath: ../secret\nside: new\nline: 4\nblob: " + blob + "\n"}, | |
| 264 | } | |
| 265 | for _, c := range cases { | |
|
b492b557
Keenan Nemetz
3d ago
|
266 | _, prob := ParseComment(idB, "---\n"+c.head+"---\nbody\n", "changes/"+idA+"/comments/"+idB+".md") |
|
9cc2f087
Keenan Nemetz
17d ago
|
267 | if prob == "" { |
| 268 | t.Fatalf("%s was accepted", c.name) | |
| 269 | } | |
| 270 | if !strings.Contains(prob, "comments/"+idB+".md") { | |
| 271 | t.Fatalf("%s: refusal does not name the file: %s", c.name, prob) | |
| 272 | } | |
| 273 | } | |
| 274 | } | |
| 275 | ||
|
79c88d72
Keenan Nemetz
17d ago
|
276 | // TestAuthorOfReadsAnyMetadataFile: the hook asks this before letting anybody |
| 277 | // change a comment, so it has to read an author out of a file whose other fields | |
| 278 | // belong to a different entity — an issue file's title, state and labels are not | |
| 279 | // the reader's business. | |
| 280 | func TestAuthorOfReadsAnyMetadataFile(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
281 | t.Parallel() |
|
79c88d72
Keenan Nemetz
17d ago
|
282 | comment := "---\nauthor: wilbur\ncreated: 2026-10-22T21:01:00Z\n---\nwords\n" |
| 283 | if got, err := AuthorOf([]byte(comment)); err != nil || got != "wilbur" { | |
| 284 | t.Fatalf("comment author = %q, %v", got, err) | |
| 285 | } | |
| 286 | issue := "---\ntitle: \"bug\"\nstate: open\nauthor: keenan\ncreated: 2026-10-22T21:00:00Z\n" + | |
| 287 | "labels:\n - bug\nassignees:\n - wilbur\n---\nbody\n" | |
| 288 | if got, err := AuthorOf([]byte(issue)); err != nil || got != "keenan" { | |
| 289 | t.Fatalf("issue author = %q, %v", got, err) | |
| 290 | } | |
| 291 | if _, err := AuthorOf([]byte("---\ncreated: 2026-10-22T21:00:00Z\n---\nbody\n")); err == nil { | |
| 292 | t.Fatal("a file with no author was accepted") | |
| 293 | } | |
| 294 | if _, err := AuthorOf([]byte("no frontmatter\n")); err == nil { | |
| 295 | t.Fatal("a file with no frontmatter was accepted") | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | // TestCommentPathMatchesTheReader: the writer and the validator have to agree | |
| 300 | // about where a comment lives, or an edit commits to a path nobody reads. | |
| 301 | func TestCommentPathMatchesTheReader(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
302 | t.Parallel() |
|
c5902451
Keenan Nemetz
16d ago
|
303 | path := IssueCommentPath(idA, idB) |
|
79c88d72
Keenan Nemetz
17d ago
|
304 | if path != "issues/"+idA+"/comments/"+idB+".md" { |
| 305 | t.Fatalf("path = %q", path) | |
| 306 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
307 | if !IsIssueCommentPath(path) { |
|
79c88d72
Keenan Nemetz
17d ago
|
308 | t.Fatal("a comment path was not recognised") |
| 309 | } | |
|
c5902451
Keenan Nemetz
16d ago
|
310 | if IsIssueCommentPath("issues/" + idA + "/issue.md") { |
|
79c88d72
Keenan Nemetz
17d ago
|
311 | t.Fatal("an issue file was mistaken for a comment") |
| 312 | } | |
|
b492b557
Keenan Nemetz
3d ago
|
313 | if IsIssueCommentPath("changes/" + idA + "/change.md") { |
|
79c88d72
Keenan Nemetz
17d ago
|
314 | t.Fatal("a request file was mistaken for a comment") |
| 315 | } | |
| 316 | } | |
| 317 | ||
|
307cbc2a
Keenan Nemetz
17d ago
|
318 | // TestUnquotedTitleIsRefused: a hand-edit that drops the quotes on a title with |
| 319 | // " # " would otherwise store a shorter title and say nothing. | |
| 320 | func TestUnquotedTitleIsRefused(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
321 | t.Parallel() |
|
307cbc2a
Keenan Nemetz
17d ago
|
322 | c := validContents() |
| 323 | c["issues/"+idA+"/issue.md"] = "---\ntitle: fix #12 crash\nstate: open\nauthor: keenan\n" + | |
| 324 | "created: 2026-10-22T21:00:00Z\n---\nbody\n" | |
|
c5902451
Keenan Nemetz
16d ago
|
325 | _, err := ParseIssues(c) |
|
307cbc2a
Keenan Nemetz
17d ago
|
326 | ve, ok := err.(*ValidationError) |
| 327 | if !ok { | |
| 328 | t.Fatalf("error is %T, want *ValidationError: %v", err, err) | |
| 329 | } | |
| 330 | if !strings.Contains(err.Error(), "must be double-quoted") { | |
| 331 | t.Fatalf("the refusal does not explain the quotes: %v", ve.Problems) | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | // TestTitleOnOneLine: the writer refuses control characters, so the reader must | |
| 336 | // too — otherwise a tree the server accepts is a tree nobody can edit again. | |
| 337 | func TestTitleOnOneLine(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
338 | t.Parallel() |
|
307cbc2a
Keenan Nemetz
17d ago
|
339 | c := validContents() |
| 340 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"two\\nlines\"\nstate: open\nauthor: keenan\n" + | |
| 341 | "created: 2026-10-22T21:00:00Z\n---\nbody\n" | |
|
c5902451
Keenan Nemetz
16d ago
|
342 | if _, err := ParseIssues(c); err == nil { |
|
307cbc2a
Keenan Nemetz
17d ago
|
343 | t.Fatal("a title with a newline was accepted") |
| 344 | } | |
| 345 | } | |
| 346 | ||
|
37c01e5a
Keenan Nemetz
17d ago
|
347 | func TestValidationFailures(t *testing.T) { |
|
44530e74
Keenan Nemetz
16d ago
|
348 | t.Parallel() |
|
37c01e5a
Keenan Nemetz
17d ago
|
349 | for name, mutate := range map[string]func(map[string]string){ |
| 350 | "bad ulid directory": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
351 | c["issues/not-a-ulid/issue.md"] = "---\ntitle: \"x\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
352 | }, |
|
307cbc2a
Keenan Nemetz
17d ago
|
353 | "missing issue file": func(c map[string]string) { |
| 354 | delete(c, "issues/"+idA+"/issue.md") | |
|
37c01e5a
Keenan Nemetz
17d ago
|
355 | }, |
| 356 | "empty title": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
357 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
358 | }, |
| 359 | "long title": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
360 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"" + strings.Repeat("x", 300) + "\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
361 | }, |
| 362 | "bad state": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
363 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: wontfix\nauthor: k\ncreated: 2026-10-22T21:00:00Z\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
364 | }, |
| 365 | "missing author": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
366 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: open\ncreated: 2026-10-22T21:00:00Z\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
367 | }, |
| 368 | "bad date": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
369 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: open\nauthor: k\ncreated: yesterday\n---\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
370 | }, |
|
307cbc2a
Keenan Nemetz
17d ago
|
371 | "unknown frontmatter field": func(c map[string]string) { |
| 372 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\nsneaky: true\n---\n" | |
|
37c01e5a
Keenan Nemetz
17d ago
|
373 | }, |
| 374 | "bad label": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
375 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\nlabels:\n - \"Has Spaces\"\n---\n" |
| 376 | }, | |
| 377 | "no frontmatter": func(c map[string]string) { | |
| 378 | c["issues/"+idA+"/issue.md"] = "just text\n" | |
| 379 | }, | |
| 380 | "unterminated frontmatter": func(c map[string]string) { | |
| 381 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"t\"\nstate: open\nauthor: k\ncreated: 2026-10-22T21:00:00Z\nbody\n" | |
|
37c01e5a
Keenan Nemetz
17d ago
|
382 | }, |
| 383 | "comment without frontmatter": func(c map[string]string) { | |
| 384 | c["issues/"+idA+"/comments/"+idB+".md"] = "just text\n" | |
| 385 | }, | |
| 386 | "comment unknown field": func(c map[string]string) { | |
|
307cbc2a
Keenan Nemetz
17d ago
|
387 | c["issues/"+idA+"/comments/"+idB+".md"] = "---\nauthor: a\ncreated: 2026-10-22T21:00:00Z\nedited: \"yes\"\n---\nx\n" |
|
37c01e5a
Keenan Nemetz
17d ago
|
388 | }, |
| 389 | "comment unterminated": func(c map[string]string) { | |
| 390 | c["issues/"+idA+"/comments/"+idB+".md"] = "---\nauthor: a\ncreated: 2026-10-22T21:00:00Z\nx\n" | |
| 391 | }, | |
| 392 | "stray file in issue dir": func(c map[string]string) { | |
| 393 | c["issues/"+idA+"/notes.txt"] = "nope" | |
| 394 | }, | |
| 395 | "nested too deep": func(c map[string]string) { | |
| 396 | c["issues/"+idA+"/comments/x/y.md"] = "nope" | |
| 397 | }, | |
| 398 | } { | |
| 399 | t.Run(name, func(t *testing.T) { | |
| 400 | c := validContents() | |
| 401 | mutate(c) | |
|
c5902451
Keenan Nemetz
16d ago
|
402 | if _, err := ParseIssues(c); err == nil { |
|
37c01e5a
Keenan Nemetz
17d ago
|
403 | t.Fatal("expected a validation error") |
| 404 | } else if _, ok := err.(*ValidationError); !ok { | |
| 405 | t.Fatalf("error is %T, want *ValidationError: %v", err, err) | |
| 406 | } | |
| 407 | }) | |
| 408 | } | |
| 409 | } | |
| 410 | ||
| 411 | func TestAllProblemsReported(t *testing.T) { | |
|
44530e74
Keenan Nemetz
16d ago
|
412 | t.Parallel() |
|
37c01e5a
Keenan Nemetz
17d ago
|
413 | c := validContents() |
|
307cbc2a
Keenan Nemetz
17d ago
|
414 | c["issues/"+idA+"/issue.md"] = "---\ntitle: \"\"\nstate: nope\nauthor: \"\"\ncreated: \"\"\n---\n" |
|
c5902451
Keenan Nemetz
16d ago
|
415 | _, err := ParseIssues(c) |
|
37c01e5a
Keenan Nemetz
17d ago
|
416 | ve, ok := err.(*ValidationError) |
| 417 | if !ok { | |
| 418 | t.Fatalf("error is %T: %v", err, err) | |
| 419 | } | |
| 420 | if len(ve.Problems) < 4 { | |
| 421 | t.Fatalf("want several problems, got %d: %v", len(ve.Problems), ve.Problems) | |
| 422 | } | |
| 423 | } |