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).
This commit is contained in:
133
classes/robots_value_test.go
Normal file
133
classes/robots_value_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package classes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// robotsCombinations enumerates every combination of the C5 grammar's
|
||||
// directives (with representative arguments).
|
||||
func robotsCombinations() []Robots {
|
||||
ptr := func(n int) *int { return &n }
|
||||
var out []Robots
|
||||
for _, index := range []string{"", "index", "noindex"} {
|
||||
for _, follow := range []string{"", "follow", "nofollow"} {
|
||||
for flags := 0; flags < 8; flags++ {
|
||||
for _, snippet := range []*int{nil, ptr(-1), ptr(0), ptr(160)} {
|
||||
for _, image := range []string{"", "none", "standard", "large"} {
|
||||
for _, video := range []*int{nil, ptr(-1), ptr(30)} {
|
||||
for _, after := range []string{"", "2027-01-31"} {
|
||||
out = append(out, Robots{
|
||||
Index: index, Follow: follow,
|
||||
NoArchive: flags&1 != 0, NoSnippet: flags&2 != 0, NoImageIndex: flags&4 != 0,
|
||||
MaxSnippet: snippet, MaxImagePreview: image, MaxVideoPreview: video,
|
||||
UnavailableAfter: after,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestRobotsValueRoundTrip(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(32))
|
||||
combos := robotsCombinations()
|
||||
if len(combos) != 3*3*8*4*4*3*2 {
|
||||
t.Fatalf("%d combinations", len(combos))
|
||||
}
|
||||
for _, want := range combos {
|
||||
value := want.String()
|
||||
got, err := ParseRobots(value)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseRobots(%q): %v", value, err)
|
||||
}
|
||||
if got.String() != value {
|
||||
t.Fatalf("round trip %q -> %q", value, got.String())
|
||||
}
|
||||
if got.Header() != strings.ReplaceAll(value, ",", ", ") {
|
||||
t.Fatalf("Header(%q) = %q", value, got.Header())
|
||||
}
|
||||
if got.Empty() != (value == "") {
|
||||
t.Fatalf("Empty(%q) = %v", value, got.Empty())
|
||||
}
|
||||
// Any order, spaces and case read the same value back in the
|
||||
// fixed order.
|
||||
parts := strings.Split(value, ",")
|
||||
rng.Shuffle(len(parts), func(i, j int) { parts[i], parts[j] = parts[j], parts[i] })
|
||||
for i := range parts {
|
||||
if i%2 == 0 {
|
||||
parts[i] = " " + strings.ToUpper(parts[i]) + " "
|
||||
}
|
||||
}
|
||||
if value != "" {
|
||||
norm, err := NormalizeRobots(strings.Join(parts, ","))
|
||||
if err != nil || norm != value {
|
||||
t.Fatalf("NormalizeRobots(%q) = %q, %v; want %q", strings.Join(parts, ","), norm, err, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotsValueAllInheritIsEmpty(t *testing.T) {
|
||||
for _, value := range []string{"", " ", "\t"} {
|
||||
r, err := ParseRobots(value)
|
||||
if err != nil || !r.Empty() || r.String() != "" {
|
||||
t.Fatalf("ParseRobots(%q) = %+v, %v", value, r, err)
|
||||
}
|
||||
}
|
||||
if s := (Robots{}).String(); s != "" {
|
||||
t.Fatalf("all-inherit serialises to %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotsValueMalformed(t *testing.T) {
|
||||
for _, value := range []string{
|
||||
",", "index,", ",noindex", "index,,follow",
|
||||
"index,noindex", "follow,nofollow", "noindex,noindex", "noarchive,noarchive",
|
||||
"max-snippet:1,max-snippet:2",
|
||||
"index:1", "nofollow:yes", "noarchive:1",
|
||||
"max-snippet", "max-snippet:", "max-snippet:abc", "max-snippet:-2", "max-snippet:1.5",
|
||||
"max-video-preview", "max-video-preview:-5",
|
||||
"max-image-preview", "max-image-preview:huge", "max-image-preview:",
|
||||
"unavailable_after", "unavailable_after:2027-13-01", "unavailable_after:2027-02-30",
|
||||
"unavailable_after:27-01-01", "unavailable_after:2027-1-1",
|
||||
"none", "all", "noodp", "indexx",
|
||||
} {
|
||||
if _, err := ParseRobots(value); !errors.Is(err, ErrRobotsValue) {
|
||||
t.Errorf("ParseRobots(%q) = %v, want ErrRobotsValue", value, err)
|
||||
}
|
||||
if _, err := NormalizeRobots(value); err == nil {
|
||||
t.Errorf("NormalizeRobots(%q) accepted", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotsValueInherit(t *testing.T) {
|
||||
parse := func(v string) Robots {
|
||||
t.Helper()
|
||||
r, err := ParseRobots(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
for _, tc := range []struct{ upper, lower, want string }{
|
||||
{"", "", ""},
|
||||
{"noindex", "index,nofollow", "noindex,nofollow"},
|
||||
{"", "noindex,follow", "noindex,follow"},
|
||||
{"index,follow", "noindex,nofollow,noarchive", "index,follow,noarchive"},
|
||||
{"max-snippet:10", "max-snippet:50,max-image-preview:large", "max-snippet:10,max-image-preview:large"},
|
||||
{"unavailable_after:2027-01-01", "unavailable_after:2028-01-01,max-video-preview:5", "max-video-preview:5,unavailable_after:2027-01-01"},
|
||||
{"nosnippet", "noimageindex", "nosnippet,noimageindex"},
|
||||
} {
|
||||
if got := parse(tc.upper).Inherit(parse(tc.lower)).String(); got != tc.want {
|
||||
t.Errorf("%q over %q = %q, want %q", tc.upper, tc.lower, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user