- contract package: Filter, Listing and their types (D-95)
- golem15_sitemap_urls slot table and the new settings columns
- Rebuild over signpost Records, stable slot assignment, rebuild job
- GET /sitemap.xml index and GET /_sitemap/files/{file} from the table
158 lines
4.1 KiB
Go
158 lines
4.1 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.
|
|
type fakeRecord struct {
|
|
ID int
|
|
Published bool
|
|
LastMod time.Time
|
|
Locales []pact.ContentLocale
|
|
}
|
|
|
|
// 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),
|
|
})
|
|
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}}}
|
|
}
|