detached d5d431e

test(web): cover the finder on blobs, raw validator lists and truncated rows

Add a blob at an abbreviated id (and a full id) to the finder page test,
a list-form If-None-Match to the raw test, and ?rows on a directory over
the entry limit. TestWebPaths keeps one 304 and leaves the validator
matrix to paths_test.go.
commit
d5d431ed16a082172069416c49bd711b7e86e1f0
author
brice · · 2026-10-12 05:10 UTC
parent
0219778
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +50 -5
M cmd/ermite/web_files_test.go +50 -5
@@ -6,11 +6,14 @@ import (
66 "bytes"
77 "compress/gzip"
88 "encoding/json"
9+ "fmt"
910 "image"
1011 "image/color"
1112 "image/png"
1213 "io"
1314 "net/http"
15+ "os"
16+ "path/filepath"
1417 "regexp"
1518 "strconv"
1619 "strings"
@@ -145,6 +148,36 @@ func TestWebTreeRows(t *testing.T) {
145148 }
146149 }
147150
151+// ?rows on a directory with more entries than are listed ends with the row
152+// that links to the directory page, which says the listing is cut.
153+func TestWebTreeRowsTruncated(t *testing.T) {
154+ h := newHarness(t, harnessOpts{})
155+ _, err := h.forge.Repos.Create(h.ctx, domain.Root(), app.CreateParams{Name: h.name("wide"), Visibility: domain.VisibilityPublic})
156+ require.NoError(t, err)
157+ wt := h.t.TempDir()
158+ h.mustGit(wt, "init", "-q", "--initial-branch=main")
159+ // The limit is domain.MaxTreeEntries: one commit with a few more.
160+ n := domain.MaxTreeEntries + 3
161+ require.NoError(t, os.MkdirAll(filepath.Join(wt, "many"), 0o755))
162+ for i := range n {
163+ require.NoError(t, os.WriteFile(filepath.Join(wt, "many", fmt.Sprintf("f%05d", i)), nil, 0o644))
164+ }
165+ require.NoError(t, os.WriteFile(filepath.Join(wt, "small.txt"), []byte("x\n"), 0o644))
166+ h.mustGit(wt, "add", ".")
167+ h.mustGit(wt, "commit", "-q", "-m", "wide")
168+ h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/wide.git"), "main")
169+
170+ status, _, body := h.get("/wide/tree/main/many?rows")
171+ require.Equal(t, 200, status)
172+ assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<tr"), "the listed entries and the row that says there are more")
173+ assert.Contains(t, body, `<tr class="tree-more"><td colspan="7"><a href="/wide/tree/main/many">more entries: open the directory</a></td></tr>`)
174+ assert.Equal(t, 1, strings.Count(body, "tree-more"))
175+
176+ // A directory that fits has no such row.
177+ _, _, body = h.get("/wide/tree?rows")
178+ assert.NotContains(t, body, "tree-more")
179+}
180+
148181 // Ref names with a slash are found by their longest prefix, and a commit id
149182 // works where a ref does.
150183 func TestWebFilesRefs(t *testing.T) {
@@ -293,6 +326,19 @@ func TestWebRaw(t *testing.T) {
293326 require.NoError(t, err)
294327 _ = resp.Body.Close()
295328 assert.Equal(t, http.StatusNotModified, resp.StatusCode)
329+ // A list of validators, weak ones and "*" match as well.
330+ for _, inm := range []string{`"other", ` + etag, "W/" + etag, "*"} {
331+ req.Header.Set("If-None-Match", inm)
332+ resp, err = http.DefaultClient.Do(req)
333+ require.NoError(t, err)
334+ _ = resp.Body.Close()
335+ assert.Equal(t, http.StatusNotModified, resp.StatusCode, inm)
336+ }
337+ req.Header.Set("If-None-Match", `"other", "another"`)
338+ resp, err = http.DefaultClient.Do(req)
339+ require.NoError(t, err)
340+ _ = resp.Body.Close()
341+ assert.Equal(t, http.StatusOK, resp.StatusCode, "no validator matches")
296342 assert.Equal(t, http.StatusOK, h.status(http.MethodHead, "/files/raw/main/notes.txt", nil))
297343 }
298344
@@ -451,15 +497,12 @@ func TestWebPaths(t *testing.T) {
451497 assert.Equal(t, want, decodeList(t, plain.body).Paths)
452498 assert.Equal(t, `"paths-`+sha+`"`, plain.hdr.Get("ETag"), "another variant, another validator")
453499
454- // Revalidation: 304, still through the access rule.
500+ // Revalidation: 304, still through the access rule. The matrix of
501+ // validators (variants, weak, "*") is in paths_test.go.
455502 nm := raw(map[string]string{"If-None-Match": etag, "Accept-Encoding": "gzip"})
456503 assert.Equal(t, http.StatusNotModified, nm.status)
457504 assert.Empty(t, nm.body)
458505 assert.Equal(t, "no-cache", nm.hdr.Get("Cache-Control"))
459- nm = raw(map[string]string{"If-None-Match": plain.hdr.Get("ETag")})
460- assert.Equal(t, http.StatusNotModified, nm.status, "identity validator, identity body")
461- assert.Equal(t, 200, raw(map[string]string{"If-None-Match": etag}).status, "the gzip validator does not match the plain body")
462- assert.Equal(t, 200, raw(map[string]string{"If-None-Match": `"paths-other"`}).status)
463506
464507 // Only a full commit id: abbreviations, refs, upper case and unknown ids.
465508 for _, p := range []string{
@@ -520,6 +563,8 @@ func TestWebFinderPage(t *testing.T) {
520563 {"full id", "/files/tree/" + sha, "/files/paths/" + sha, "/files/blob/" + sha},
521564 {"abbreviated id", "/files/tree/" + sha[:7], "/files/paths/" + sha, "/files/blob/" + sha[:7]},
522565 {"blob page", "/files/blob/main/main.go", "/files/paths/" + sha, "/files/blob/main"},
566+ {"blob at full id", "/files/blob/" + sha + "/main.go", "/files/paths/" + sha, "/files/blob/" + sha},
567+ {"blob at abbreviated id", "/files/blob/" + sha[:7] + "/main.go", "/files/paths/" + sha, "/files/blob/" + sha[:7]},
523568 } {
524569 t.Run(tt.name, func(t *testing.T) {
525570 status, _, body := h.get(tt.path)
@@ -6,11 +6,14 @@ import (
6 "bytes"6 "bytes"
7 "compress/gzip"7 "compress/gzip"
8 "encoding/json"8 "encoding/json"
9+ "fmt"
9 "image"10 "image"
10 "image/color"11 "image/color"
11 "image/png"12 "image/png"
12 "io"13 "io"
13 "net/http"14 "net/http"
15+ "os"
16+ "path/filepath"
14 "regexp"17 "regexp"
15 "strconv"18 "strconv"
16 "strings"19 "strings"
@@ -145,6 +148,36 @@ func TestWebTreeRows(t *testing.T) {
145 }148 }
146 }149 }
147 150
151+// ?rows on a directory with more entries than are listed ends with the row
152+// that links to the directory page, which says the listing is cut.
153+func TestWebTreeRowsTruncated(t *testing.T) {
154+ h := newHarness(t, harnessOpts{})
155+ _, err := h.forge.Repos.Create(h.ctx, domain.Root(), app.CreateParams{Name: h.name("wide"), Visibility: domain.VisibilityPublic})
156+ require.NoError(t, err)
157+ wt := h.t.TempDir()
158+ h.mustGit(wt, "init", "-q", "--initial-branch=main")
159+ // The limit is domain.MaxTreeEntries: one commit with a few more.
160+ n := domain.MaxTreeEntries + 3
161+ require.NoError(t, os.MkdirAll(filepath.Join(wt, "many"), 0o755))
162+ for i := range n {
163+ require.NoError(t, os.WriteFile(filepath.Join(wt, "many", fmt.Sprintf("f%05d", i)), nil, 0o644))
164+ }
165+ require.NoError(t, os.WriteFile(filepath.Join(wt, "small.txt"), []byte("x\n"), 0o644))
166+ h.mustGit(wt, "add", ".")
167+ h.mustGit(wt, "commit", "-q", "-m", "wide")
168+ h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/wide.git"), "main")
169+
170+ status, _, body := h.get("/wide/tree/main/many?rows")
171+ require.Equal(t, 200, status)
172+ assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<tr"), "the listed entries and the row that says there are more")
173+ assert.Contains(t, body, `<tr class="tree-more"><td colspan="7"><a href="/wide/tree/main/many">more entries: open the directory</a></td></tr>`)
174+ assert.Equal(t, 1, strings.Count(body, "tree-more"))
175+
176+ // A directory that fits has no such row.
177+ _, _, body = h.get("/wide/tree?rows")
178+ assert.NotContains(t, body, "tree-more")
179+}
180+
148 // Ref names with a slash are found by their longest prefix, and a commit id181 // Ref names with a slash are found by their longest prefix, and a commit id
149 // works where a ref does.182 // works where a ref does.
150 func TestWebFilesRefs(t *testing.T) {183 func TestWebFilesRefs(t *testing.T) {
@@ -293,6 +326,19 @@ func TestWebRaw(t *testing.T) {
293 require.NoError(t, err)326 require.NoError(t, err)
294 _ = resp.Body.Close()327 _ = resp.Body.Close()
295 assert.Equal(t, http.StatusNotModified, resp.StatusCode)328 assert.Equal(t, http.StatusNotModified, resp.StatusCode)
329+ // A list of validators, weak ones and "*" match as well.
330+ for _, inm := range []string{`"other", ` + etag, "W/" + etag, "*"} {
331+ req.Header.Set("If-None-Match", inm)
332+ resp, err = http.DefaultClient.Do(req)
333+ require.NoError(t, err)
334+ _ = resp.Body.Close()
335+ assert.Equal(t, http.StatusNotModified, resp.StatusCode, inm)
336+ }
337+ req.Header.Set("If-None-Match", `"other", "another"`)
338+ resp, err = http.DefaultClient.Do(req)
339+ require.NoError(t, err)
340+ _ = resp.Body.Close()
341+ assert.Equal(t, http.StatusOK, resp.StatusCode, "no validator matches")
296 assert.Equal(t, http.StatusOK, h.status(http.MethodHead, "/files/raw/main/notes.txt", nil))342 assert.Equal(t, http.StatusOK, h.status(http.MethodHead, "/files/raw/main/notes.txt", nil))
297 }343 }
298 344
@@ -451,15 +497,12 @@ func TestWebPaths(t *testing.T) {
451 assert.Equal(t, want, decodeList(t, plain.body).Paths)497 assert.Equal(t, want, decodeList(t, plain.body).Paths)
452 assert.Equal(t, `"paths-`+sha+`"`, plain.hdr.Get("ETag"), "another variant, another validator")498 assert.Equal(t, `"paths-`+sha+`"`, plain.hdr.Get("ETag"), "another variant, another validator")
453 499
454- // Revalidation: 304, still through the access rule.500+ // Revalidation: 304, still through the access rule. The matrix of
501+ // validators (variants, weak, "*") is in paths_test.go.
455 nm := raw(map[string]string{"If-None-Match": etag, "Accept-Encoding": "gzip"})502 nm := raw(map[string]string{"If-None-Match": etag, "Accept-Encoding": "gzip"})
456 assert.Equal(t, http.StatusNotModified, nm.status)503 assert.Equal(t, http.StatusNotModified, nm.status)
457 assert.Empty(t, nm.body)504 assert.Empty(t, nm.body)
458 assert.Equal(t, "no-cache", nm.hdr.Get("Cache-Control"))505 assert.Equal(t, "no-cache", nm.hdr.Get("Cache-Control"))
459- nm = raw(map[string]string{"If-None-Match": plain.hdr.Get("ETag")})
460- assert.Equal(t, http.StatusNotModified, nm.status, "identity validator, identity body")
461- assert.Equal(t, 200, raw(map[string]string{"If-None-Match": etag}).status, "the gzip validator does not match the plain body")
462- assert.Equal(t, 200, raw(map[string]string{"If-None-Match": `"paths-other"`}).status)
463 506
464 // Only a full commit id: abbreviations, refs, upper case and unknown ids.507 // Only a full commit id: abbreviations, refs, upper case and unknown ids.
465 for _, p := range []string{508 for _, p := range []string{
@@ -520,6 +563,8 @@ func TestWebFinderPage(t *testing.T) {
520 {"full id", "/files/tree/" + sha, "/files/paths/" + sha, "/files/blob/" + sha},563 {"full id", "/files/tree/" + sha, "/files/paths/" + sha, "/files/blob/" + sha},
521 {"abbreviated id", "/files/tree/" + sha[:7], "/files/paths/" + sha, "/files/blob/" + sha[:7]},564 {"abbreviated id", "/files/tree/" + sha[:7], "/files/paths/" + sha, "/files/blob/" + sha[:7]},
522 {"blob page", "/files/blob/main/main.go", "/files/paths/" + sha, "/files/blob/main"},565 {"blob page", "/files/blob/main/main.go", "/files/paths/" + sha, "/files/blob/main"},
566+ {"blob at full id", "/files/blob/" + sha + "/main.go", "/files/paths/" + sha, "/files/blob/" + sha},
567+ {"blob at abbreviated id", "/files/blob/" + sha[:7] + "/main.go", "/files/paths/" + sha, "/files/blob/" + sha[:7]},
523 } {568 } {
524 t.Run(tt.name, func(t *testing.T) {569 t.Run(tt.name, func(t *testing.T) {
525 status, _, body := h.get(tt.path)570 status, _, body := h.get(tt.path)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close