Files
khosra/internal/web/asset_test.go
T
bdeshi f4ba695e87 generate sized derivatives ahead of the request
A pass over the content at startup writes three widths per picture into a cache
outside the site root, named by the source's content hash and the width (ADR-0042).
Idempotent by construction: a rerun stats and skips, an edited picture takes a new
name, and nothing stale can be served under an old one. Restarting the evidence
site made 0 derivatives the second time, as it should.

Ahead of the request rather than during it, because resampling is felt and there is
no page cache yet to hide it. Outside the site root, because the engine reads that
directory and must not leave generated files in somebody's content git — a lost
cache costs one startup pass and no correctness.

Markup now carries the original as src, the derivatives as srcset closed by the
original at its own width, and width/height from the original — which retires most
of the latent row about the output floor; only a gallery's alt is still empty, and
a filename cannot supply that.

Two things the work itself decided:

`Fragment.Items` became `Fragment.Pictures`, ADR-0037's own revisit trigger. Items
had one consumer, so widening it beat adding a second list beside it.

"A browser can show it" and "we can resample it" are different questions, and
conflating them nearly deleted content: an SVG has no decoder here, so a single
predicate would have dropped SVGs from galleries silently. Undecodable and
unsupported pictures are now rendered as they are, without a size or a srcset.
2026-07-31 03:57:52 +06:00

105 lines
3.9 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"testing/fstest"
"khosra/internal/content"
"khosra/internal/render"
)
func assetHandler(t *testing.T) http.Handler {
t.Helper()
fsys := fstest.MapFS{
// A directory bundle owns the files beside it.
"content/art/monsoon/index.md": {Data: []byte("---\ntitle: Monsoon\n---\n{{< gallery >}}\n")},
// A Bengali variant, so /bn/ is a language prefix at all: the engine treats a leading segment as a
// language only when some bundle is written in it (content-model.md).
"content/art/monsoon/index.bn.md": {Data: []byte("---\ntitle: বর্ষা\n---\nx\n")},
"content/art/monsoon/10-first.jpg": {Data: []byte("\xff\xd8\xff-not-really-a-jpeg")},
"content/art/monsoon/_notes.md": {Data: []byte("a fragment\n")},
"content/art/monsoon/notes.md": {Data: []byte("---\ntitle: Notes\n---\nx\n")},
// A single-file bundle has no directory of its own.
"content/posts/plain.md": {Data: []byte("---\ntitle: Plain\n---\nx\n")},
"content/posts/loose.jpg": {Data: []byte("bytes")},
// A series landing page, whose directory also holds child bundles.
"content/comics/series/_index.md": {Data: []byte("---\ntitle: Series\n---\nx\n")},
"content/comics/series/cover.png": {Data: []byte("png")},
"content/comics/series/one.md": {Data: []byte("---\ntitle: One\n---\nx\n")},
}
bundles, err := content.Scan(fsys)
if err != nil {
t.Fatal(err)
}
r, err := render.New(nil, content.Settings{}, nil)
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
}
func TestABundlesOwnFilesAreServed(t *testing.T) {
h := assetHandler(t)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/art/monsoon/10-first.jpg", nil))
if rec.Code != http.StatusOK {
t.Fatalf("got %d, want 200 — this is what a relative src in a body resolves to", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "image/jpeg") {
t.Errorf("content-type = %q, want image/jpeg from the extension", ct)
}
// A landing page's directory works the same way.
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/series/cover.png", nil))
if rec.Code != http.StatusOK {
t.Errorf("a series landing page owns its files too: got %d", rec.Code)
}
// Under a language prefix as well: an image is not translated.
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/bn/art/monsoon/10-first.jpg", nil))
if rec.Code != http.StatusOK {
t.Errorf("prefixed request for the same file: got %d", rec.Code)
}
}
func TestMarkdownInsideABundleIsNeverAnAsset(t *testing.T) {
// Serving these raw would publish fragments an author never addressed, and hand out the source of a
// bundle that has its own rendered URL.
h := assetHandler(t)
for _, path := range []string{"/art/monsoon/_notes.md", "/art/monsoon/notes.md"} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code == http.StatusOK {
t.Errorf("GET %s = 200, want a miss:\n%s", path, rec.Body.String())
}
}
}
func TestASingleFileBundleOwnsNoDirectory(t *testing.T) {
// Its neighbours belong to the section, not to it — so nothing is served under its slash-terminated URL.
h := assetHandler(t)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/plain/loose.jpg", nil))
if rec.Code != http.StatusNotFound {
t.Errorf("got %d, want 404", rec.Code)
}
}
func TestAnAssetRequestCannotWanderOffItsBundle(t *testing.T) {
h := assetHandler(t)
for _, path := range []string{
"/art/monsoon/../../posts/loose.jpg",
"/art/nonexistent/10-first.jpg",
"/art/monsoon/missing.jpg",
} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code == http.StatusOK {
t.Errorf("GET %s = 200, want a miss:\n%s", path, rec.Body.String())
}
}
}