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:
148
classes/schema/custom_test.go
Normal file
148
classes/schema/custom_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user