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.
105 lines
3.9 KiB
Go
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())
|
|
}
|
|
}
|
|
}
|