- 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
206 lines
9.7 KiB
Go
206 lines
9.7 KiB
Go
package crawl
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
func okPage(key string, facts *PageFacts) *Page {
|
|
return &Page{Key: key, Resp: &Response{Status: 200, ContentType: "text/html"}, Facts: facts}
|
|
}
|
|
|
|
func kinds(fs []Finding) map[string]Finding {
|
|
out := map[string]Finding{}
|
|
for _, f := range fs {
|
|
out[f.Kind] = f
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestSeverityAndKinds pins the kinds table: every listed kind has a
|
|
// severity, an unknown kind is a warning, and each kind is listed once.
|
|
func TestSeverityAndKinds(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for _, k := range Kinds() {
|
|
if seen[k] {
|
|
t.Fatalf("%s is listed twice", k)
|
|
}
|
|
seen[k] = true
|
|
if _, ok := severities[k]; !ok {
|
|
t.Errorf("%s has no severity", k)
|
|
}
|
|
}
|
|
if len(seen) != len(severities) {
|
|
t.Fatalf("%d kinds listed, %d severities", len(seen), len(severities))
|
|
}
|
|
if Severity("nope") != models.SeverityWarning || Severity(KindPageStatus) != models.SeverityError || Severity(KindSitemapMissing) != models.SeverityNotice {
|
|
t.Fatal("Severity is wrong")
|
|
}
|
|
}
|
|
|
|
// TestCheckStatus pins page_status: a fetch error (not a cancel) and an
|
|
// error status other than 404 and 410; nothing for 200, 301, 404, 410.
|
|
func TestCheckStatus(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
resp *Response
|
|
want bool
|
|
}{
|
|
{&Response{Err: "timeout"}, true}, {&Response{Err: "canceled"}, false}, {&Response{Status: 500}, true}, {&Response{Status: 403}, true},
|
|
{&Response{Status: 404}, false}, {&Response{Status: 410}, false}, {&Response{Status: 200}, false}, {&Response{Status: 301}, false}, {nil, false},
|
|
} {
|
|
got := checkStatus(&Result{}, &Page{Key: "/a", Resp: tc.resp}, nil)
|
|
if (len(got) == 1) != tc.want || (tc.want && (got[0].Kind != KindPageStatus || got[0].Tool != ToolCreateRedirect)) {
|
|
t.Errorf("%+v: %+v", tc.resp, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCheckHead pins missing title, description and canonical (positive)
|
|
// and their absence on a complete head (negative), and the H1/title
|
|
// mismatch only when neither contains the other.
|
|
func TestCheckHead(t *testing.T) {
|
|
bad := kinds(checkHead(&Result{}, okPage("/a", &PageFacts{Headings: []Heading{{1, "Kanię"}}, Title: "Grzyby"}), nil))
|
|
for _, k := range []string{KindMissingDesc, KindCanonicalMissing, KindH1TitleMismatch} {
|
|
if _, ok := bad[k]; !ok {
|
|
t.Errorf("missing %s in %v", k, bad)
|
|
}
|
|
}
|
|
if bad[KindMissingDesc].Tool != ToolSetURLSEO {
|
|
t.Fatal("a missing description is fixed with seo_set_url_seo")
|
|
}
|
|
if _, ok := kinds(checkHead(&Result{}, okPage("/a", &PageFacts{}), nil))[KindMissingTitle]; !ok {
|
|
t.Fatal("no missing_title for an empty title")
|
|
}
|
|
good := checkHead(&Result{}, okPage("/a", &PageFacts{Title: "Grzyby lecznicze | Acme", Description: "d", Canonical: "https://example.test/a", Headings: []Heading{{1, "Grzyby LECZNICZE"}}}), nil)
|
|
if len(good) != 0 {
|
|
t.Fatalf("a complete head: %+v", good)
|
|
}
|
|
}
|
|
|
|
// TestCheckHeadings pins h1_missing, h1_multiple, heading_skipped (first
|
|
// skip with every skip counted) and heading_empty, and the fitting tool:
|
|
// an H1 in the record's content routes to the headings tool, a skipped
|
|
// level the record holds to the edit tool, a template heading to code.
|
|
func TestCheckHeadings(t *testing.T) {
|
|
rec := &Record{Headings: []pact.ContentHeading{{Level: 1, Text: "In body"}, {Level: 4, Text: "Deep"}}, Tools: map[string]string{pact.ContentToolHeadings: "journal_fix_headings", pact.ContentToolEdit: "journal_update_post"}}
|
|
got := kinds(checkHeadings(okPage("/a", &PageFacts{Headings: []Heading{{1, "Title"}, {1, "In body"}, {2, "A"}, {4, "Deep"}, {2, "B"}, {5, "Deeper"}, {3, ""}}}), rec))
|
|
multi, skip, empty := got[KindH1Multiple], got[KindHeadingSkipped], got[KindHeadingEmpty]
|
|
if multi.Details["count"] != 2 || len(multi.Content) != 2 || multi.Content[0] != pact.ContentToolHeadings {
|
|
t.Fatalf("h1_multiple = %+v", multi)
|
|
}
|
|
if fix, tool, _ := Route(multi); fix != models.FixTool || tool != "journal_fix_headings" {
|
|
t.Fatalf("h1_multiple routes to %s %s, want the headings tool", fix, tool)
|
|
}
|
|
if skip.Details["from"] != 2 || skip.Details["to"] != 4 || skip.Details["count"] != 2 || len(skip.Content) != 1 || skip.Content[0] != pact.ContentToolEdit {
|
|
t.Fatalf("heading_skipped = %+v", skip)
|
|
}
|
|
if fix, tool, _ := Route(skip); fix != models.FixTool || tool != "journal_update_post" {
|
|
t.Fatalf("heading_skipped routes to %s %s, want the edit tool", fix, tool)
|
|
}
|
|
if empty.Details["level"] != 3 || empty.Details["count"] != 1 || len(empty.Content) != 0 {
|
|
t.Fatalf("heading_empty = %+v", empty)
|
|
}
|
|
if fix, _, what := Route(empty); fix != models.FixCode || what == "" {
|
|
t.Fatalf("a template heading routes to code, got %s %q", fix, what)
|
|
}
|
|
// A category's skipped level the record does not hold is fixed in code.
|
|
cat := &Record{Tools: map[string]string{pact.ContentToolEdit: "journal_update_category"}}
|
|
skipCat := kinds(checkHeadings(okPage("/c", &PageFacts{Headings: []Heading{{1, "Cat"}, {3, "Post card"}}}), cat))[KindHeadingSkipped]
|
|
if fix, _, what := Route(skipCat); fix != models.FixCode || what != defaultWhat[KindHeadingSkipped] {
|
|
t.Fatalf("category skip routes to %s %q, want code", fix, what)
|
|
}
|
|
if _, ok := kinds(checkHeadings(okPage("/b", &PageFacts{}), nil))[KindH1Missing]; !ok {
|
|
t.Fatal("no h1_missing")
|
|
}
|
|
if clean := checkHeadings(okPage("/b", &PageFacts{Headings: []Heading{{1, "T"}, {2, "a"}, {3, "b"}, {2, "c"}}}), nil); len(clean) != 0 {
|
|
t.Fatalf("clean headings: %+v", clean)
|
|
}
|
|
}
|
|
|
|
// TestCheckJSONLD pins jsonld_invalid (the custom tool when the record
|
|
// stores custom JSON-LD), jsonld_multiple, jsonld_required and
|
|
// jsonld_recommended (the settings tool for a publisher node), and nothing
|
|
// for a valid single graph.
|
|
func TestCheckJSONLD(t *testing.T) {
|
|
got := kinds(checkJSONLD(okPage("/a", &PageFacts{JSONLD: []string{`{"@type":`, `{"@graph":[{"@type":"BlogPosting","headline":"h"},{"@type":"Organization"}]}`}}), &Record{CustomJSONLD: true}))
|
|
if got[KindJSONLDInvalid].Tool != ToolSetURLSEO || got[KindJSONLDMultiple].Details["count"] != 2 {
|
|
t.Fatalf("invalid/multiple = %+v", got)
|
|
}
|
|
if rec := got[KindJSONLDRecommended]; rec.Details["type"] != "BlogPosting" || rec.Tool != ToolSetURLSEO {
|
|
t.Fatalf("recommended = %+v", rec)
|
|
}
|
|
pub := kinds(checkJSONLD(okPage("/a", &PageFacts{JSONLD: []string{`[{"@type":"Person"}]`}}), nil))[KindJSONLDRequired]
|
|
if pub.Details["property"] != "name" || pub.Tool != ToolUpdateSettings {
|
|
t.Fatalf("publisher required = %+v", pub)
|
|
}
|
|
if none := checkJSONLD(okPage("/a", &PageFacts{JSONLD: []string{`{"@type":"WebSite","name":"n","url":"u"}`}}), nil); len(none) != 0 {
|
|
t.Fatalf("a valid graph: %+v", none)
|
|
}
|
|
if none := checkJSONLD(okPage("/a", &PageFacts{}), nil); none != nil {
|
|
t.Fatal("no scripts, no findings")
|
|
}
|
|
}
|
|
|
|
// TestCheckImages pins image_alt (an empty alt is decorative, not a
|
|
// finding; a record image routes to the image_alt tool), image_size and
|
|
// featured_lazy, and the list cap.
|
|
func TestCheckImages(t *testing.T) {
|
|
facts := &PageFacts{OGImage: "https://example.test/m/cover.jpg", Images: []Image{
|
|
{Src: "https://example.test/m/cover.jpg", Alt: "c", HasAlt: true, Width: "10", Height: "10", Loading: "lazy"},
|
|
{Src: "https://example.test/m/a.jpg", HasAlt: false, Width: "1", Height: "1"},
|
|
{Src: "https://example.test/m/deco.png", Alt: "", HasAlt: true},
|
|
}}
|
|
rec := &Record{Images: []pact.ContentImage{{URL: "/m/a.jpg"}}, Tools: map[string]string{pact.ContentToolImageAlt: "journal_set_image_alt"}}
|
|
got := kinds(checkImages(okPage("/a", facts), rec))
|
|
if alt := got[KindImageAlt]; alt.Details["count"] != 1 || len(alt.Content) != 2 {
|
|
t.Fatalf("image_alt = %+v", alt)
|
|
}
|
|
if _, tool, _ := Route(got[KindImageAlt]); tool != "journal_set_image_alt" {
|
|
t.Fatalf("image_alt routes to %q", tool)
|
|
}
|
|
if got[KindImageSize].Details["count"] != 1 || got[KindFeaturedLazy].Details["image"] != "https://example.test/m/cover.jpg" {
|
|
t.Fatalf("size/lazy = %+v", got)
|
|
}
|
|
many := &PageFacts{}
|
|
for i := 0; i < 15; i++ {
|
|
many.Images = append(many.Images, Image{Src: "/x.jpg", Width: "1", Height: "1"})
|
|
}
|
|
if alt := kinds(checkImages(okPage("/b", many), nil))[KindImageAlt]; alt.Details["count"] != 15 || len(alt.Details["images"].([]string)) != maxListed || len(alt.Content) != 0 {
|
|
t.Fatalf("capped image_alt = %+v", alt)
|
|
}
|
|
}
|
|
|
|
// TestCheckContentCMS pins content_js (check cms): the record title
|
|
// missing from the H1, under half the record's words; nothing when both
|
|
// are met or without a record.
|
|
func TestCheckContentCMS(t *testing.T) {
|
|
words := 100
|
|
rec := &Record{Title: "Chaga", Words: &words}
|
|
got := checkContentCMS(&Result{}, okPage("/a", &PageFacts{Headings: []Heading{{1, "Other"}}, Words: 40}), rec)
|
|
if len(got) != 1 || got[0].Kind != KindContentJS || got[0].Check != checkCMS || got[0].Details["words_cms"] != 100 || got[0].Details["title"] != "Chaga" {
|
|
t.Fatalf("content_js = %+v", got)
|
|
}
|
|
if ok := checkContentCMS(&Result{}, okPage("/a", &PageFacts{Headings: []Heading{{1, "Chaga — grzyb"}}, Words: 50}), rec); ok != nil {
|
|
t.Fatalf("met: %+v", ok)
|
|
}
|
|
if none := checkContentCMS(&Result{}, okPage("/a", &PageFacts{}), nil); none != nil {
|
|
t.Fatal("no record, no finding")
|
|
}
|
|
}
|
|
|
|
// TestEnvNoindex pins the staging lock: on a non-production environment
|
|
// no page counts as noindex; otherwise meta robots and X-Robots-Tag do.
|
|
func TestEnvNoindex(t *testing.T) {
|
|
meta := okPage("/a", &PageFacts{Robots: "noindex"})
|
|
header := &Page{Key: "/b", Resp: &Response{Status: 200, XRobots: "noindex, nofollow"}, Facts: &PageFacts{}}
|
|
if !(&Env{}).noindex(meta) || !(&Env{}).noindex(header) || (&Env{Staging: true}).noindex(meta) || (&Env{}).noindex(okPage("/c", &PageFacts{})) {
|
|
t.Fatal("noindex is wrong")
|
|
}
|
|
var nilEnv *Env
|
|
if nilEnv.record("x") != nil || locale(nil) != "" || locale(&Record{Locale: "pl"}) != "pl" {
|
|
t.Fatal("nil env and record helpers")
|
|
}
|
|
}
|