package content import ( "testing" "testing/fstest" ) 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 } // seriesFS is a series joined structurally: the landing bundle plus four chapters nested under it, two // carrying order and two not, and one outsider that must never join. func seriesFS() fstest.MapFS { return fstest.MapFS{ "content/comics/the-long-monsoon/_index.md": {Data: []byte("---\ntitle: The Long Monsoon\n---\n")}, "content/comics/the-long-monsoon/first-rain.md": {Data: []byte("---\ntitle: First Rain\norder: 10\n---\n")}, "content/comics/the-long-monsoon/the-flood.md": {Data: []byte("---\ntitle: The Flood\norder: 20\n---\n")}, "content/comics/the-long-monsoon/appendix-a.md": {Data: []byte("---\ntitle: Appendix A\n---\n")}, "content/comics/the-long-monsoon/appendix-b.bn.md": {Data: []byte("---\ntitle: পরিশিষ্ট খ\n---\n")}, "content/comics/the-long-monsoon/appendix-b.md": {Data: []byte("---\ntitle: Appendix B\n---\n")}, "content/comics/elsewhere.md": {Data: []byte("---\ntitle: Elsewhere\n---\n")}, } } func TestSequenceMembershipIsStructural(t *testing.T) { site := NewSite(mustScan(t, seriesFS())) seq, ok := site.Sequence("comics/the-long-monsoon/the-flood", "en") if !ok { t.Fatal("a bundle nested under a landing bundle is a member (ADR-0033)") } if seq.Series.Title != "The Long Monsoon" { t.Errorf("series = %q, want the enclosing landing bundle", seq.Series.Title) } var titles []string for _, m := range seq.Members { titles = append(titles, m.Title) } want := []string{"First Rain", "The Flood", "Appendix A", "Appendix B"} if len(titles) != len(want) { t.Fatalf("members = %v, want %v", titles, want) } for i := range want { if titles[i] != want[i] { t.Fatalf("members = %v, want %v — ordered first, then by name", titles, want) } } if seq.Index != 2 { t.Errorf("index = %d, want 2", seq.Index) } if _, ok := site.Sequence("comics/elsewhere", "en"); ok { t.Error("a bundle beside the series is not in it") } } func TestSequenceOnTheLandingPageListsItsMembers(t *testing.T) { site := NewSite(mustScan(t, seriesFS())) seq, ok := site.Sequence("comics/the-long-monsoon", "en") if !ok { t.Fatal("a bundle with bundles nested under it is a series landing page") } if seq.Index != 0 { t.Errorf("index = %d, want 0: the landing page holds no position in its own series", seq.Index) } if len(seq.Members) != 4 { t.Errorf("members = %d, want 4", len(seq.Members)) } } func TestSequenceMembersFollowTheLanguageFallback(t *testing.T) { site := NewSite(mustScan(t, seriesFS())) seq, ok := site.Sequence("comics/the-long-monsoon/the-flood", "bn") if !ok { t.Fatal("the series must resolve in every language") } if len(seq.Members) != 4 { t.Fatalf("members = %d, want 4: a chapter missing in Bengali still holds its place", len(seq.Members)) } if got := seq.Members[3].Title; got != "পরিশিষ্ট খ" { t.Errorf("last member = %q, want the Bengali variant where one exists", got) } if got := seq.Members[1].Title; got != "The Flood" { t.Errorf("member without a Bengali variant = %q, want the English fallback", got) } } func TestNestedSeriesReportsItsOwnMembers(t *testing.T) { fsys := seriesFS() fsys["content/comics/_index.md"] = &fstest.MapFile{Data: []byte("---\ntitle: Comics\n---\n")} site := NewSite(mustScan(t, fsys)) seq, ok := site.Sequence("comics/the-long-monsoon", "en") if !ok { t.Fatal("still a series") } if len(seq.Members) != 4 || seq.Series.Title != "The Long Monsoon" { t.Errorf("a landing page inside another series reports its own members, got %d under %q", len(seq.Members), seq.Series.Title) } // The outsider's nearest bundle ancestor is now the section landing, so it joins that sequence. seq, ok = site.Sequence("comics/elsewhere", "en") if !ok || seq.Series.Title != "Comics" { t.Errorf("nearest ancestor should win, got %q ok=%v", seq.Series.Title, ok) } } func TestQueryFiltersByTagAndSection(t *testing.T) { fsys := fstest.MapFS{ "content/posts/a.md": {Data: []byte("---\ntitle: A\ndate: 2026-01-03\ntags: [Monsoon, prose]\n---\n")}, "content/posts/b.md": {Data: []byte("---\ntitle: B\ndate: 2026-01-02\ntags: [prose]\n---\n")}, "content/comics/c.md": {Data: []byte("---\ntitle: C\ndate: 2026-01-01\ntags: [monsoon]\n---\n")}, "content/writing/d.md": {Data: []byte("---\ntitle: D\n---\n")}, } site := NewSite(mustScan(t, fsys)) got := func(q Query) []string { var titles []string for _, b := range site.Run(q) { titles = append(titles, b.Title) } return titles } if titles := got(Query{Tag: "monsoon", Lang: "en"}); len(titles) != 2 || titles[0] != "A" || titles[1] != "C" { t.Errorf("global tag query = %v, want [A C] — a tag spans sections and case does not matter", titles) } if titles := got(Query{Section: "posts", Tag: "monsoon", Lang: "en"}); len(titles) != 1 || titles[0] != "A" { t.Errorf("section-narrowed tag query = %v, want [A]", titles) } if titles := got(Query{Tag: "nothing", Lang: "en"}); titles != nil { t.Errorf("unknown tag = %v, want none", titles) } }