Files
sm-journal-plugin/content_descriptors_test.go
Jakub Zych 0a62090435 fix: content descriptors list only locales with content
The post, category, tag and author descriptors keep one entry per
enabled locale but give a locale a URL only when the record has content
in it: posts and categories by their own translation (a translated slug
alone is no content), tags and authors when one of their published
posts is translated into it, the default locale always. Without content
the entry's URL is "", so hreflang and the sitemaps never advertise an
untranslated locale. The menu item types' Resolve is unchanged.
2026-11-04 23:27:57 +01:00

506 lines
22 KiB
Go

package journal_test
import (
"context"
"strings"
"testing"
"time"
"git.golem15.com/golem15/sm-journal-plugin/classes"
"git.golem15.com/golem15/sm-journal-plugin/models"
translate "git.golem15.com/golem15/sm-translate-plugin/classes"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/signpost"
"gorm.io/gorm"
)
// contentSeed is the records the descriptor tests read: a published post
// with a Polish slug in the news category (Polish slug wiadomosci), a
// draft, and a tag.
type contentSeed struct {
news models.Category
hello models.Post
draft models.Post
tag models.Tag
}
func seedContent(t *testing.T, gdb *gorm.DB) contentSeed {
t.Helper()
ctx := context.Background()
for _, row := range []struct {
code, name string
def bool
order int
}{{"en", "English", true, 1}, {"pl", "Polski", false, 2}} {
if err := gdb.Exec(`INSERT INTO golem15_translate_locales (code, name, is_default, is_enabled, sort_order)
SELECT ?, ?, ?, TRUE, ? WHERE NOT EXISTS (SELECT 1 FROM golem15_translate_locales WHERE code = ?)`,
row.code, row.name, row.def, row.order, row.code).Error; err != nil {
t.Fatal(err)
}
}
var s contentSeed
s.news = menuCategory(t, gdb, "News", "news", nil, 1, 2)
past := time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC)
s.hello = models.Post{Title: "Hello", Slug: "hello", Excerpt: "<p>Short &amp; sweet</p>", Content: "x", ContentHTML: "<p>One two three</p>", Published: true, PublishedAt: &past}
if err := gdb.Create(&s.hello).Error; err != nil {
t.Fatal(err)
}
s.draft = menuPost(t, gdb, "Draft", "draft", false, &past)
attachCategory(t, gdb, s.hello, s.news)
for field, value := range map[string]string{"slug": "witaj", "title": "Witaj"} {
if err := translate.SetTranslated(ctx, gdb, &s.hello, field, "pl", value); err != nil {
t.Fatal(err)
}
}
// The category and the tag have content in pl (P33-64): the category
// its own translated name, the tag through its published post Hello,
// translated into pl.
for field, value := range map[string]string{"slug": "wiadomosci", "name": "Wiadomości"} {
if err := translate.SetTranslated(ctx, gdb, &s.news, field, "pl", value); err != nil {
t.Fatal(err)
}
}
s.tag = models.Tag{Name: "Mushrooms", Slug: "mushrooms"}
if err := gdb.Create(&s.tag).Error; err != nil {
t.Fatal(err)
}
if err := gdb.Create(&models.PostTag{PostID: s.hello.ID, TagID: s.tag.ID}).Error; err != nil {
t.Fatal(err)
}
return s
}
func contentKeys(types []pact.MenuItemType) string {
keys := []string{}
for _, t := range types {
keys = append(keys, t.Key)
}
return strings.Join(keys, " ")
}
func localeURLs(rec *pact.ContentRecord) string {
parts := []string{}
for _, l := range rec.Locales {
parts = append(parts, l.Locale+"="+l.URL)
}
return strings.Join(parts, " ")
}
// matchPost runs journal-post's Match directly with a pattern.
func matchPost(t *testing.T, reg *signpost.Registry, path, pattern string) *pact.ContentMatchResult {
t.Helper()
typ, ok := reg.ContentType(classes.MenuTypePost)
if !ok || typ.Content.Match == nil {
t.Fatal("journal-post has no Match")
}
res, err := typ.Content.Match(context.Background(), pact.ContentMatch{Path: path, Pattern: pattern})
if err != nil {
t.Fatal(err)
}
return res
}
// TestContentDescriptors (D-93, D-11, D-20c): Journal's post and category
// descriptors describe, page and match their records through signpost;
// strict segments make a wrong category, a stale locale prefix or another
// locale's slug inexact, and drafts never match.
func TestContentDescriptors(t *testing.T) {
requirePostgres(t)
ctx := context.Background()
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
s := seedContent(t, gdb)
if got := contentKeys(reg.ContentTypes()); got != "journal-category journal-post journal-tag journal-author" {
t.Fatalf("ContentTypes = %s", got)
}
for controller, key := range map[string]string{
"golem15.journal.posts": classes.MenuTypePost,
"golem15.journal.categories": classes.MenuTypeCategory,
"golem15.journal.tags": classes.MenuTypeTag,
} {
if typ, ok := reg.ContentTypeForController(controller); !ok || typ.Key != key {
t.Fatalf("ContentTypeForController(%s) = %q %v", controller, typ.Key, ok)
}
}
if typ, ok := reg.ContentTypeForModel(models.Category{}.MorphName()); !ok || typ.Key != classes.MenuTypeCategory {
t.Fatalf("category model maps to %q %v", typ.Key, ok)
}
t.Run("describe", func(t *testing.T) {
rec, err := reg.Describe(ctx, classes.MenuTypePost, id(s.hello.ID), "")
if err != nil || rec == nil {
t.Fatalf("Describe post = %+v, %v", rec, err)
}
if got := localeURLs(rec); got != "en=/blog/hello pl=/pl/blog/witaj" {
t.Fatalf("post URLs = %s", got)
}
en := rec.Locales[0]
if !rec.Published || en.Title != "Hello" || rec.Locales[1].Title != "Witaj" || en.Description != "Short & sweet" || en.Words == nil || *en.Words != 3 {
t.Fatalf("post record = %+v, en %+v", rec, en)
}
draft, err := reg.Describe(ctx, classes.MenuTypePost, id(s.draft.ID), "")
if err != nil || draft == nil || draft.Published {
t.Fatalf("Describe draft = %+v, %v", draft, err)
}
cat, err := reg.Describe(ctx, classes.MenuTypeCategory, id(s.news.ID), "")
if err != nil || cat == nil || localeURLs(cat) != "en=/blog/category/news pl=/pl/blog/category/wiadomosci" || !cat.Published {
t.Fatalf("Describe category = %+v, %v", cat, err)
}
if missing, err := reg.Describe(ctx, classes.MenuTypePost, "999999", ""); err != nil || missing != nil {
t.Fatalf("Describe of a missing post = %+v, %v", missing, err)
}
})
t.Run("records", func(t *testing.T) {
page, err := reg.Records(ctx, classes.MenuTypePost, "", 1, "")
if err != nil || len(page.Records) != 1 || page.Records[0].ID != id(s.hello.ID) || page.Next != id(s.hello.ID) {
t.Fatalf("first page = %+v, %v", page, err)
}
page, err = reg.Records(ctx, classes.MenuTypePost, page.Next, 1, "")
if err != nil || len(page.Records) != 1 || page.Records[0].ID != id(s.draft.ID) || page.Next != "" {
t.Fatalf("last page = %+v, %v", page, err)
}
// The migrations seed a default category before news.
cats, err := reg.Records(ctx, classes.MenuTypeCategory, "", 0, "")
if err != nil || len(cats.Records) == 0 || cats.Records[len(cats.Records)-1].ID != id(s.news.ID) || cats.Next != "" {
t.Fatalf("category page = %+v, %v", cats, err)
}
tags, err := reg.Records(ctx, classes.MenuTypeTag, "", 10, "")
if err != nil || len(tags.Records) != 1 || tags.Records[0].ID != id(s.tag.ID) {
t.Fatalf("tag page = %+v, %v", tags, err)
}
})
t.Run("match", func(t *testing.T) {
for _, tc := range []struct {
path, key, locale string
exact bool
}{
{"/blog/hello", classes.MenuTypePost, "en", true},
{"/pl/blog/witaj", classes.MenuTypePost, "pl", true},
{"/pl/blog/hello", classes.MenuTypePost, "pl", false},
{"/blog/witaj", classes.MenuTypePost, "en", false},
{"/en/blog/hello", classes.MenuTypePost, "en", false},
{"/blog/category/news", classes.MenuTypeCategory, "en", true},
{"/pl/blog/category/wiadomosci", classes.MenuTypeCategory, "pl", true},
{"/blog/tag/mushrooms", classes.MenuTypeTag, "en", true},
} {
key, res, err := reg.MatchPath(ctx, tc.path, "")
if err != nil || key != tc.key || res == nil || res.Locale != tc.locale || res.Exact != tc.exact {
t.Fatalf("MatchPath(%s) = %q %+v %v, want %s %s exact=%v", tc.path, key, res, err, tc.key, tc.locale, tc.exact)
}
}
for _, path := range []string{"/blog/draft", "/blog/nothing", "/blog/hello/extra", "/elsewhere"} {
if key, res, err := reg.MatchPath(ctx, path, ""); err != nil || res != nil {
t.Fatalf("MatchPath(%s) = %q %+v %v, want no match", path, key, res, err)
}
}
// Strict segments under a host pattern with the category.
if res := matchPost(t, reg, "/news/hello", "/:category/:slug"); res == nil || !res.Exact || res.ID != id(s.hello.ID) {
t.Fatalf("right category = %+v", res)
}
if res := matchPost(t, reg, "/sport/hello", "/:category/:slug"); res == nil || res.Exact || res.ID != id(s.hello.ID) {
t.Fatalf("wrong category = %+v, want the post, inexact", res)
}
if res := matchPost(t, reg, "/pl/wiadomosci/witaj", "/:category/:slug"); res == nil || !res.Exact || res.Locale != "pl" {
t.Fatalf("Polish category path = %+v", res)
}
if res := matchPost(t, reg, "/blog/"+id(s.hello.ID)+"-x", "/blog/:id-x"); res != nil {
t.Fatalf("a placeholder inside a segment matched: %+v", res)
}
if res := matchPost(t, reg, "/p/"+id(s.hello.ID), "/p/:id"); res == nil || !res.Exact {
t.Fatalf("id pattern = %+v", res)
}
if res := matchPost(t, reg, "/p/"+id(s.draft.ID), "/p/:id"); res != nil {
t.Fatalf("draft by id matched: %+v", res)
}
})
}
// TestJournalTagType (D-102, P-11): journal-tag lists tags as references,
// resolves to /blog/tag/<slug>, and is described and matched; with
// golem15.journal.tag_pages false its URLs are empty and its paths never
// match.
func TestJournalTagType(t *testing.T) {
requirePostgres(t)
ctx := context.Background()
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
s := seedContent(t, gdb)
typ, ok := reg.Type(classes.MenuTypeTag)
if !ok || typ.DefaultPattern != classes.DefaultTagPattern || typ.Label != "golem15.journal::lang.menuitem.journal_tag" || typ.Content == nil {
t.Fatalf("journal-tag = %+v %v", typ, ok)
}
refs, err := signpost.ReferenceOptions(ctx, reg, classes.MenuTypeTag)
if err != nil || !refs.Applies || len(refs.Options) != 1 || refs.Options[0].Value != id(s.tag.ID) || refs.Options[0].Label != "Mushrooms" {
t.Fatalf("tag references = %+v, %v", refs, err)
}
item := resolve(t, reg, classes.MenuTypeTag, id(s.tag.ID), "", false)
if item.URL != "/blog/tag/mushrooms" || item.Title != "Mushrooms" {
t.Fatalf("resolved tag = %+v", item)
}
rec, err := reg.Describe(ctx, classes.MenuTypeTag, id(s.tag.ID), "")
if err != nil || rec == nil || localeURLs(rec) != "en=/blog/tag/mushrooms pl=/pl/blog/tag/mushrooms" {
t.Fatalf("Describe tag = %+v, %v", rec, err)
}
_, offDB, off := bootMenuItemTypes(t, map[string]string{"golem15.yaml": "journal:\n tag_pages: false\n"})
offSeed := seedContent(t, offDB)
rec, err = off.Describe(ctx, classes.MenuTypeTag, id(offSeed.tag.ID), "")
if err != nil || rec == nil || localeURLs(rec) != "en= pl=" {
t.Fatalf("Describe tag without tag pages = %+v, %v", rec, err)
}
if key, res, err := off.MatchPath(ctx, "/blog/tag/mushrooms", ""); err != nil || res != nil {
t.Fatalf("tag path matched without tag pages: %q %+v %v", key, res, err)
}
if got, err := off.Resolve(ctx, classes.MenuTypeTag, id(offSeed.tag.ID), "", false, ""); err != nil || got != nil {
t.Fatalf("tag resolved without tag pages: %+v %v", got, err)
}
}
// TestContentDescriptorsMatrix (D-93, D-20c, D-58): the descriptor matrix
// for posts, categories and tags: Describe per locale with and without an
// unprefixed locale, Match exact and inexact per locale, Records paging,
// host patterns, and unknown or malformed ids.
func TestContentDescriptorsMatrix(t *testing.T) {
requirePostgres(t)
ctx := context.Background()
t.Run("describe and match with Polish unprefixed", func(t *testing.T) {
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
s := seedContent(t, gdb)
for _, tc := range []struct{ key, id, want string }{
{classes.MenuTypePost, id(s.hello.ID), "en=/en/blog/hello pl=/blog/witaj"},
{classes.MenuTypeCategory, id(s.news.ID), "en=/en/blog/category/news pl=/blog/category/wiadomosci"},
{classes.MenuTypeTag, id(s.tag.ID), "en=/en/blog/tag/mushrooms pl=/blog/tag/mushrooms"},
} {
rec, err := reg.Describe(ctx, tc.key, tc.id, "pl")
if err != nil || rec == nil || localeURLs(rec) != tc.want {
t.Fatalf("Describe(%s) = %+v, %v; want %s", tc.key, rec, err, tc.want)
}
}
for _, tc := range []struct {
path, key, locale string
exact bool
}{
{"/blog/witaj", classes.MenuTypePost, "pl", true},
{"/en/blog/hello", classes.MenuTypePost, "en", true},
{"/pl/blog/witaj", classes.MenuTypePost, "pl", false},
{"/blog/hello", classes.MenuTypePost, "pl", false},
{"/blog/category/wiadomosci", classes.MenuTypeCategory, "pl", true},
{"/en/blog/category/news", classes.MenuTypeCategory, "en", true},
{"/blog/category/news", classes.MenuTypeCategory, "pl", false},
{"/blog/tag/mushrooms", classes.MenuTypeTag, "pl", true},
{"/en/blog/tag/mushrooms", classes.MenuTypeTag, "en", true},
} {
key, res, err := reg.MatchPath(ctx, tc.path, "pl")
if err != nil || key != tc.key || res == nil || res.Locale != tc.locale || res.Exact != tc.exact {
t.Fatalf("MatchPath(%s, pl) = %q %+v %v, want %s %s exact=%v", tc.path, key, res, err, tc.key, tc.locale, tc.exact)
}
}
})
t.Run("match per locale without an unprefixed locale", func(t *testing.T) {
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
s := seedContent(t, gdb)
for _, tc := range []struct {
path, key, locale string
exact bool
}{
{"/blog/category/wiadomosci", classes.MenuTypeCategory, "en", false},
{"/pl/blog/category/news", classes.MenuTypeCategory, "pl", false},
{"/en/blog/category/news", classes.MenuTypeCategory, "en", false},
{"/pl/blog/tag/mushrooms", classes.MenuTypeTag, "pl", true},
} {
key, res, err := reg.MatchPath(ctx, tc.path, "")
if err != nil || key != tc.key || res == nil || res.Locale != tc.locale || res.Exact != tc.exact {
t.Fatalf("MatchPath(%s) = %q %+v %v, want %s %s exact=%v", tc.path, key, res, err, tc.key, tc.locale, tc.exact)
}
}
for _, path := range []string{"/pl/blog/draft", "/blog/tag/missing", "/blog/category/missing", "/blog/category/news/extra", "/blog/tag/"} {
if key, res, err := reg.MatchPath(ctx, path, ""); err != nil || res != nil {
t.Fatalf("MatchPath(%s) = %q %+v %v, want no match", path, key, res, err)
}
}
for _, tc := range []struct{ key, id string }{
{classes.MenuTypePost, "999999"}, {classes.MenuTypePost, "abc"},
{classes.MenuTypeCategory, "999999"}, {classes.MenuTypeTag, "999999"}, {classes.MenuTypeTag, ""},
} {
if rec, err := reg.Describe(ctx, tc.key, tc.id, ""); err != nil || rec != nil {
t.Fatalf("Describe(%s, %q) = %+v, %v; want nothing", tc.key, tc.id, rec, err)
}
}
draft, err := reg.Describe(ctx, classes.MenuTypePost, id(s.draft.ID), "")
// The draft has no pl translation (P33-64): no pl address.
if err != nil || draft == nil || draft.Published || localeURLs(draft) != "en=/blog/draft pl=" {
t.Fatalf("Describe draft = %+v, %v", draft, err)
}
})
t.Run("records paging", func(t *testing.T) {
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
s := seedContent(t, gdb)
second := models.Tag{Name: "Forest", Slug: "forest"}
if err := gdb.Create(&second).Error; err != nil {
t.Fatal(err)
}
all, err := reg.Records(ctx, classes.MenuTypePost, "", 0, "")
if err != nil || len(all.Records) != 2 || all.Next != "" {
t.Fatalf("limit 0 = %+v, %v", all, err)
}
if again, err := reg.Records(ctx, classes.MenuTypePost, "not-an-id", 0, ""); err != nil || len(again.Records) != 2 {
t.Fatalf("a malformed cursor = %+v, %v; want the first page", again, err)
}
if end, err := reg.Records(ctx, classes.MenuTypePost, id(s.draft.ID), 10, ""); err != nil || len(end.Records) != 0 || end.Next != "" {
t.Fatalf("after the last post = %+v, %v", end, err)
}
first, err := reg.Records(ctx, classes.MenuTypeTag, "", 1, "")
if err != nil || len(first.Records) != 1 || first.Records[0].ID != id(s.tag.ID) || first.Next != id(s.tag.ID) {
t.Fatalf("first tag page = %+v, %v", first, err)
}
next, err := reg.Records(ctx, classes.MenuTypeTag, first.Next, 1, "")
if err != nil || len(next.Records) != 1 || next.Records[0].ID != id(second.ID) || next.Next != "" {
t.Fatalf("second tag page = %+v, %v", next, err)
}
// Forest has no published post with content in pl (P33-64).
if got := localeURLs(&next.Records[0]); got != "en=/blog/tag/forest pl=" {
t.Fatalf("paged tag URLs = %s", got)
}
if !all.Records[0].Published || all.Records[1].Published {
t.Fatalf("published flags = %v %v", all.Records[0].Published, all.Records[1].Published)
}
})
t.Run("host patterns", func(t *testing.T) {
_, gdb, reg := bootMenuItemTypes(t, map[string]string{"menu_items.yaml": "patterns:\n journal-category: /c/:slug\n journal-tag: /t/:slug\n journal-post: /:category/:slug\n"})
s := seedContent(t, gdb)
for _, tc := range []struct{ key, id, want string }{
{classes.MenuTypeCategory, id(s.news.ID), "en=/c/news pl=/pl/c/wiadomosci"},
{classes.MenuTypeTag, id(s.tag.ID), "en=/t/mushrooms pl=/pl/t/mushrooms"},
{classes.MenuTypePost, id(s.hello.ID), "en=/news/hello pl=/pl/wiadomosci/witaj"},
} {
rec, err := reg.Describe(ctx, tc.key, tc.id, "")
if err != nil || rec == nil || localeURLs(rec) != tc.want {
t.Fatalf("Describe(%s) = %+v, %v; want %s", tc.key, rec, err, tc.want)
}
}
for _, tc := range []struct {
path, key string
exact bool
}{
{"/c/news", classes.MenuTypeCategory, true},
{"/t/mushrooms", classes.MenuTypeTag, true},
{"/news/hello", classes.MenuTypePost, true},
{"/pl/wiadomosci/witaj", classes.MenuTypePost, true},
} {
key, res, err := reg.MatchPath(ctx, tc.path, "")
if err != nil || key != tc.key || res == nil || res.Exact != tc.exact {
t.Fatalf("MatchPath(%s) = %q %+v %v", tc.path, key, res, err)
}
}
if key, res, err := reg.MatchPath(ctx, "/blog/category/news", ""); err != nil || res != nil {
t.Fatalf("the default pattern still matched: %q %+v %v", key, res, err)
}
})
}
// localeTitles lists a record's locales with their URL and title.
func localeTitles(rec *pact.ContentRecord) string {
parts := []string{}
for _, l := range rec.Locales {
parts = append(parts, l.Locale+"="+l.URL+"|"+l.Title)
}
return strings.Join(parts, " ")
}
// TestContentDescriptorsTranslatedLocales pins G3 (D-55, D-57, P33-64): with
// pl as translate's default and en and de enabled, the descriptors give a
// locale a URL only when the record has content in it: a post and a
// category by their own translation, a tag and an author through one of
// their published posts, the default locale always. Every enabled locale
// keeps its entry (URL "" and the default locale's values for one without
// content), and the menu item types' Resolve still lists all three.
func TestContentDescriptorsTranslatedLocales(t *testing.T) {
requirePostgres(t)
ctx := context.Background()
_, gdb, reg := bootMenuItemTypes(t, map[string]string{})
if err := gdb.Exec(`DELETE FROM golem15_translate_locales`).Error; err != nil {
t.Fatal(err)
}
for _, row := range []struct {
code string
def bool
order int
}{{"pl", true, 1}, {"en", false, 2}, {"de", false, 3}} {
if err := gdb.Exec(`INSERT INTO golem15_translate_locales (code, name, is_default, is_enabled, sort_order) VALUES (?, ?, ?, TRUE, ?)`,
row.code, row.code, row.def, row.order).Error; err != nil {
t.Fatal(err)
}
}
past := time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC)
ada := seedAuthor(t, gdb, "ada", "Ada", "Lovelace", "ada")
bob := seedAuthor(t, gdb, "bob", "Bob", "Builder", "bob")
translated := models.Post{Title: "Grzyby", Slug: "grzyby", Content: "Treść", Published: true, PublishedAt: &past, UserID: &ada}
plain := models.Post{Title: "Tylko po polsku", Slug: "tylko", Content: "Treść", Published: true, PublishedAt: &past, UserID: &bob}
for _, p := range []*models.Post{&translated, &plain} {
if err := gdb.Create(p).Error; err != nil {
t.Fatal(err)
}
}
for field, value := range map[string]string{"title": "Mushrooms", "content": "Body", "slug": "mushrooms"} {
if err := translate.SetTranslated(ctx, gdb, &translated, field, "en", value); err != nil {
t.Fatal(err)
}
}
// A slug alone is no content.
if err := translate.SetTranslated(ctx, gdb, &plain, "slug", "de", "nur"); err != nil {
t.Fatal(err)
}
cat := menuCategory(t, gdb, "Leśne", "lesne", nil, 1, 2)
bare := menuCategory(t, gdb, "Puste", "puste", nil, 3, 4)
if err := translate.SetTranslated(ctx, gdb, &cat, "name", "en", "Forest"); err != nil {
t.Fatal(err)
}
attachCategory(t, gdb, translated, cat)
tag := models.Tag{Name: "Las", Slug: "las"}
if err := gdb.Create(&tag).Error; err != nil {
t.Fatal(err)
}
if err := gdb.Create(&models.PostTag{PostID: translated.ID, TagID: tag.ID}).Error; err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
key, id, want string
}{
{classes.MenuTypePost, id(translated.ID), "pl=/blog/grzyby|Grzyby en=/en/blog/mushrooms|Mushrooms de=|Grzyby"},
{classes.MenuTypePost, id(plain.ID), "pl=/blog/tylko|Tylko po polsku en=|Tylko po polsku de=|Tylko po polsku"},
{classes.MenuTypeCategory, id(cat.ID), "pl=/blog/category/lesne|Leśne en=/en/blog/category/lesne|Forest de=|Leśne"},
{classes.MenuTypeCategory, id(bare.ID), "pl=/blog/category/puste|Puste en=|Puste de=|Puste"},
{classes.MenuTypeTag, id(tag.ID), "pl=/blog/tag/las|Las en=/en/blog/tag/las|Las de=|Las"},
{classes.MenuTypeAuthor, id(ada), "pl=/blog/author/ada|Ada Lovelace en=/en/blog/author/ada|Ada Lovelace de=|Ada Lovelace"},
{classes.MenuTypeAuthor, id(bob), "pl=/blog/author/bob|Bob Builder en=|Bob Builder de=|Bob Builder"},
} {
rec := describe(t, reg, tc.key, tc.id)
if got := localeTitles(rec); got != tc.want {
t.Errorf("Describe(%s, %s) = %s\nwant %s", tc.key, tc.id, got, tc.want)
}
}
// The untranslated locale keeps the default locale's word count.
if rec := describe(t, reg, classes.MenuTypePost, id(translated.ID)); rec.Locales[2].Words == nil || *rec.Locales[2].Words != *rec.Locales[0].Words {
t.Errorf("de words = %v, want the pl count", rec.Locales[2].Words)
}
// The menu item types' Resolve is unchanged: every enabled locale.
for key, ref := range map[string]string{classes.MenuTypePost: id(translated.ID), classes.MenuTypeCategory: id(cat.ID)} {
item, err := reg.Resolve(ctx, key, ref, "", false, "")
if err != nil || item == nil || len(item.Alternates) != 3 {
t.Fatalf("Resolve(%s) = %+v, %v", key, item, err)
}
for _, a := range item.Alternates {
if a.URL == "" {
t.Errorf("Resolve(%s) %s has no URL", key, a.Locale)
}
}
}
}