Files
khosra/internal/web/slug_test.go
T
bdeshi 7da2a58fd5 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-08-01 02:23:36 +06:00

157 lines
6.4 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"testing/fstest"
"khosra/internal/content"
"khosra/internal/render"
)
func slugHandler(t *testing.T, fsys fstest.MapFS) http.Handler {
t.Helper()
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 TestASlugRenamesTheAddressInEveryLanguage(t *testing.T) {
h := slugHandler(t, fstest.MapFS{
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\nslug: ekti-post\n---\nEnglish.\n")},
"content/posts/hello-world.bn.md": {Data: []byte("---\ntitle: একটি পোস্ট\n---\nবাংলা।\n")},
})
for _, path := range []string{"/posts/ekti-post/", "/bn/posts/ekti-post/"} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusOK {
t.Errorf("GET %s = %d, want 200 — a slug renames every language (ADR-0035)", path, rec.Code)
}
}
// The old path is gone: the engine serves the new one only, and an author who wants both writes an alias.
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/hello-world/", nil))
if rec.Code != http.StatusNotFound {
t.Errorf("the derived path = %d, want 404", rec.Code)
}
// Canonical and hreflang must name the new address, not the key.
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/bn/posts/ekti-post/", nil))
body := rec.Body.String()
if !strings.Contains(body, `rel="canonical" href="/bn/posts/ekti-post/"`) {
t.Errorf("canonical should name the served address:\n%s", body)
}
if strings.Contains(body, "hello-world") {
t.Errorf("nothing the engine emits should still say hello-world:\n%s", body)
}
}
func TestAnAliasCanKeepTheOldPathWorking(t *testing.T) {
h := slugHandler(t, fstest.MapFS{
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\nslug: ekti-post\naliases: [posts/hello-world]\n---\nx\n")},
})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/hello-world/", nil))
if rec.Code != http.StatusMovedPermanently {
t.Fatalf("got %d, want 301 — this is how a rename keeps its promise (ADR-0008)", rec.Code)
}
if loc := rec.Header().Get("Location"); loc != "/posts/ekti-post/" {
t.Errorf("Location = %q, want the new address", loc)
}
}
func TestListingsAndSitemapsUseTheSluggedAddress(t *testing.T) {
fsys := fstest.MapFS{
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\ndate: 2026-03-01\nslug: ekti-post\n---\nx\n")},
"content/posts/plain.md": {Data: []byte("---\ntitle: Plain\ndate: 2026-02-01\n---\nx\n")},
}
bundles, err := content.Scan(fsys)
if err != nil {
t.Fatal(err)
}
settings := content.Settings{Base: "https://khosra.example"}
r, err := render.New(nil, settings, nil)
if err != nil {
t.Fatal(err)
}
h := Handler(content.NewSite(bundles), r, fsys, nil, settings)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/", nil))
if body := rec.Body.String(); !strings.Contains(body, `href="/posts/ekti-post/"`) || strings.Contains(body, "hello-world") {
t.Errorf("a listing must link the address, not the key:\n%s", body)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/sitemap.xml", nil))
if body := rec.Body.String(); !strings.Contains(body, "/posts/ekti-post/") || strings.Contains(body, "hello-world") {
t.Errorf("a sitemap that disagrees with what is served is worse than none:\n%s", body)
}
}
func TestAmbiguousOrCollidingSlugsAreDropped(t *testing.T) {
// Both cases keep the derived path rather than picking a winner, the same rule aliases follow.
h := slugHandler(t, fstest.MapFS{
// Variants disagree.
"content/posts/a.md": {Data: []byte("---\ntitle: A\nslug: one\n---\nx\n")},
"content/posts/a.bn.md": {Data: []byte("---\ntitle: ক\nslug: two\n---\nx\n")},
// Slug lands on a bundle that already exists.
"content/posts/b.md": {Data: []byte("---\ntitle: B\nslug: taken\n---\nx\n")},
"content/posts/taken.md": {Data: []byte("---\ntitle: Taken\n---\nx\n")},
})
for path, want := range map[string]int{
"/posts/a/": http.StatusOK, // kept its derived path
"/posts/one/": http.StatusNotFound, // neither declared slug won
"/posts/two/": http.StatusNotFound,
"/posts/b/": http.StatusOK,
"/posts/taken/": http.StatusOK, // the bundle already there keeps its URL
} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != want {
t.Errorf("GET %s = %d, want %d", path, rec.Code, want)
}
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/taken/", nil))
if body := rec.Body.String(); !strings.Contains(body, "<h1>Taken</h1>") {
t.Errorf("the bundle already there must still be the one served:\n%s", body)
}
}
func TestASeriesSurvivesItsLandingBeingSlugged(t *testing.T) {
// The reason a slug does not touch the key: membership is the directory (ADR-0033), so renaming the
// landing page's address must not orphan its chapters.
h := slugHandler(t, fstest.MapFS{
"content/comics/the-long-monsoon/_index.md": {Data: []byte("---\ntitle: The Long Monsoon\nslug: monsoon\n---\nx\n")},
"content/comics/the-long-monsoon/first-rain.md": {Data: []byte("---\ntitle: First Rain\norder: 10\n---\nx\n")},
"content/comics/the-long-monsoon/the-flood.md": {Data: []byte("---\ntitle: The Flood\norder: 20\n---\nx\n")},
})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/monsoon/", nil))
if rec.Code != http.StatusOK {
t.Fatalf("the landing page = %d, want 200 at its new address", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"First Rain", "The Flood"} {
if !strings.Contains(body, want) {
t.Errorf("the archive lost %q — a slug must not orphan a series:\n%s", want, body)
}
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/the-long-monsoon/the-flood/", nil))
if rec.Code != http.StatusOK {
t.Errorf("a chapter = %d, want 200: chapters keep their own addresses", rec.Code)
}
if body := rec.Body.String(); !strings.Contains(body, `href="/comics/monsoon/"`) {
t.Errorf("the chapter should link its series at the slugged address:\n%s", body)
}
}