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).
This commit is contained in:
134
classes/helpers_test.go
Normal file
134
classes/helpers_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user