Pure tests of the classes package (no database): the resolver's value chains with every layer present and absent, robots per directive, the staging lock and drafts; the redirect graph (chains, flattened and back-and-forth graphs, loops by exact, pattern and regex probes, the 10-hop cap); URL normalisation across every address rule; the robots grammar over every directive combination; Google's robots.txt matcher examples; templates and unknown placeholders; CSV export and import; owner resolution; and every kind of the issue report. go.mod: gorm.io/driver/postgres becomes a direct requirement (the tests' dry-run database).
135 lines
4.1 KiB
Go
135 lines
4.1 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
)
|
|
|
|
// testService is a service over an application config without a database:
|
|
// env is SUMMER_ENV, site the golem15.seo.site_url ("" leaves it unset),
|
|
// environ more SUMMER_ variables.
|
|
func testService(t *testing.T, env, site string, environ ...string) *Service {
|
|
t.Helper()
|
|
svc, _ := testApp(t, env, site, environ...)
|
|
return svc
|
|
}
|
|
|
|
func testApp(t *testing.T, env, site string, environ ...string) (*Service, *backpack.App) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: seo-unit\nlocale: en\nfallback_locale: en\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vars := append([]string{"SUMMER_ENV=" + env}, environ...)
|
|
if site != "" {
|
|
vars = append(vars, "SUMMER_GOLEM15__SEO__SITE_URL="+site)
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: vars})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := backpack.New(cfg)
|
|
svc, err := NewService(app)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return svc, app
|
|
}
|
|
|
|
// testPage is a record of the pages content type (/pages/:slug): a path
|
|
// may carry a /pl prefix; a published record is matched exactly on its own
|
|
// slug in the path's locale and inexactly on another of its slugs; a draft
|
|
// is never matched.
|
|
type testPage struct {
|
|
id string
|
|
slugs map[string]string // locale -> slug
|
|
published bool
|
|
}
|
|
|
|
type typesPlugin struct{ types []pact.MenuItemType }
|
|
|
|
func (typesPlugin) ID() string { return "acme.pages" }
|
|
func (p typesPlugin) MenuItemTypes() []pact.MenuItemType { return p.types }
|
|
|
|
func testPagesType(pages ...testPage) pact.MenuItemType {
|
|
find := func(slug string) (testPage, string, bool) {
|
|
for _, p := range pages {
|
|
for locale, s := range p.slugs {
|
|
if s == slug {
|
|
return p, locale, true
|
|
}
|
|
}
|
|
}
|
|
return testPage{}, "", false
|
|
}
|
|
return pact.MenuItemType{
|
|
Key: "acme-page", DefaultPattern: "/pages/:slug",
|
|
Resolve: func(context.Context, pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) { return nil, nil },
|
|
Content: &pact.ContentDescriptor{
|
|
Model: "acme_pages", Controller: "acme.pages.pages",
|
|
Describe: func(_ context.Context, in pact.ContentDescribe) (*pact.ContentRecord, error) {
|
|
for _, p := range pages {
|
|
if p.id == in.ID {
|
|
rec := &pact.ContentRecord{ID: p.id, Published: p.published}
|
|
for locale, slug := range p.slugs {
|
|
rec.Locales = append(rec.Locales, pact.ContentLocale{Locale: locale, URL: "/pages/" + slug, Title: "Page " + p.id})
|
|
}
|
|
return rec, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
},
|
|
Match: func(_ context.Context, in pact.ContentMatch) (*pact.ContentMatchResult, error) {
|
|
locale, path := "en", in.Path
|
|
if rest, ok := strings.CutPrefix(path, "/pl/"); ok {
|
|
locale, path = "pl", "/"+rest
|
|
}
|
|
values, ok := signpost.MatchPattern(in.Pattern, path)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
p, own, ok := find(values["slug"])
|
|
if !ok || !p.published {
|
|
return nil, nil
|
|
}
|
|
return &pact.ContentMatchResult{ID: p.id, Locale: locale, Exact: own == locale}, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// testServiceWithPages is testService with the pages content type
|
|
// registered in signpost.
|
|
func testServiceWithPages(t *testing.T, site string, pages ...testPage) *Service {
|
|
t.Helper()
|
|
svc, app := testApp(t, "production", site)
|
|
if err := signpost.Activate(app, []typesPlugin{{types: []pact.MenuItemType{testPagesType(pages...)}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// dryDB is a Postgres-dialect GORM handle that never connects: every query
|
|
// is built and not run, so reads find no rows.
|
|
func dryDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(postgres.New(postgres.Config{DSN: "host=127.0.0.1 port=1 user=seo dbname=seo sslmode=disable"}),
|
|
&gorm.Config{DryRun: true, DisableAutomaticPing: true, Logger: logger.Discard})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|