Files
sm-seo-plugin/classes/normalize_test.go
Jakub Zych 9e45ddc848 test: unit tests for the SEO classes
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).
2026-10-27 13:21:38 +01:00

122 lines
4.6 KiB
Go

package classes
import (
"net/url"
"testing"
"git.golem15.com/golem15/sm-seo-plugin/models"
)
// TestNormalizeMatrix: every combination of the address rules on every
// input form reaches its final address in one hop (normalising the result
// changes nothing), never touches the query, keeps the root and does not
// modify its input.
func TestNormalizeMatrix(t *testing.T) {
inputs := []string{
"/", "/a", "/a/", "/A/b", "//a//b/", "/a/index.html", "/A/Index.PHP", "/index.htm",
"/sitemap.xml", "/files/Doc.PDF/", "/%C5%81%C3%B3d%C5%BA/Abc/", "/a?Page=2&x=Y", "/a#frag",
"http://example.test", "http://example.test/A//B/", "https://www.example.test/a/index.html?q=1",
"https://WWW.Example.Test/Blog/", "http://www.example.test/",
}
var all []NormalizeOptions
for _, host := range []string{models.HostKeep, models.HostWWW, models.HostBare} {
for _, slash := range []string{models.SlashKeep, models.SlashAdd, models.SlashDrop} {
for _, lower := range []bool{false, true} {
all = append(all, NormalizeOptions{Host: host, Slash: slash, Lowercase: lower})
}
}
}
for _, opts := range all {
for _, in := range inputs {
u, err := url.Parse(in)
if err != nil {
t.Fatal(err)
}
before := *u
once, _ := Normalize(u, opts)
if *u != before {
t.Fatalf("%+v: Normalize(%s) modified its input", opts, in)
}
twice, changed := Normalize(once, opts)
if changed || twice.String() != once.String() {
t.Fatalf("%+v: %s -> %s -> %s (changed %v): not one hop", opts, in, once, twice, changed)
}
if once.RawQuery != u.RawQuery {
t.Fatalf("%+v: %s: query %q became %q", opts, in, u.RawQuery, once.RawQuery)
}
if once.Fragment != "" {
t.Fatalf("%+v: %s kept its fragment", opts, in)
}
if once.Host != "" && once.Scheme != "https" {
t.Fatalf("%+v: %s scheme %s", opts, in, once.Scheme)
}
if u.Path == "/" && once.Path != "/" {
t.Fatalf("%+v: the root became %s", opts, once.Path)
}
}
}
}
func TestNormalizeMatrixExpectations(t *testing.T) {
for _, tc := range []struct {
in string
opts NormalizeOptions
want string
}{
{"/A//B/index.html", NormalizeOptions{Host: models.HostKeep, Slash: models.SlashKeep}, "/A/B"},
{"/A//B/index.html", NormalizeOptions{Slash: models.SlashAdd, Lowercase: true}, "/a/b/"},
{"/a/b///", NormalizeOptions{Slash: models.SlashDrop}, "/a/b"},
{"/a/b///", NormalizeOptions{Slash: models.SlashKeep}, "/a/b/"},
{"/report.PDF", NormalizeOptions{Slash: models.SlashAdd, Lowercase: true}, "/report.pdf"},
{"/v1.2/page", NormalizeOptions{Slash: models.SlashAdd}, "/v1.2/page/"},
{"http://EXAMPLE.test/a", NormalizeOptions{Host: models.HostKeep}, "https://example.test/a"},
{"http://www.example.test/a/", NormalizeOptions{Host: models.HostBare, Slash: models.SlashDrop}, "https://example.test/a"},
{"https://example.test", NormalizeOptions{Host: models.HostWWW, Slash: models.SlashAdd}, "https://www.example.test/"},
{"/%c5%81/%C5%82X", NormalizeOptions{Lowercase: true}, "/%C5%81/%C5%82x"},
} {
u, _ := url.Parse(tc.in)
got, _ := Normalize(u, tc.opts)
if got.String() != tc.want {
t.Errorf("Normalize(%s, %+v) = %s, want %s", tc.in, tc.opts, got, tc.want)
}
}
if got, changed := Normalize(nil, NormalizeOptions{}); got != nil || changed {
t.Fatal("Normalize(nil) answered a URL")
}
if got := NormalizeOptionsOf(models.Settings{URLHost: models.HostWWW, URLSlash: models.SlashAdd, URLLowercase: true}); got != (NormalizeOptions{Host: models.HostWWW, Slash: models.SlashAdd, Lowercase: true}) {
t.Fatalf("NormalizeOptionsOf = %+v", got)
}
}
func TestNormalizeHelpers(t *testing.T) {
for in, want := range map[string]string{"/": "/", "//": "/", "/a//b": "/a/b", "///a///": "/a/", "/a/b": "/a/b"} {
if got := collapseSlashes(in); got != want {
t.Errorf("collapseSlashes(%q) = %q", in, got)
}
}
if got := lowerASCII("/ŁÓDŹ/ABC-z"); got != "/ŁÓdŹ/abc-z" {
t.Errorf("lowerASCII = %q", got)
}
for in, want := range map[string]string{"/a/b.c": "b.c", "/a/": "", "x": "x", "/": ""} {
if got := lastSegment(in); got != want {
t.Errorf("lastSegment(%q) = %q", in, got)
}
}
for _, tc := range []struct {
site, host string
same bool
}{
{"https://example.test", "example.test", true},
{"https://example.test", "WWW.example.test", true},
{"https://www.example.test", "example.test", true},
{"https://example.test", "evil.test", false},
{"https://example.test", "", false},
{"", "example.test", false},
{"https://example.test", "example.test.evil", false},
} {
if got := sameSite(tc.site, tc.host); got != tc.same {
t.Errorf("sameSite(%q, %q) = %v", tc.site, tc.host, got)
}
}
}