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/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.

Blame · metadata/limits_test.go

Line attribution on main · 185 line(s)

4581690d
Keenan Nemetz
13d ago
1 package metadata
2
3 import (
4 "strings"
5 "testing"
6 "time"
7
d37d301f
Keenan Nemetz
15h ago
8 "rodent.software/vole/core/git"
4581690d
Keenan Nemetz
13d ago
9 )
10
11 // limitsRepo is a bare repository with an empty metadata branch.
12 func 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.
31 func 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
43 func 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.
51 func 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.
69 func TestLoadRequestsRefusesAnOversizedFile(t *testing.T) {
70 t.Parallel()
71 g := limitsRepo(t)
72 huge := strings.Repeat("x", MaxFileBytes+1)
b492b557
Keenan Nemetz
3d ago
73 putIssueFile(t, g, ChangesDir+"/01M5JR2JK29KKMAA1T9MDNTPBZ/change.md",
4581690d
Keenan Nemetz
13d ago
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.
85 func 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.
102 func 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.
122 func 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 }
5cecc618
Keenan Nemetz
10d ago
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.
138 func 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++ {
b492b557
Keenan Nemetz
3d ago
151 // Odd and even requests share a body, so their change.md files are the
5cecc618
Keenan Nemetz
10d ago
152 // same object under different paths.
153 body := requestBody(headSHA)
154 if i%2 == 1 {
155 body = requestBody(sideSHA)
156 }
b492b557
Keenan Nemetz
3d ago
157 files[ChangesDir+"/"+id(i)+"/change.md"] = []byte(body)
158 files[ChangesDir+"/"+id(i)+"/comments/"+id(i+n)+".md"] = []byte(
5cecc618
Keenan Nemetz
10d ago
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 }