Pure tests of the classes package (no database): the resolver's value chains with every layer present and absent, robots per directive, the staging lock and drafts; the redirect graph (chains, flattened and back-and-forth graphs, loops by exact, pattern and regex probes, the 10-hop cap); URL normalisation across every address rule; the robots grammar over every directive combination; Google's robots.txt matcher examples; templates and unknown placeholders; CSV export and import; owner resolution; and every kind of the issue report. go.mod: gorm.io/driver/postgres becomes a direct requirement (the tests' dry-run database).
150 lines
5.9 KiB
Go
150 lines
5.9 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
)
|
|
|
|
// TestIssues: each kind of the CMS-data issue report from loaded data
|
|
// (the database reads of Issues are covered by TestIssues in the plugin
|
|
// package).
|
|
func TestIssues(t *testing.T) {
|
|
svc := testServiceWithPages(t, "https://example.test",
|
|
testPage{id: "1", slugs: map[string]string{"en": "a"}, published: true},
|
|
testPage{id: "2", slugs: map[string]string{"en": "b"}, published: true},
|
|
)
|
|
entry := func(url, locale, title, desc string) models.Entry {
|
|
return models.Entry{URL: url, Locale: locale, EffectiveTitle: title, EffectiveDescription: desc}
|
|
}
|
|
canonicals := map[string][2]string{
|
|
"/noindexed": {IssueCanonicalNoindex, ""},
|
|
"/moved": {IssueCanonicalRedirected, "/pages/a"},
|
|
}
|
|
canonical := func(_ context.Context, value string) (string, string, error) {
|
|
if value == "/boom" {
|
|
return "", "", errors.New("resolver down")
|
|
}
|
|
got := canonicals[value]
|
|
return got[0], got[1], nil
|
|
}
|
|
data := issueData{
|
|
locale: "en",
|
|
entries: []models.Entry{
|
|
func() models.Entry {
|
|
e := entry("/pages/a", "en", "Same Title", "First")
|
|
e.Canonical = "/moved"
|
|
return e
|
|
}(),
|
|
entry("/pages/b", "en", "same title ", ""),
|
|
entry("/pages/c", "en", "", "Shared description"),
|
|
entry("/pages/c", "pl", "Same Title", "Shared description"),
|
|
entry("/pages/d", "en", "Unique", "shared DESCRIPTION"),
|
|
},
|
|
rules: []models.Rule{
|
|
{ID: 7, URL: "/pages/a", Match: models.MatchExact, Robots: "noindex"},
|
|
{ID: 8, URL: "/landing", Match: models.MatchExact, Title: "Unique", Canonical: "/noindexed"},
|
|
{ID: 9, URL: "/pages/*", Match: models.MatchPattern, Title: "Same Title"},
|
|
{ID: 10, URL: "/pages/b?x=1", Match: models.MatchExact, Title: "Query rule"},
|
|
{ID: 11, URL: "/pages/b", Match: models.MatchExact},
|
|
},
|
|
redirects: []models.Redirect{rd(1, "/old1", "/old2"), rd(2, "/old2", "/pages/a"), rd(3, "/direct", "/pages/a")},
|
|
robots: models.Robots{Content: "User-agent: *\nDisallow: /pages/a\n"},
|
|
noindex: []string{"/pages/a", "/landing"},
|
|
}
|
|
issues, err := svc.issuesOf(t.Context(), data, canonical)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := map[string]Issue{}
|
|
var order []string
|
|
for _, is := range issues {
|
|
key := is.Kind + " " + is.URL + " " + is.Locale
|
|
if _, dup := got[key]; dup {
|
|
t.Fatalf("issue listed twice: %s", key)
|
|
}
|
|
got[key] = is
|
|
order = append(order, key)
|
|
}
|
|
want := []string{
|
|
"canonical_noindex /landing ",
|
|
"canonical_redirected /pages/a en",
|
|
"duplicate_description /pages/c en",
|
|
"duplicate_description /pages/d en",
|
|
"duplicate_title /landing ",
|
|
"duplicate_title /pages/a en",
|
|
"duplicate_title /pages/b en",
|
|
"duplicate_title /pages/d en",
|
|
"missing_description /pages/b en",
|
|
"missing_title /pages/c en",
|
|
"noindex_blocked /pages/a ",
|
|
"redirect_chain /old1 ",
|
|
"rule_overrides_record /pages/a en",
|
|
}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Fatalf("issues (sorted by kind, url, locale):\n%v\nwant\n%v", order, want)
|
|
}
|
|
tools := map[string]string{
|
|
"canonical_noindex /landing ": "seo_update_rule", "canonical_redirected /pages/a en": "seo_set_url_seo",
|
|
"missing_title /pages/c en": "seo_set_url_seo", "noindex_blocked /pages/a ": "seo_update_robots",
|
|
"redirect_chain /old1 ": "seo_update_redirect", "rule_overrides_record /pages/a en": "seo_update_rule",
|
|
}
|
|
for key, tool := range tools {
|
|
if got[key].Tool != tool {
|
|
t.Errorf("%s tool = %s, want %s", key, got[key].Tool, tool)
|
|
}
|
|
}
|
|
if d := got["duplicate_title /pages/a en"].Details; d["value"] != "same title" || fmt.Sprint(d["also"]) != "[/pages/b]" {
|
|
t.Errorf("duplicate_title details = %v", d)
|
|
}
|
|
if d := got["duplicate_title /landing "].Details; fmt.Sprint(d["also"]) != "[/pages/d]" {
|
|
t.Errorf("a rule's title joins the default locale's duplicates: %v", d)
|
|
}
|
|
if d := got["rule_overrides_record /pages/a en"].Details; d["rule_id"] != uint(7) || d["type"] != "acme-page" || d["id"] != "1" || fmt.Sprint(d["fields"]) != "[robots]" {
|
|
t.Errorf("rule_overrides_record details = %v", d)
|
|
}
|
|
if d := got["redirect_chain /old1 "].Details; d["hops"] != 2 || d["final"] != "/pages/a" || fmt.Sprint(d["path"]) != "[/old1 /old2 /pages/a]" {
|
|
t.Errorf("redirect_chain details = %v", d)
|
|
}
|
|
if d := got["canonical_redirected /pages/a en"].Details; d["canonical"] != "/moved" || d["target"] != "/pages/a" {
|
|
t.Errorf("canonical_redirected details = %v", d)
|
|
}
|
|
if d := got["canonical_noindex /landing "].Details; d["rule_id"] != uint(8) || d["canonical"] != "/noindexed" {
|
|
t.Errorf("canonical_noindex details = %v", d)
|
|
}
|
|
|
|
t.Run("a clean site has no issues", func(t *testing.T) {
|
|
clean := issueData{locale: "en", entries: []models.Entry{entry("/pages/a", "en", "A", "About A"), entry("/pages/b", "en", "B", "About B")}}
|
|
if issues, err := svc.issuesOf(t.Context(), clean, canonical); err != nil || len(issues) != 0 {
|
|
t.Fatalf("issues = %+v, %v", issues, err)
|
|
}
|
|
})
|
|
|
|
t.Run("the same text in two locales is not a duplicate", func(t *testing.T) {
|
|
two := issueData{locale: "en", entries: []models.Entry{entry("/pages/a", "en", "Name", "Text"), entry("/pl/pages/a", "pl", "Name", "Text")}}
|
|
if issues, _ := svc.issuesOf(t.Context(), two, canonical); len(issues) != 0 {
|
|
t.Fatalf("issues = %+v", issues)
|
|
}
|
|
})
|
|
|
|
t.Run("a canonical check that fails stops the report", func(t *testing.T) {
|
|
broken := issueData{locale: "en", entries: []models.Entry{func() models.Entry { e := entry("/pages/a", "en", "A", "B"); e.Canonical = "/boom"; return e }()}}
|
|
if _, err := svc.issuesOf(t.Context(), broken, canonical); err == nil {
|
|
t.Fatal("the resolver error was dropped")
|
|
}
|
|
})
|
|
|
|
t.Run("canonicals on other sites and empty canonicals are not checked", func(t *testing.T) {
|
|
for _, value := range []string{"", " ", "https://other.test/a"} {
|
|
kind, target, err := svc.canonicalProblem(t.Context(), value)
|
|
if kind != "" || target != "" || err != nil {
|
|
t.Errorf("canonicalProblem(%q) = %q %q %v", value, kind, target, err)
|
|
}
|
|
}
|
|
})
|
|
}
|