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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 6 | 6 | "bytes" | |
| 7 | 7 | "compress/gzip" | |
| 8 | 8 | "encoding/json" | |
| 9 | + | "fmt" | |
| 9 | 10 | "image" | |
| 10 | 11 | "image/color" | |
| 11 | 12 | "image/png" | |
| 12 | 13 | "io" | |
| 13 | 14 | "net/http" | |
| 15 | + | "os" | |
| 16 | + | "path/filepath" | |
| 14 | 17 | "regexp" | |
| 15 | 18 | "strconv" | |
| 16 | 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 | 181 | // Ref names with a slash are found by their longest prefix, and a commit id | |
| 149 | 182 | // works where a ref does. | |
| 150 | 183 | func TestWebFilesRefs(t *testing.T) { | |
| @@ -293,6 +326,19 @@ func TestWebRaw(t *testing.T) { | |||
| 293 | 326 | require.NoError(t, err) | |
| 294 | 327 | _ = resp.Body.Close() | |
| 295 | 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 | 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 | 497 | assert.Equal(t, want, decodeList(t, plain.body).Paths) | |
| 452 | 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 | 502 | nm := raw(map[string]string{"If-None-Match": etag, "Accept-Encoding": "gzip"}) | |
| 456 | 503 | assert.Equal(t, http.StatusNotModified, nm.status) | |
| 457 | 504 | assert.Empty(t, nm.body) | |
| 458 | 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 | 507 | // Only a full commit id: abbreviations, refs, upper case and unknown ids. | |
| 465 | 508 | for _, p := range []string{ | |
| @@ -520,6 +563,8 @@ func TestWebFinderPage(t *testing.T) { | |||
| 520 | 563 | {"full id", "/files/tree/" + sha, "/files/paths/" + sha, "/files/blob/" + sha}, | |
| 521 | 564 | {"abbreviated id", "/files/tree/" + sha[:7], "/files/paths/" + sha, "/files/blob/" + sha[:7]}, | |
| 522 | 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 | 569 | t.Run(tt.name, func(t *testing.T) { | |
| 525 | 570 | 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 id | 181 | // 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) | ||