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") } }