Files
sm-sitemap-plugin/content_fixture_test.go
Jakub Zych 5bf56fde57 feat: image sitemap files (tracer)
- migration 202610280004: image_file_no, kind, published_at and parent_refs
  on the slot table; the image, News, IndexNow and HTML sitemap settings
- the rebuild stores each page's featured and body images (absolute on the
  site, deduplicated, at most 1000) and numbers the rows with images into
  sitemap-images-<n>.xml with the per-type files' stable rules
- the index lists the image files while images_enabled is on; the files
  route serves them with image:image and image:loc only
2026-11-04 12:19:05 +01:00

164 lines
4.4 KiB
Go

package sitemap_test
import (
"context"
"slices"
"strconv"
"sync"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
)
// contentPluginID is a fake content plugin with two content types whose
// records live in memory (contentStore), so a test sets exactly what the
// sitemap reads through signpost Records. It knows nothing about the
// sitemap plugin.
const contentPluginID = "acme.content"
// The fake content types: pages (/pages/<slug>, group pages) and offers
// (/offers/<slug>, group offers).
const (
pageType = "acme-page"
pageModel = "acme_pages"
offerType = "acme-offer"
offerModel = "acme_offers"
)
// fakeRecord is one record of a fake content type. PublishedAt and Parents
// are the optional descriptor facts (the News sitemap, the HTML sitemap's
// tree).
type fakeRecord struct {
ID int
Published bool
LastMod time.Time
Locales []pact.ContentLocale
PublishedAt *time.Time
Parents []pact.ContentRef
}
// store holds the fake records per content type key.
type store struct {
mu sync.Mutex
records map[string][]fakeRecord
}
var contentStore = &store{records: map[string][]fakeRecord{}}
// reset empties the store and clears it again when the test ends.
func (s *store) reset(t *testing.T) {
t.Helper()
s.mu.Lock()
s.records = map[string][]fakeRecord{}
s.mu.Unlock()
t.Cleanup(func() {
s.mu.Lock()
s.records = map[string][]fakeRecord{}
s.mu.Unlock()
})
}
// put adds or replaces a record of typ.
func (s *store) put(typ string, rec fakeRecord) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.records[typ]
for i := range list {
if list[i].ID == rec.ID {
list[i] = rec
return
}
}
s.records[typ] = append(list, rec)
}
// remove deletes a record of typ.
func (s *store) remove(typ string, id int) {
s.mu.Lock()
defer s.mu.Unlock()
s.records[typ] = slices.DeleteFunc(s.records[typ], func(r fakeRecord) bool { return r.ID == id })
}
// page answers one ContentList page of typ, by ascending ID.
func (s *store) page(typ string, in pact.ContentList) pact.ContentPage {
s.mu.Lock()
defer s.mu.Unlock()
list := slices.Clone(s.records[typ])
slices.SortFunc(list, func(a, b fakeRecord) int { return a.ID - b.ID })
after, _ := strconv.Atoi(in.After)
limit := in.Limit
if limit <= 0 {
limit = 100
}
out := pact.ContentPage{Records: []pact.ContentRecord{}}
for _, r := range list {
if r.ID <= after {
continue
}
if len(out.Records) == limit {
out.Next = strconv.Itoa(after)
break
}
mod := r.LastMod
out.Records = append(out.Records, pact.ContentRecord{
ID: strconv.Itoa(r.ID),
Published: r.Published,
LastMod: &mod,
Locales: slices.Clone(r.Locales),
PublishedAt: r.PublishedAt,
Parents: slices.Clone(r.Parents),
})
after = r.ID
}
return out
}
// contentPlugin is acme.content.
type contentPlugin struct{}
func (contentPlugin) ID() string { return contentPluginID }
func (contentPlugin) Requires() []string { return nil }
func (contentPlugin) Register(*backpack.App) error { return nil }
func (contentPlugin) Boot(*backpack.App) error { return nil }
func (contentPlugin) MenuItemTypes() []pact.MenuItemType {
typ := func(key, label, model, pattern string) pact.MenuItemType {
return pact.MenuItemType{
Key: key,
Label: label,
DefaultPattern: pattern,
Placeholders: []string{"slug"},
Resolve: func(context.Context, pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
return nil, nil
},
Content: &pact.ContentDescriptor{
Model: model,
Records: func(_ context.Context, in pact.ContentList) (pact.ContentPage, error) {
return contentStore.page(key, in), nil
},
Describe: func(context.Context, pact.ContentDescribe) (*pact.ContentRecord, error) {
return nil, nil
},
},
}
}
return []pact.MenuItemType{
typ(pageType, "Pages", pageModel, "/pages/:slug"),
typ(offerType, "Offers", offerModel, "/offers/:slug"),
}
}
var _ pact.HasMenuItemTypes = contentPlugin{}
func init() {
party.Register(contentPlugin{})
}
// page1 is a published record in one locale at /pages/<slug>.
func page1(id int, slug string, mod time.Time) fakeRecord {
return fakeRecord{ID: id, Published: true, LastMod: mod, Locales: []pact.ContentLocale{{Locale: "en", URL: "/pages/" + slug, Title: "Page " + slug}}}
}