Files
sm-seo-plugin/classes/helpers_test.go
Jakub Zych 9e45ddc848 test: unit tests for the SEO classes
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).
2026-10-27 13:21:38 +01:00

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
}