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: "
Short & sweet
", Content: "x", ContentHTML: "One two three
", 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) } } if err := translate.SetTranslated(ctx, gdb, &s.news, "slug", "pl", "wiadomosci"); 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) } 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" { 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/