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

View File

@@ -0,0 +1,86 @@
package schema
import (
"strings"
"testing"
)
func findings(r Report) string {
var out []string
for _, f := range r.Findings {
out = append(out, f.Level+":"+f.Property)
}
return strings.Join(out, " ")
}
// TestValidateNodeRules pins every rule of the table: the required and
// recommended properties per type, the LocalBusiness subtypes checked as
// LocalBusiness, and a type without Google rules reported as Rules false
// with no findings (RESEARCH Pitfall 11).
func TestValidateNodeRules(t *testing.T) {
for _, tc := range []struct {
node map[string]any
rules bool
want string
}{
{map[string]any{"@type": TypeBlogPosting}, true, "recommended:author recommended:datePublished recommended:dateModified recommended:headline recommended:image"},
{map[string]any{"@type": TypeArticle, "headline": "x", "author": []any{map[string]any{"name": "a"}}, "datePublished": "d", "dateModified": "d", "image": []any{"i"}}, true, ""},
{map[string]any{"@type": TypeNewsArticle, "headline": " "}, true, "recommended:author recommended:datePublished recommended:dateModified recommended:headline recommended:image"},
{map[string]any{"@type": TypeBreadcrumbList, "itemListElement": []any{}}, true, "required:itemListElement"},
{map[string]any{"@type": TypeVideoObject, "name": "v"}, true, "required:thumbnailUrl required:uploadDate recommended:description recommended:embedUrl"},
{map[string]any{"@type": TypeOrganization, "name": "o", "url": "u", "logo": map[string]any{"url": "l"}}, true, ""},
{map[string]any{"@type": TypePerson}, true, "required:name recommended:url recommended:image"},
{map[string]any{"@type": TypeWebSite, "url": "u"}, true, "required:name"},
{map[string]any{"@type": TypeProfilePage, "mainEntity": map[string]any{}}, true, "required:mainEntity"},
{map[string]any{"@type": "Store", "name": "s"}, true, "required:address recommended:telephone recommended:url recommended:geo recommended:openingHoursSpecification recommended:priceRange recommended:image"},
{map[string]any{"@type": "FinancialService", "name": "s", "address": map[string]any{"a": 1}, "telephone": "1", "url": "u", "geo": map[string]any{"x": 1}, "openingHoursSpecification": []any{1}, "priceRange": "$", "image": "i"}, true, ""},
{map[string]any{"@type": TypeWebPage}, false, ""},
{map[string]any{"@type": TypeService}, false, ""},
{map[string]any{"@type": "FAQPage"}, false, ""},
{map[string]any{"@type": TypeCollectionPage}, false, ""},
} {
r := ValidateNode(tc.node)
if r.Rules != tc.rules || findings(r) != tc.want || r.Findings == nil {
t.Errorf("%v: rules=%v findings=%q, want rules=%v %q", tc.node["@type"], r.Rules, findings(r), tc.rules, tc.want)
}
}
}
// TestNodeType pins the first @type of a node in each JSON form.
func TestNodeType(t *testing.T) {
for _, tc := range []struct {
node map[string]any
want string
}{
{map[string]any{"@type": "Article"}, "Article"},
{map[string]any{"@type": []any{1, "Store", "LocalBusiness"}}, "Store"},
{map[string]any{"@type": []string{"Person"}}, "Person"},
{map[string]any{"@type": []string{}}, ""},
{map[string]any{}, ""},
{map[string]any{"@type": 5}, ""},
} {
if got := NodeType(tc.node); got != tc.want {
t.Errorf("NodeType(%v) = %q, want %q", tc.node, got, tc.want)
}
}
}
// TestValidateGraph pins Validate over a graph: one report per node with
// its @id, in order, and Nodes reading both graph forms and skipping
// non-objects.
func TestValidateGraph(t *testing.T) {
g := map[string]any{"@graph": []any{map[string]any{"@type": TypeWebSite, "@id": "w", "name": "n", "url": "u"}, "junk", map[string]any{"@type": TypePerson, "@id": "p"}}}
reps := Validate(g)
if len(reps) != 2 || reps[0].ID != "w" || len(reps[0].Findings) != 0 || reps[1].ID != "p" || findings(reps[1]) != "required:name recommended:url recommended:image" {
t.Fatalf("reports = %+v", reps)
}
if len(Nodes(map[string]any{"@graph": []map[string]any{{"@id": "a"}}})) != 1 || Nodes(map[string]any{}) != nil {
t.Fatal("Nodes is wrong")
}
if got := Validate(map[string]any{}); got == nil || len(got) != 0 {
t.Fatalf("empty graph = %v, want []", got)
}
if r, rec, ok := RulesFor("Unknown"); ok || r != nil || rec != nil {
t.Fatal("an unknown type has no rules")
}
}