Files
summercms/modules/signpost/content_fields_smoke_test.go
Jakub Zych 561a56c768 feat(signpost): content descriptor fields for dates, parents, authors, media and outline
- pact.ContentDescriptor gains Kind and Tools with the ContentKind and
  ContentTool constants
- pact.ContentRecord gains PublishedAt, Parents, Authors and TopHeading;
  pact.ContentLocale gains Images, Videos and Headings, with the value
  types ContentRef, ContentPerson, ContentImage, ContentVideo and
  ContentHeading; every field is optional
- TestContentDescriptorFields proves signpost hands them through and
  that a type setting none of them describes as before
- the runnable content example, the pact and signpost READMEs and the
  menu item types page document the fields
2026-11-04 08:31:49 +01:00

200 lines
7.2 KiB
Go

package signpost_test
import (
"context"
"reflect"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/pact"
)
// fullRecord is a record that sets every optional descriptor fact: first
// publication, two candidate parents, an author with a page of their own,
// two images (one decorative with an empty alt), a video and a heading
// outline under a level-2 top heading.
func fullRecord(id string) *pact.ContentRecord {
published := time.Date(2026, 10, 1, 9, 30, 0, 0, time.UTC)
changed := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)
words := 420
return &pact.ContentRecord{
ID: id,
Published: true,
LastMod: &changed,
PublishedAt: &published,
Parents: []pact.ContentRef{
{Type: "acme-category", ID: "3"},
{Type: "acme-category", ID: "8"},
},
Authors: []pact.ContentPerson{{Name: "Ada Lovelace", Ref: pact.ContentRef{Type: "acme-author", ID: "12"}}},
TopHeading: 2,
Locales: []pact.ContentLocale{{
Locale: "en",
URL: "/articles/" + id,
Title: "Engines",
Words: &words,
Images: []pact.ContentImage{
{URL: "/media/engine.jpg", Alt: "The analytical engine", Width: 1200, Height: 800},
{URL: "/media/rule.png", Decorative: true},
},
Videos: []pact.ContentVideo{{Provider: "youtube", ID: "abc123"}},
Headings: []pact.ContentHeading{
{Level: 2, Text: "Origins"},
{Level: 3, Text: "The mill"},
{Level: 2, Text: "Legacy"},
},
}},
}
}
// TestContentDescriptorFields: a type that sets the optional descriptor
// facts (Kind, Tools and the record and locale fields) hands every value
// through signpost.Describe and signpost.Records unchanged, and ContentType
// exposes Kind and Tools; a type that sets none of them describes exactly
// as before, with nil and empty slices treated alike.
func TestContentDescriptorFields(t *testing.T) {
full := pact.MenuItemType{
Key: "acme-article",
DefaultPattern: "/articles/:slug",
Resolve: echo,
Content: &pact.ContentDescriptor{
Model: "acme_articles",
Kind: pact.ContentKindArticle,
Tools: map[string]string{pact.ContentToolEdit: "acme_update_page"},
Describe: func(_ context.Context, in pact.ContentDescribe) (*pact.ContentRecord, error) {
return fullRecord(in.ID), nil
},
Records: func(_ context.Context, in pact.ContentList) (pact.ContentPage, error) {
return pact.ContentPage{Records: []pact.ContentRecord{*fullRecord("1")}}, nil
},
},
}
plainRecord := func(id string) *pact.ContentRecord {
return &pact.ContentRecord{ID: id, Published: true, Locales: []pact.ContentLocale{{Locale: "en", URL: "/pages/" + id, Title: "About"}}}
}
plain := pact.MenuItemType{
Key: "acme-page",
DefaultPattern: "/pages/:slug",
Resolve: echo,
Content: &pact.ContentDescriptor{
Model: "acme_pages",
Describe: func(_ context.Context, in pact.ContentDescribe) (*pact.ContentRecord, error) {
return plainRecord(in.ID), nil
},
Records: func(_ context.Context, in pact.ContentList) (pact.ContentPage, error) {
return pact.ContentPage{Records: []pact.ContentRecord{*plainRecord("1")}}, nil
},
},
}
reg := contentRegistry(t, "", full, plain)
ctx := context.Background()
t.Run("full type hands every value through Describe", func(t *testing.T) {
got, err := reg.Describe(ctx, "acme-article", "5", "")
if err != nil {
t.Fatalf("Describe: %v", err)
}
if want := fullRecord("5"); !reflect.DeepEqual(got, want) {
t.Fatalf("Describe = %+v, want %+v", got, want)
}
if got.TopHeading != 2 || len(got.Parents) != 2 || got.Parents[0].ID != "3" {
t.Fatalf("record facts lost: TopHeading %d, Parents %+v", got.TopHeading, got.Parents)
}
if a := got.Authors[0]; a.Name != "Ada Lovelace" || a.Ref != (pact.ContentRef{Type: "acme-author", ID: "12"}) {
t.Fatalf("author = %+v", a)
}
loc := got.Locales[0]
if len(loc.Images) != 2 || !loc.Images[1].Decorative || loc.Images[1].Alt != "" || loc.Images[0].Width != 1200 {
t.Fatalf("images = %+v", loc.Images)
}
if len(loc.Videos) != 1 || loc.Videos[0] != (pact.ContentVideo{Provider: "youtube", ID: "abc123"}) {
t.Fatalf("videos = %+v", loc.Videos)
}
if len(loc.Headings) != 3 || loc.Headings[1] != (pact.ContentHeading{Level: 3, Text: "The mill"}) {
t.Fatalf("headings = %+v", loc.Headings)
}
})
t.Run("full type hands every value through Records", func(t *testing.T) {
page, err := reg.Records(ctx, "acme-article", "", 10, "")
if err != nil {
t.Fatalf("Records: %v", err)
}
if len(page.Records) != 1 || !reflect.DeepEqual(page.Records[0], *fullRecord("1")) {
t.Fatalf("Records = %+v", page.Records)
}
})
t.Run("ContentType exposes Kind and Tools", func(t *testing.T) {
typ, ok := reg.ContentType("acme-article")
if !ok {
t.Fatal("ContentType(acme-article) not found")
}
if typ.Content.Kind != "article" {
t.Fatalf("Kind = %q", typ.Content.Kind)
}
if !reflect.DeepEqual(typ.Content.Tools, map[string]string{"edit": "acme_update_page"}) {
t.Fatalf("Tools = %v", typ.Content.Tools)
}
})
t.Run("type without the facts describes as before", func(t *testing.T) {
got, err := reg.Describe(ctx, "acme-page", "4", "")
if err != nil {
t.Fatalf("Describe: %v", err)
}
if !reflect.DeepEqual(got, plainRecord("4")) {
t.Fatalf("Describe = %+v", got)
}
if got.PublishedAt != nil || got.Parents != nil || got.Authors != nil || got.TopHeading != 0 {
t.Fatalf("plain record gained facts: %+v", got)
}
loc := got.Locales[0]
if loc.Images != nil || loc.Videos != nil || loc.Headings != nil {
t.Fatalf("plain locale gained facts: %+v", loc)
}
page, err := reg.Records(ctx, "acme-page", "", 10, "")
if err != nil || len(page.Records) != 1 || !reflect.DeepEqual(page.Records[0], *plainRecord("1")) {
t.Fatalf("Records = %+v, %v", page, err)
}
typ, _ := reg.ContentType("acme-page")
if typ.Content.Kind != "" || typ.Content.Tools != nil {
t.Fatalf("plain type: Kind %q, Tools %v", typ.Content.Kind, typ.Content.Tools)
}
if k, _ := reg.ContentTypeForModel("acme_pages"); k.Key != "acme-page" {
t.Fatalf("ContentTypeForModel = %q", k.Key)
}
})
t.Run("nil and empty slices are handed through alike", func(t *testing.T) {
empty := pact.MenuItemType{
Key: "acme-note",
DefaultPattern: "/notes/:slug",
Resolve: echo,
Content: &pact.ContentDescriptor{
Model: "acme_notes",
Tools: map[string]string{},
Describe: func(_ context.Context, in pact.ContentDescribe) (*pact.ContentRecord, error) {
return &pact.ContentRecord{
ID: in.ID, Parents: []pact.ContentRef{}, Authors: []pact.ContentPerson{},
Locales: []pact.ContentLocale{{Locale: "en", Images: []pact.ContentImage{}, Videos: []pact.ContentVideo{}, Headings: []pact.ContentHeading{}}},
}, nil
},
},
}
r := contentRegistry(t, "", empty)
got, err := r.Describe(ctx, "acme-note", "1", "")
if err != nil {
t.Fatalf("Describe: %v", err)
}
if len(got.Parents) != 0 || len(got.Authors) != 0 || len(got.Locales[0].Images) != 0 ||
len(got.Locales[0].Videos) != 0 || len(got.Locales[0].Headings) != 0 {
t.Fatalf("empty slices grew: %+v", got)
}
typ, _ := r.ContentType("acme-note")
if len(typ.Content.Tools) != 0 {
t.Fatalf("Tools = %v", typ.Content.Tools)
}
})
}