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