Files
sm-seo-plugin/classes/schema/custom_test.go
Jakub Zych 82b0dacff9 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
2026-11-04 19:43:31 +01:00

149 lines
6.3 KiB
Go

package schema
import (
"encoding/json"
"errors"
"strings"
"testing"
)
func codeOf(err error) string {
var e *Error
if errors.As(err, &e) {
return e.Code
}
return ""
}
// TestDecodeCustom pins the C17 caps: an object or a non-empty list of
// objects, 16 KB, 8 levels, no <script in any case or spacing, the
// parser's line and column for invalid JSON, nothing after the value, and
// @context dropped.
func TestDecodeCustom(t *testing.T) {
nodes, err := DecodeCustom(` {"@context":"https://schema.org","@type":"FAQPage","n":1.50} `)
if err != nil || len(nodes) != 1 || nodes[0]["@context"] != nil || nodes[0]["n"].(json.Number).String() != "1.50" {
t.Fatalf("object = %v err=%v", nodes, err)
}
if nodes, err := DecodeCustom(`[{"@type":"A"},{"@type":"B"}]`); err != nil || len(nodes) != 2 {
t.Fatalf("list = %v err=%v", nodes, err)
}
if nodes, err := DecodeCustom(" "); err != nil || nodes != nil {
t.Fatalf("blank = %v err=%v", nodes, err)
}
deep := strings.Repeat(`{"a":`, 8) + "1" + strings.Repeat("}", 8)
if _, err := DecodeCustom(deep); err != nil {
t.Fatalf("8 levels must pass: %v", err)
}
for name, tc := range map[string]struct{ text, code string }{
"too large": {`{"a":"` + strings.Repeat("x", MaxCustomBytes) + `"}`, CodeTooLarge},
"too deep": {strings.Repeat(`{"a":`, 9) + "1" + strings.Repeat("}", 9), CodeTooDeep},
"script": {`{"a":"</script><script>x"}`, CodeScriptTag},
"script case": {`{"a":"< SCRIPT src=x>"}`, CodeScriptTag},
"string": {`"x"`, CodeNotObject},
"number": {`5`, CodeNotObject},
"empty list": {`[]`, CodeNotObject},
"mixed list": {`[{"a":1},2]`, CodeNotObject},
"syntax": {"{\n \"a\": 1,\n \"b\" 2\n}", CodeInvalidJSON},
"truncated": {`{"a":`, CodeInvalidJSON},
"trailing": {`{"a":1} {"b":2}`, CodeInvalidJSON},
} {
if _, err := DecodeCustom(tc.text); codeOf(err) != tc.code {
t.Errorf("%s: err %v, want code %s", name, err, tc.code)
}
}
_, err = DecodeCustom("{\n \"a\": 1,\n \"b\" 2\n}")
if e := err.(*Error); !strings.HasPrefix(e.Detail, "line 3, column ") {
t.Fatalf("syntax detail = %q, want line 3", e.Detail)
}
if _, err := DecodeCustom(`{"a":`); !strings.Contains(err.Error(), "unexpected end of JSON input") {
t.Fatalf("truncated = %v", err)
}
}
// TestNormalize pins the stored form of the seo_schema value: the type
// checked against the kind, Service fields only for Service (price and
// currency normalised and checked), custom JSON-LD re-serialised, and the
// empty value stored as "".
func TestNormalize(t *testing.T) {
stored, v, err := Normalize(KindPage, VariantOffer, `{"type":"","fields":{"price":" 149,99 ","currency":"pln","areaServed":" Kraków "},"custom":"{\"@type\":\"FAQPage\"}"}`)
if err != nil || v.Fields.Price != "149.99" || v.Fields.Currency != "PLN" || v.Fields.AreaServed != "Kraków" || !strings.Contains(v.Custom, "\n \"@type\": \"FAQPage\"\n") {
t.Fatalf("service value = %+v err=%v", v, err)
}
var back Value
if json.Unmarshal([]byte(stored), &back) != nil || back != v {
t.Fatalf("stored = %s, want the normalised value", stored)
}
if _, v, _ := Normalize(KindArticle, "", `{"type":"Article","fields":{"price":"5"}}`); v.Fields != (ServiceFields{}) || v.Type != TypeArticle {
t.Fatalf("an article keeps no Service fields: %+v", v)
}
if stored, _, err := Normalize(KindPage, "", `{"type":" ","fields":{"price":"5"}}`); err != nil || stored != "" {
t.Fatalf("empty value = %q err=%v", stored, err)
}
if stored, _, err := Normalize(KindPage, "", ""); err != nil || stored != "" {
t.Fatalf("blank = %q err=%v", stored, err)
}
if s, _, _ := Normalize(KindPage, "", `{"custom":"[{\"@type\":\"A\"},{\"@type\":\"B\"}]"}`); !strings.Contains(s, `[\n {`) {
t.Fatalf("a list stays a list: %s", s)
}
for name, tc := range map[string]struct{ kind, variant, raw, code, field string }{
"type": {KindPage, "", `{"type":"BlogPosting"}`, CodeTypeUnknown, ""},
"price": {KindPage, VariantService, `{"fields":{"price":"abc"}}`, CodeFieldInvalid, "price"},
"currency": {KindPage, VariantService, `{"fields":{"currency":"zloty"}}`, CodeFieldInvalid, "currency"},
"json": {KindPage, "", `{"type":`, CodeInvalidJSON, ""},
"custom": {KindPage, "", `{"custom":"<script>"}`, CodeScriptTag, ""},
} {
_, _, err := Normalize(tc.kind, tc.variant, tc.raw)
var e *Error
if !errors.As(err, &e) || e.Code != tc.code || e.Field != tc.field {
t.Errorf("%s: err %v, want %s %s", name, err, tc.code, tc.field)
}
}
}
// TestMerge pins the merge of custom nodes: a node with a generated @id is
// merged into it (custom values win, the replaced properties reported
// sorted), a node without @id or with a new @id is appended, and an equal
// value is not reported.
func TestMerge(t *testing.T) {
g := map[string]any{"@context": Context, "@graph": []any{
map[string]any{"@type": TypeWebPage, "@id": "p#webpage", "name": "Old", "url": "u"},
map[string]any{"@type": TypeWebSite, "@id": "s#website", "name": "Site"},
}}
out, merged := Merge(g, []map[string]any{
{"@id": "p#webpage", "name": "New", "url": "u", "about": "x", "description": "d"},
{"@type": "FAQPage"},
{"@id": "p#faq", "@type": "FAQPage"},
{"@id": "s#website", "name": "Site"},
})
nodes := Nodes(out)
if len(nodes) != 4 || nodes[0]["name"] != "New" || nodes[0]["description"] != "d" || nodes[2]["@type"] != "FAQPage" || nodes[3]["@id"] != "p#faq" {
t.Fatalf("merged graph = %v", nodes)
}
if len(merged) != 1 || merged[0].ID != "p#webpage" || merged[0].Type != TypeWebPage || strings.Join(merged[0].Props, ",") != "name" {
t.Fatalf("merges = %+v", merged)
}
if out["@context"] != Context {
t.Fatal("the merged graph keeps its context")
}
_, none := Merge(g, nil)
if none != nil {
t.Fatalf("no custom nodes reported %v", none)
}
}
// TestParseValue pins the stored value's reading.
func TestParseValue(t *testing.T) {
if v, err := ParseValue(""); err != nil || v != (Value{}) {
t.Fatal("blank is the zero value")
}
if _, err := ParseValue("{"); codeOf(err) != CodeInvalidJSON {
t.Fatalf("invalid = %v", err)
}
if v, _ := ParseValue(`{"type":"Article","custom":"{}"}`); v.Type != "Article" || v.Custom != "{}" {
t.Fatalf("value = %+v", v)
}
if (&Error{Code: "x"}).Error() != "schema: x" || (&Error{Code: "x", Detail: "d"}).Error() != "schema: x: d" {
t.Fatal("Error() is wrong")
}
}