Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 36m ago 32290114
history blame raw 6.3 KiB
1package metadata
2
3import (
4 "strings"
5 "testing"
6 "time"
7
8 "rodent.software/vole/core/git"
9)
10
11// limitsRepo is a bare repository with an empty metadata branch.
12func limitsRepo(t *testing.T) *git.Repo {
13 t.Helper()
14 root := t.TempDir()
15 if err := git.InitBare(root, "main"); err != nil {
16 t.Fatal(err)
17 }
18 g := git.Open(root)
19 seed, err := g.CommitTree(mustTree(t, g), "", "seed")
20 if err != nil {
21 t.Fatal(err)
22 }
23 if err := g.UpdateRef(Ref, seed); err != nil {
24 t.Fatal(err)
25 }
26 return g
27}
28
29// putIssueFile writes raw issue bytes exactly as a hand-edited push would, which
30// is the only way to produce a metadata branch the writers would have refused.
31func putIssueFile(t *testing.T, g *git.Repo, path string, body []byte) string {
32 t.Helper()
33 commit, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error {
34 files[path] = body
35 return nil
36 }, "issue: hand written", "keenan")
37 if err != nil {
38 t.Fatal(err)
39 }
40 return commit
41}
42
43func issueDoc(title, author, body string) []byte {
44 return []byte("---\nid: 01M5JR2JK29KKMAA1T9MDNTPBZ\ntitle: " + title +
45 "\nstate: open\nauthor: " + author + "\ncreated: 2026-10-27T12:00:00Z\n---\n\n" + body)
46}
47
48// TestLoadIssuesRefusesAnOversizedFile is the read-side half of the limit: an
49// anonymous page render must not allocate a file a contributor pushed. Before
50// the cap, LoadIssues read every blob in the tree in full.
51func TestLoadIssuesRefusesAnOversizedFile(t *testing.T) {
52 t.Parallel()
53 g := limitsRepo(t)
54 huge := strings.Repeat("x", MaxFileBytes+1)
55 putIssueFile(t, g, IssuesDir+"/01M5JR2JK29KKMAA1T9MDNTPBZ/issue.md",
56 issueDoc("huge", "keenan", huge))
57
58 _, err := LoadIssues(g, Ref)
59 if err == nil {
60 t.Fatal("LoadIssues read a file past MaxFileBytes")
61 }
62 if !strings.Contains(err.Error(), "limit") {
63 t.Fatalf("refusal did not name the limit: %v", err)
64 }
65}
66
67// TestLoadRequestsRefusesAnOversizedFile is the same rule for change requests,
68// which had no entry cap at all before this.
69func TestLoadRequestsRefusesAnOversizedFile(t *testing.T) {
70 t.Parallel()
71 g := limitsRepo(t)
72 huge := strings.Repeat("x", MaxFileBytes+1)
73 putIssueFile(t, g, ChangesDir+"/01M5JR2JK29KKMAA1T9MDNTPBZ/change.md",
74 []byte("---\nid: 01M5JR2JK29KKMAA1T9MDNTPBZ\ntitle: huge\nauthor: keenan\nbase: main\nhead: "+
75 strings.Repeat("a", 40)+"\ncreated: 2026-10-27T12:00:00Z\n---\n\n"+huge))
76
77 if _, err := LoadRequests(g, Ref); err == nil {
78 t.Fatal("LoadRequests read a file past MaxFileBytes")
79 }
80}
81
82// TestValidateIssuesRefusesAnOversizedTree is the push-side half: pre-receive
83// runs this on the pushed commit, so the author learns why instead of finding
84// that the issue list stopped loading.
85func TestValidateIssuesRefusesAnOversizedTree(t *testing.T) {
86 t.Parallel()
87 g := limitsRepo(t)
88 commit := putIssueFile(t, g, IssuesDir+"/01M5JR2JK29KKMAA1T9MDNTPBZ/issue.md",
89 issueDoc("huge", "keenan", strings.Repeat("x", MaxFileBytes+1)))
90
91 problems := ValidateIssues(g, commit)
92 if len(problems) == 0 {
93 t.Fatal("ValidateIssues accepted a file past MaxFileBytes")
94 }
95 if !strings.Contains(problems[0], "limit") {
96 t.Fatalf("rejection did not name the limit: %v", problems)
97 }
98}
99
100// TestOversizedBodyIsNotWritten keeps the CLI and the web form in step with the
101// push boundary: a body that cannot be pushed should not be committable either.
102func TestOversizedBodyIsNotWritten(t *testing.T) {
103 t.Parallel()
104 iss, err := NewIssue("title", strings.Repeat("x", MaxBodyBytes+1), "keenan", nil, nil,
105 time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC))
106 if err != nil {
107 t.Fatal(err)
108 }
109 if err := validateIssueForWrite(iss); err == nil {
110 t.Fatal("an oversized issue body passed local validation")
111 }
112 if _, err := NewIssueComment(strings.Repeat("x", MaxBodyBytes+1), "keenan", time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC)); err == nil {
113 t.Fatal("an oversized comment body passed local validation")
114 }
115 if _, err := NewRequestComment(strings.Repeat("x", MaxBodyBytes+1), "keenan", time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC)); err == nil {
116 t.Fatal("an oversized request comment passed local validation")
117 }
118}
119
120// TestParseRefusesOversizedBody covers a body that arrives in a file rather than
121// through a writer: the parse path is what pre-receive uses.
122func TestParseRefusesOversizedBody(t *testing.T) {
123 t.Parallel()
124 _, prob := ParseComment("01M5JR2JK29KKMAA1T9MDNTPBZ",
125 "---\nauthor: keenan\ncreated: 2026-10-27T12:00:00Z\n---\n\n"+strings.Repeat("x", MaxBodyBytes+1),
126 "issues/01M5JR2JK29KKMAA1T9MDNTPBZ/comments/01M5JR2JK29KKMAA1T9MDNTPBZ.md")
127 if prob == "" {
128 t.Fatal("ParseComment accepted a body past MaxBodyBytes")
129 }
130}
131
132// TestLoadRequestsReadsEveryFile guards the batched read of the metadata tree:
133// many requests and comments come back complete, and two files with identical
134// bytes (one object in the pack, two paths in the tree) each get their content.
135// The list page reads this tree on every render, so the loader is also the place
136// where a per-file git process would show up as a page that costs as much as the
137// repository has issues.
138func TestLoadRequestsReadsEveryFile(t *testing.T) {
139 t.Parallel()
140 g := limitsRepo(t)
141
142 const n = 12
143 // One character distinguishes the ids, which keeps every id a well-formed
144 // ULID of the same length.
145 const ulidPrefix = "01M5JR2JK29KKMAA1T9MDNTRB"
146 id := func(i int) string {
147 return ulidPrefix + string("0123456789ABCDEFGHJKMNPQRSTVWXYZ"[i])
148 }
149 files := map[string][]byte{}
150 for i := 0; i < n; i++ {
151 // Odd and even requests share a body, so their change.md files are the
152 // same object under different paths.
153 body := requestBody(headSHA)
154 if i%2 == 1 {
155 body = requestBody(sideSHA)
156 }
157 files[ChangesDir+"/"+id(i)+"/change.md"] = []byte(body)
158 files[ChangesDir+"/"+id(i)+"/comments/"+id(i+n)+".md"] = []byte(
159 "---\nauthor: keenan\ncreated: 2026-10-22T22:00:00Z\n---\n\ncomment " + string('a'+rune(i)) + "\n")
160 }
161 if _, err := git.CommitFiles(g, Ref, func(f map[string][]byte) error {
162 for path, body := range files {
163 f[path] = body
164 }
165 return nil
166 }, "requests: batch", "keenan"); err != nil {
167 t.Fatal(err)
168 }
169
170 list, err := LoadRequests(g, Ref)
171 if err != nil {
172 t.Fatalf("LoadRequests: %v", err)
173 }
174 if len(list) != n {
175 t.Fatalf("got %d requests, want %d", len(list), n)
176 }
177 for _, req := range list {
178 if len(req.Comments) != 1 {
179 t.Fatalf("%s has %d comments, want 1", req.ID, len(req.Comments))
180 }
181 if req.Title != "Land the retry" {
182 t.Fatalf("%s title = %q", req.ID, req.Title)
183 }
184 }
185}
186