package content import ( "io/fs" "os" "path/filepath" "testing" "testing/fstest" ) func TestSplitNameDerivesKeyAndLang(t *testing.T) { cases := []struct { name, key, lang string ok bool }{ {name: "pages/about.md", key: "pages/about", lang: "en", ok: true}, {name: "pages/about.en.md", key: "pages/about", lang: "en", ok: true}, {name: "pages/about.bn.md", key: "pages/about", lang: "bn", ok: true}, {name: "posts/hello/index.md", key: "posts/hello", lang: "en", ok: true}, {name: "posts/hello/index.bn.md", key: "posts/hello", lang: "bn", ok: true}, {name: "comics/monsoon/_index.md", key: "comics/monsoon", lang: "en", ok: true}, {name: "posts/my.post.md", key: "posts/my.post", lang: "en", ok: true}, {name: "posts/notes.txt", ok: false}, } for _, c := range cases { key, lang, ok := splitName(c.name) if ok != c.ok { t.Errorf("%s: ok = %v, want %v", c.name, ok, c.ok) continue } if ok && (key != c.key || lang != c.lang) { t.Errorf("%s: got %q/%q, want %q/%q", c.name, key, lang, c.key, c.lang) } } } func TestParseSplitsFrontmatterAndKeepsUnknownKeys(t *testing.T) { b, err := Parse("posts/hello.md", []byte("---\ntitle: Hello\ntags: [a, b]\n---\n\nBody text.\n")) if err != nil { t.Fatal(err) } if b.Title != "Hello" { t.Errorf("title = %q", b.Title) } if got := string(b.Body); got != "Body text.\n" { t.Errorf("body = %q", got) } if _, ok := b.Extra["tags"]; !ok { t.Error("tags did not land in Extra") } if _, ok := b.Extra["title"]; ok { t.Error("title should be lifted out of Extra, not duplicated") } } func TestParseWithoutFrontmatterIsAllBody(t *testing.T) { b, err := Parse("pages/now.md", []byte("Just prose.\n")) if err != nil { t.Fatal(err) } if b.Title != "" || string(b.Body) != "Just prose.\n" { t.Errorf("got title %q body %q", b.Title, b.Body) } } func TestParseRejectsBrokenFrontmatter(t *testing.T) { if _, err := Parse("posts/bad.md", []byte("---\ntitle: [unclosed\n---\nbody\n")); err == nil { t.Fatal("want an error for unparseable YAML") } } func TestParseMissingTitleIsLegal(t *testing.T) { b, err := Parse("status/note.md", []byte("---\ndate: 2026-07-30\n---\nhi\n")) if err != nil { t.Fatalf("a titleless bundle must parse: %v", err) } if b.Title != "" { t.Errorf("title = %q, want empty", b.Title) } } func TestScanSkipsBadBundlesAndUnderscoreDirs(t *testing.T) { fsys := fstest.MapFS{ "content/pages/about.md": {Data: []byte("---\ntitle: About\n---\nx\n")}, "content/posts/hello/index.md": {Data: []byte("---\ntitle: Hello\n---\ny\n")}, "content/posts/broken.md": {Data: []byte("---\ntitle: [oops\n---\nz\n")}, "content/_drafts/secret.md": {Data: []byte("---\ntitle: Secret\n---\nq\n")}, "content/pages/notes.txt": {Data: []byte("not markdown")}, } got, err := Scan(fsys) if err != nil { t.Fatal(err) } keys := map[string]bool{} for _, b := range got { keys[b.Key] = true } if len(got) != 2 || !keys["pages/about"] || !keys["posts/hello"] { t.Fatalf("got %d bundles %v, want pages/about and posts/hello only", len(got), keys) } } func TestScanDropsAmbiguousVariants(t *testing.T) { fsys := fstest.MapFS{ "content/pages/about.md": {Data: []byte("---\ntitle: A\n---\n")}, "content/pages/about.en.md": {Data: []byte("---\ntitle: B\n---\n")}, "content/pages/now.md": {Data: []byte("---\ntitle: Now\n---\n")}, } got, err := Scan(fsys) if err != nil { t.Fatal(err) } if len(got) != 1 || got[0].Key != "pages/now" { t.Fatalf("got %+v, want only pages/now: two spellings of one variant are ambiguous", got) } } func TestNormaliseFoldsDecomposedBengali(t *testing.T) { decomposed := "\u0995\u09c7\u09be" // ka + vowel sign e + vowel sign aa composed := "\u0995\u09cb" // ka + vowel sign o if decomposed == composed { t.Skip("inputs are already identical; nothing to prove") } if Normalise(decomposed) != composed { t.Errorf("NFC(%q) = %q, want %q", decomposed, Normalise(decomposed), composed) } } // TestOpenSiteRefusesSymlinkEscape is the path-traversal guard's evidence: os.DirFS would happily // follow this symlink, os.Root does not (ADR-0031). func TestOpenSiteRefusesSymlinkEscape(t *testing.T) { tmp := t.TempDir() site := filepath.Join(tmp, "site") if err := os.MkdirAll(filepath.Join(site, "content"), 0o755); err != nil { t.Fatal(err) } secret := filepath.Join(tmp, "secret.txt") if err := os.WriteFile(secret, []byte("private"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink("../secret.txt", filepath.Join(site, "escape.txt")); err != nil { t.Skipf("symlinks unavailable: %v", err) } fsys, err := OpenSite(site) if err != nil { t.Fatal(err) } if data, err := fs.ReadFile(fsys, "escape.txt"); err == nil { t.Fatalf("read outside the site root succeeded with %q", data) } if _, err := fs.ReadFile(fsys, "../secret.txt"); err == nil { t.Fatal("traversal with .. succeeded") } } func TestSiteLookupFindsDefaultVariant(t *testing.T) { fsys := fstest.MapFS{ "content/pages/about.md": {Data: []byte("---\ntitle: About\n---\nhi\n")}, "content/pages/about.bn.md": {Data: []byte("---\ntitle: পরিচিতি\n---\nহাই\n")}, } bundles, err := Scan(fsys) if err != nil { t.Fatal(err) } site := NewSite(bundles) if site.Len() != 2 { t.Fatalf("indexed %d bundles, want 2", site.Len()) } b, served, ok := site.Lookup("pages/about", "en") if !ok || b.Title != "About" || served != "en" { t.Fatalf("Lookup gave %+v %q %v, want the English variant", b, served, ok) } if _, _, ok := site.Lookup("pages/missing", "en"); ok { t.Error("Lookup invented a bundle") } } func TestLookupFallsBackThroughLanguages(t *testing.T) { fsys := fstest.MapFS{ "content/pages/about.md": {Data: []byte("---\ntitle: About\n---\n")}, "content/pages/about.bn.md": {Data: []byte("---\ntitle: পরিচিতি\n---\n")}, "content/posts/only.bn.md": {Data: []byte("---\ntitle: শুধু\n---\n")}, "content/posts/plain.md": {Data: []byte("---\ntitle: Plain\n---\n")}, } bundles, err := Scan(fsys) if err != nil { t.Fatal(err) } site := NewSite(bundles) cases := []struct{ key, want, served string }{ {"pages/about", "পরিচিতি", "bn"}, // the requested language exists {"posts/plain", "Plain", "en"}, // falls back to the default {"posts/only", "শুধু", "bn"}, // no default either: any variant beats a 404 } for _, c := range cases { b, served, ok := site.Lookup(c.key, "bn") if !ok || b.Title != c.want || served != c.served { t.Errorf("Lookup(%q, bn) = %q/%q ok=%v, want %q/%q", c.key, b.Title, served, ok, c.want, c.served) } } if got := site.Variants("pages/about"); len(got) != 2 || got[0] != "bn" || got[1] != "en" { t.Errorf("Variants = %v, want [bn en]", got) } if !site.HasLang("bn") || site.HasLang("fr") { t.Error("HasLang misreported") } } func TestURLPrefixesOnlyNonDefaultLanguages(t *testing.T) { cases := map[string]string{"en": "/pages/about/", "": "/pages/about/", "bn": "/bn/pages/about/"} for lang, want := range cases { if got := URL("pages/about", lang); got != want { t.Errorf("URL(pages/about, %q) = %q, want %q", lang, got, want) } } } func TestAliasesAreParsedAndIndexed(t *testing.T) { fsys := fstest.MapFS{ "content/posts/new-name.md": {Data: []byte("---\ntitle: New\naliases: [/posts/old-name/, posts/older]\n---\n")}, "content/pages/single.md": {Data: []byte("---\ntitle: Single\naliases: pages/one\n---\n")}, } bundles, err := Scan(fsys) if err != nil { t.Fatal(err) } site := NewSite(bundles) for alias, want := range map[string]string{ "posts/old-name": "posts/new-name", "posts/older": "posts/new-name", "pages/one": "pages/single", } { got, ok := site.Alias(alias) if !ok || got != want { t.Errorf("Alias(%q) = %q %v, want %q", alias, got, ok, want) } } for _, b := range bundles { if _, leaked := b.Extra["aliases"]; leaked { t.Error("aliases should be lifted out of Extra, not duplicated") } } } func TestAmbiguousAliasesAreDropped(t *testing.T) { fsys := fstest.MapFS{ "content/pages/real.md": {Data: []byte("---\ntitle: Real\n---\n")}, "content/pages/a.md": {Data: []byte("---\ntitle: A\naliases: [pages/real, pages/shared]\n---\n")}, "content/pages/b.md": {Data: []byte("---\ntitle: B\naliases: [pages/shared]\n---\n")}, } site := NewSite(mustScan(t, fsys)) if _, ok := site.Alias("pages/real"); ok { t.Error("an alias naming a real bundle must be dropped, not shadow it") } if _, ok := site.Alias("pages/shared"); ok { t.Error("an alias claimed by two bundles must be dropped, not picked arbitrarily") } if _, _, ok := site.Lookup("pages/real", "en"); !ok { t.Error("the real bundle must keep its URL") } } func mustScan(t *testing.T, fsys fstest.MapFS) []Bundle { t.Helper() b, err := Scan(fsys) if err != nil { t.Fatal(err) } return b }