test: unit tests for the schema builder, crawler and page helpers

- schema: types per kind, @id links, every node kind, the publisher and
  local business, every validator rule, custom JSON-LD caps and merge
- crawl: HTML parse, each check's positive and negative case, fix routes
  with the fitting tool, the rendered comparison, allowRequest,
  resolverRules and NewRenderer
- pagination, alternates and x-default, primary parents, the filter's
  addresses, registry paths and fields, auto-link addresses
This commit is contained in:
Jakub Zych
2026-11-04 19:43:31 +01:00
parent dfc85bc805
commit 82b0dacff9
14 changed files with 1543 additions and 0 deletions

110
classes/registry_test.go Normal file
View File

@@ -0,0 +1,110 @@
package classes
import (
"strings"
"testing"
"time"
"git.golem15.com/golem15/sm-seo-plugin/models"
)
// TestValidPagePath pins the registry address rule: a path from one /,
// no domain, query, fragment, backslash or space, valid UTF-8 without
// control characters, at most MaxRouteBytes.
func TestValidPagePath(t *testing.T) {
for p, want := range map[string]bool{
"/": true, "/kontakt": true, "/trwają-prace": true, "/a/b/c": true, "/" + strings.Repeat("a", MaxRouteBytes-1): true,
"": false, "kontakt": false, "//evil.test": false, "/a?b": false, "/a#b": false, `/a\b`: false, "/a b": false,
"/a\x01": false, "/\xff": false, "https://example.test/a": false, "/" + strings.Repeat("a", MaxRouteBytes): false,
} {
if got := ValidPagePath(p); got != want {
t.Errorf("ValidPagePath(%q) = %v, want %v", p, got, want)
}
}
}
// TestNormalPagePath pins the site's address rules on a registry path: the
// path trimmed, lower-cased and slashed as the settings say, an invalid
// path returned trimmed.
func TestNormalPagePath(t *testing.T) {
if got := normalPagePath(" /Kontakt/ ", NormalizeOptions{Lowercase: true, Slash: "remove"}); got != "/kontakt" {
t.Fatalf("lowercase, no slash = %q", got)
}
if got := normalPagePath("/Kontakt", NormalizeOptions{}); got != "/Kontakt" {
t.Fatalf("no rules = %q", got)
}
if got := normalPagePath(" kontakt ", NormalizeOptions{Lowercase: true}); got != "kontakt" {
t.Fatalf("invalid path = %q", got)
}
}
// TestApplyPageFields pins the scalar fields of a page write: kind and
// note trimmed, the parent set and cleared, the content date parsed,
// cleared by "" and refused when invalid; unset fields stay.
func TestApplyPageFields(t *testing.T) {
kind, note, date, parent := " offer ", " n ", "2026-11-04", uint(7)
p := &models.Page{Kind: models.PageKindPage}
if problems := ApplyPageFields(p, PageWrite{Kind: &kind, Note: &note, ContentUpdatedAt: &date, ParentSet: true, ParentID: &parent}); len(problems) != 0 {
t.Fatalf("problems = %v", problems)
}
if p.Kind != "offer" || p.Note != "n" || p.ContentUpdatedAt == nil || p.ContentUpdatedAt.String() != "2026-11-04" || p.ParentID == nil || uint(*p.ParentID) != 7 {
t.Fatalf("page = %+v", p)
}
empty, bad, zero := "", "04.11.2026", uint(0)
if problems := ApplyPageFields(p, PageWrite{ContentUpdatedAt: &empty, ParentSet: true, ParentID: &zero}); len(problems) != 0 || p.ContentUpdatedAt != nil || p.ParentID != nil || p.Kind != "offer" {
t.Fatalf("cleared = %+v %v", p, problems)
}
if problems := ApplyPageFields(p, PageWrite{ContentUpdatedAt: &bad}); len(problems["content_updated_at"]) != 1 {
t.Fatalf("an invalid date: %v", problems)
}
}
// TestPageGroupAndMissing pins the sitemap group per kind (unknown kinds
// with the pages) and the Not on the site flag: a reported page the
// newest report did not name; never a manual page or before any report.
func TestPageGroupAndMissing(t *testing.T) {
for kind, want := range map[string]string{models.PageKindPage: "pages", models.PageKindOffer: "offers", models.PageKindService: "services", models.PageKindLanding: "landing-pages", "other": "pages"} {
if got := PageGroup(kind); got != want {
t.Errorf("PageGroup(%q) = %q, want %q", kind, got, want)
}
}
now := time.Now().UTC()
older := now.Add(-time.Hour)
for _, tc := range []struct {
p models.Page
reported *time.Time
want bool
}{
{models.Page{Origin: models.PageOriginReported, ReportedAt: &older}, &now, true},
{models.Page{Origin: models.PageOriginReported}, &now, true},
{models.Page{Origin: models.PageOriginReported, ReportedAt: &now}, &now, false},
{models.Page{Origin: models.PageOriginManual, ReportedAt: &older}, &now, false},
{models.Page{Origin: models.PageOriginReported, ReportedAt: &older}, nil, false},
} {
if got := PageMissing(tc.p, tc.reported); got != tc.want {
t.Errorf("PageMissing(%+v) = %v, want %v", tc.p, got, tc.want)
}
}
}
// TestPageBeforeSaveUTC pins the registry page's timestamps in UTC
// whatever the process's zone: a new row gets both, a saved row keeps its
// created_at.
func TestPageBeforeSaveUTC(t *testing.T) {
local := time.Local
time.Local = time.FixedZone("UTC+3", 3*3600)
defer func() { time.Local = local }()
p := &models.Page{}
if err := p.BeforeSave(nil); err != nil {
t.Fatal(err)
}
if p.CreatedAt == nil || p.UpdatedAt == nil || p.CreatedAt.Location() != time.UTC || p.UpdatedAt.Location() != time.UTC {
t.Fatalf("stamps = %v %v", p.CreatedAt, p.UpdatedAt)
}
created := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
p.CreatedAt = &created
_ = p.BeforeSave(nil)
if !p.CreatedAt.Equal(created) || !p.UpdatedAt.After(created) {
t.Fatalf("a saved row = %v %v", p.CreatedAt, p.UpdatedAt)
}
}