test(17-06): sitemap parity unit tests
- TestURLTo (every Url::to golden case, BackendURL, URLPath), TestXMLEscaping (PHP XmlEscapingTest as behaviour), TestTiers (each default rule, first match, off, bad pattern fails Boot), TestRewrite, TestHooks, TestURLCap, TestManifest (fail-safe reads, key normalization, dates), TestFillable (PHP MassAssignmentTest), TestDefinitionItemJSON, TestBackendURL (trusted forwarded proto) - TestImportWinter on Postgres: maps and drops with report, --theme, --dry-run, --force, settings upsert, refusals
This commit is contained in:
151
classes/generator_test.go
Normal file
151
classes/generator_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/festival"
|
||||
)
|
||||
|
||||
func urlItems(paths ...string) []models.DefinitionItem {
|
||||
out := make([]models.DefinitionItem, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
out = append(out, models.DefinitionItem{Type: models.ItemTypeURL, URL: text(p), Changefreq: text("daily"), Priority: text("0.5")})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestHooks pins the typed successors of the PHP events (D-14): a
|
||||
// BeforeAddItem listener that sets Skip drops the URL before it is counted,
|
||||
// the first skipping listener stops the rest, AddItem listeners see the
|
||||
// written loc and their Extra is written before the url closing tag in
|
||||
// order, and a listener error stops the generation.
|
||||
func TestHooks(t *testing.T) {
|
||||
bus := festival.New()
|
||||
var seenBefore, seenAdd, afterSkip []string
|
||||
bus.Listen("acme.first", func(_ context.Context, e *classes.BeforeAddItem) error {
|
||||
seenBefore = append(seenBefore, e.Info.URL)
|
||||
if strings.HasSuffix(e.Info.URL, "/skip") {
|
||||
e.Skip = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
bus.Listen("acme.second", func(_ context.Context, e *classes.BeforeAddItem) error {
|
||||
afterSkip = append(afterSkip, e.Info.URL)
|
||||
return nil
|
||||
})
|
||||
bus.Listen("acme.extra", func(_ context.Context, e *classes.AddItem) error {
|
||||
seenAdd = append(seenAdd, e.Info.URL)
|
||||
e.Extra = append(e.Extra, "<image:image><image:loc>"+classes.EscapeText(e.Info.URL+"?a&b")+"</image:loc></image:image>")
|
||||
return nil
|
||||
})
|
||||
bus.Listen("acme.more", func(_ context.Context, e *classes.AddItem) error {
|
||||
e.Extra = append(e.Extra, "<x/>")
|
||||
return nil
|
||||
})
|
||||
gen := &classes.Generator{Root: "https://h.test", Events: bus}
|
||||
xml, err := gen.Generate(context.Background(), urlItems("/a", "/skip", "/b"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(seenBefore, ",") != "https://h.test/a,https://h.test/skip,https://h.test/b" {
|
||||
// The url item's URL is filled through Url::to before the event.
|
||||
t.Fatalf("BeforeAddItem saw %v", seenBefore)
|
||||
}
|
||||
if strings.Join(afterSkip, ",") != "https://h.test/a,https://h.test/b" {
|
||||
t.Fatalf("a listener after the skipping one ran for the skipped URL: %v", afterSkip)
|
||||
}
|
||||
if strings.Contains(xml, "/skip") {
|
||||
t.Fatalf("the skipped URL is written:\n%s", xml)
|
||||
}
|
||||
if strings.Join(seenAdd, ",") != "https://h.test/a,https://h.test/b" {
|
||||
t.Fatalf("AddItem saw %v", seenAdd)
|
||||
}
|
||||
want := "<url><loc>https://h.test/a</loc><changefreq>daily</changefreq><priority>0.5</priority><image:image><image:loc>https://h.test/a?a&b</image:loc></image:image><x/></url>"
|
||||
if !strings.Contains(xml, want) {
|
||||
t.Fatalf("extra not written before the closing tag:\n%s", xml)
|
||||
}
|
||||
|
||||
// A skip on the exact URL the event carries.
|
||||
bus2 := festival.New()
|
||||
var second []string
|
||||
bus2.Listen("acme.skip", func(_ context.Context, e *classes.BeforeAddItem) error {
|
||||
e.Skip = strings.HasSuffix(e.Info.URL, "/skip")
|
||||
return nil
|
||||
})
|
||||
bus2.Listen("acme.later", func(_ context.Context, e *classes.BeforeAddItem) error {
|
||||
second = append(second, e.Info.URL)
|
||||
return nil
|
||||
})
|
||||
gen = &classes.Generator{Root: "https://h.test", Events: bus2}
|
||||
xml, err = gen.Generate(context.Background(), urlItems("/a", "/skip"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(xml, "/skip<") || strings.Join(second, ",") != "https://h.test/a" {
|
||||
t.Fatalf("UntilHandled did not stop at the skipping listener: %v\n%s", second, xml)
|
||||
}
|
||||
|
||||
// Without listeners or a bus nothing changes.
|
||||
gen = &classes.Generator{Root: "https://h.test"}
|
||||
if xml, err := gen.Generate(context.Background(), urlItems("/a")); err != nil || !strings.Contains(xml, "<loc>https://h.test/a</loc>") {
|
||||
t.Fatalf("no bus: %v\n%s", err, xml)
|
||||
}
|
||||
|
||||
// A listener error stops the generation.
|
||||
failing := festival.New()
|
||||
failing.Listen("acme.fail", func(_ context.Context, e *classes.AddItem) error { return errors.New("listener failed") })
|
||||
gen = &classes.Generator{Root: "https://h.test", Events: failing}
|
||||
if _, err := gen.Generate(context.Background(), urlItems("/a")); err == nil || !strings.Contains(err.Error(), "listener failed") {
|
||||
t.Fatalf("listener error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestURLCap pins the protocol limit: of 50,001 URLs the 50,000th is
|
||||
// written and the 50,001st is not; skipped and empty URLs are not counted;
|
||||
// no items is an empty document.
|
||||
func TestURLCap(t *testing.T) {
|
||||
paths := make([]string, classes.MaxURLs+1)
|
||||
for i := range paths {
|
||||
paths[i] = "/u/" + strconv.Itoa(i+1)
|
||||
}
|
||||
gen := &classes.Generator{Root: "https://api.example.test"}
|
||||
xml, err := gen.Generate(context.Background(), urlItems(paths...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := strings.Count(xml, "<url>"); n != classes.MaxURLs {
|
||||
t.Fatalf("%d url elements, want %d", n, classes.MaxURLs)
|
||||
}
|
||||
if !strings.Contains(xml, "<loc>https://api.example.test/u/50000</loc>") || strings.Contains(xml, "<loc>https://api.example.test/u/50001</loc>") {
|
||||
t.Fatal("the 50000th URL must be written and the 50001st must not")
|
||||
}
|
||||
|
||||
// Skipped URLs do not count toward the cap.
|
||||
bus := festival.New()
|
||||
bus.Listen("acme.skip", func(_ context.Context, e *classes.BeforeAddItem) error {
|
||||
e.Skip = strings.HasSuffix(e.Info.URL, "/u/1")
|
||||
return nil
|
||||
})
|
||||
gen = &classes.Generator{Root: "https://api.example.test", Events: bus}
|
||||
xml, err = gen.Generate(context.Background(), urlItems(paths...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Count(xml, "<url>") != classes.MaxURLs || !strings.Contains(xml, "/u/50001</loc>") {
|
||||
t.Fatal("a skipped URL took a place under the cap")
|
||||
}
|
||||
|
||||
// A generator is reusable: a second run starts its count again.
|
||||
if again, err := gen.Generate(context.Background(), urlItems("/x")); err != nil || strings.Count(again, "<url>") != 1 {
|
||||
t.Fatalf("second run = %v, %d urls", err, strings.Count(again, "<url>"))
|
||||
}
|
||||
if empty, err := (&classes.Generator{Root: "https://h"}).Generate(context.Background(), nil); err != nil || empty != "" {
|
||||
t.Fatalf("no items = %q %v", empty, err)
|
||||
}
|
||||
}
|
||||
81
classes/manifest_test.go
Normal file
81
classes/manifest_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
)
|
||||
|
||||
// TestManifest pins the updates manifest reader: it fails safe (a missing,
|
||||
// empty-path, directory, oversized, non-object or invalid file is no
|
||||
// manifest, so lastmod is omitted), normalizes keys, ignores non-string
|
||||
// values, and parses the ISO dates strtotime reads, a date without a zone
|
||||
// as UTC.
|
||||
func TestManifest(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
for name, path := range map[string]string{
|
||||
"empty path": "",
|
||||
"spaces": " ",
|
||||
"missing": filepath.Join(dir, "missing.json"),
|
||||
"directory": dir,
|
||||
"list": write("list.json", `["/a"]`),
|
||||
"string": write("string.json", `"x"`),
|
||||
"invalid": write("invalid.json", `{"/a": `),
|
||||
"empty file": write("empty.json", ""),
|
||||
"oversized": write("big.json", `{"/a":"2026-01-01","pad":"`+strings.Repeat("x", classes.ManifestMaxBytes)+`"}`),
|
||||
"null": write("null.json", "null"),
|
||||
"leading space": write("space.json", " \n"),
|
||||
} {
|
||||
if m := classes.LoadManifest(path); m != nil {
|
||||
t.Errorf("%s: manifest = %v, want none", name, m)
|
||||
}
|
||||
}
|
||||
if fifo := filepath.Join(dir, "link.json"); os.Symlink(dir, fifo) == nil {
|
||||
if m := classes.LoadManifest(fifo); m != nil {
|
||||
t.Error("a symlink to a directory is not a manifest")
|
||||
}
|
||||
}
|
||||
|
||||
path := write("updates.json", ` {"blog/a/":"2026-01-02T03:04:05+02:00","/b":"2026-02-03","c":"2026-03-04 05:06:07","/":"2026-04-05T06:07","/num":12,"/bad":"yesterday","/epoch":"1970-01-01T00:00:00Z","/nested":{"x":1}}`)
|
||||
m := classes.LoadManifest(path)
|
||||
if m == nil {
|
||||
t.Fatal("valid manifest not read")
|
||||
}
|
||||
if _, ok := m["/num"]; ok {
|
||||
t.Fatal("a non-string value was kept")
|
||||
}
|
||||
for url, want := range map[string]string{
|
||||
"https://h/blog/a": "2026-01-02T01:04:05Z",
|
||||
"https://h/blog/a/": "2026-01-02T01:04:05Z",
|
||||
"https://h/b?x=1": "2026-02-03T00:00:00Z",
|
||||
"/c": "2026-03-04T05:06:07Z",
|
||||
"https://h": "2026-04-05T06:07:00Z",
|
||||
"https://h/": "2026-04-05T06:07:00Z",
|
||||
} {
|
||||
got, ok := m.Lastmod(url)
|
||||
if !ok || got.UTC().Format(time.RFC3339) != want {
|
||||
t.Errorf("Lastmod(%s) = %v %v, want %s", url, got, ok, want)
|
||||
}
|
||||
}
|
||||
for _, url := range []string{"https://h/bad", "https://h/epoch", "https://h/unlisted", "https://h/num"} {
|
||||
if got, ok := m.Lastmod(url); ok {
|
||||
t.Errorf("Lastmod(%s) = %v, want none", url, got)
|
||||
}
|
||||
}
|
||||
var none classes.Manifest
|
||||
if _, ok := none.Lastmod("https://h/b"); ok {
|
||||
t.Fatal("a nil manifest answered")
|
||||
}
|
||||
}
|
||||
52
classes/rewrite_test.go
Normal file
52
classes/rewrite_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
)
|
||||
|
||||
// TestRewrite pins the PHP routes.php frontend_url rewrite: the backend URL
|
||||
// is replaced inside loc, inside xhtml:link href and in the exact stylesheet
|
||||
// href, nowhere else; slashes are trimmed on both; an empty or "0" frontend
|
||||
// URL and equal URLs leave the document unchanged.
|
||||
func TestRewrite(t *testing.T) {
|
||||
const backend = "https://api.ex.pl"
|
||||
doc := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
`<?xml-stylesheet type="text/xsl" href="https://api.ex.pl/sitemap.xsl"?>` + "\n" +
|
||||
`<urlset><url><loc>https://api.ex.pl/a</loc><lastmod>2026</lastmod><changefreq>https://api.ex.pl</changefreq><priority>1</priority>` +
|
||||
`<xhtml:link rel="alternate" hreflang="pl" href="https://api.ex.pl/a"/><xhtml:link rel="alternate" hreflang="en" href="https://api.ex.pl/en/a"/>` +
|
||||
`<extra>https://api.ex.pl/x</extra></url><url><loc>https://other.pl/https://api.ex.pl</loc><changefreq>c</changefreq><priority>p</priority></url></urlset>` + "\n"
|
||||
want := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
`<?xml-stylesheet type="text/xsl" href="https://www.ex.pl/sitemap.xsl"?>` + "\n" +
|
||||
`<urlset><url><loc>https://www.ex.pl/a</loc><lastmod>2026</lastmod><changefreq>https://api.ex.pl</changefreq><priority>1</priority>` +
|
||||
`<xhtml:link rel="alternate" hreflang="pl" href="https://www.ex.pl/a"/><xhtml:link rel="alternate" hreflang="en" href="https://www.ex.pl/en/a"/>` +
|
||||
`<extra>https://api.ex.pl/x</extra></url><url><loc>https://other.pl/https://www.ex.pl</loc><changefreq>c</changefreq><priority>p</priority></url></urlset>` + "\n"
|
||||
for _, frontend := range []string{"https://www.ex.pl", "https://www.ex.pl/", "https://www.ex.pl///"} {
|
||||
for _, back := range []string{backend, backend + "/"} {
|
||||
if got := classes.Rewrite(doc, back, frontend); got != want {
|
||||
t.Fatalf("Rewrite(%q, %q):\n%s\nwant:\n%s", back, frontend, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, frontend := range []string{"", "0"} {
|
||||
if got := classes.Rewrite(doc, backend, frontend); got != doc {
|
||||
t.Fatalf("frontend %q changed the document", frontend)
|
||||
}
|
||||
}
|
||||
if got := classes.Rewrite(doc, backend, backend); got != doc {
|
||||
t.Fatal("equal URLs changed the document")
|
||||
}
|
||||
if got := classes.Rewrite(doc, backend+"/", backend); got != doc {
|
||||
t.Fatal("equal URLs up to a slash changed the document")
|
||||
}
|
||||
// An empty backend URL replaces nothing.
|
||||
if got := classes.Rewrite(doc, "", "https://www.ex.pl"); got != doc {
|
||||
t.Fatal("an empty backend URL changed the document")
|
||||
}
|
||||
// A stylesheet on another path is not the PHP pattern.
|
||||
other := `<?xml-stylesheet type="text/xsl" href="https://api.ex.pl/other.xsl"?>`
|
||||
if got := classes.Rewrite(other, backend, "https://www.ex.pl"); got != other {
|
||||
t.Fatalf("other stylesheet = %q", got)
|
||||
}
|
||||
}
|
||||
95
classes/serialize_test.go
Normal file
95
classes/serialize_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
||||
)
|
||||
|
||||
// TestXMLEscaping ports the PHP XmlEscapingTest (SITE-002) as behaviour:
|
||||
// every value the sitemap writes is escaped exactly as libxml escapes it,
|
||||
// text nodes (loc, lastmod, changefreq, priority) and attribute values
|
||||
// (hreflang, href), and no raw value reaches the output.
|
||||
func TestXMLEscaping(t *testing.T) {
|
||||
const raw = "a&b<c>d\"e'f\rg\th\ni"
|
||||
if got := classes.EscapeText(raw); got != "a&b<c>d\"e'f g\th\ni" {
|
||||
t.Fatalf("EscapeText = %q", got)
|
||||
}
|
||||
if got := classes.EscapeAttr(raw); got != "a&b<c>d"e'f g	h i" {
|
||||
t.Fatalf("EscapeAttr = %q", got)
|
||||
}
|
||||
if got := classes.EscapeText("zażółć — &"); got != "zażółć — &amp;" {
|
||||
t.Fatalf("EscapeText keeps UTF-8 and escapes an existing entity again: %q", got)
|
||||
}
|
||||
|
||||
xml := classes.Serialize("https://h/sitemap.xsl", []classes.URLEntry{{
|
||||
Loc: "https://h/a?x=1&y=<2>",
|
||||
Lastmod: "2026-01-02T03:04:05+00:00",
|
||||
Changefreq: "weekly&<>",
|
||||
Priority: "0.5\r",
|
||||
Alternates: []classes.Alternate{
|
||||
{Hreflang: "pl\"x", Href: "https://h/pl?a=1&b=\"2\"\t\n"},
|
||||
{Hreflang: "en", Href: "https://h/en'x"},
|
||||
},
|
||||
}})
|
||||
want := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<?xml-stylesheet type=\"text/xsl\" href=\"https://h/sitemap.xsl\"?>\n" +
|
||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">` +
|
||||
"<url><loc>https://h/a?x=1&y=<2></loc><lastmod>2026-01-02T03:04:05+00:00</lastmod><changefreq>weekly&<></changefreq><priority>0.5 </priority>" +
|
||||
`<xhtml:link rel="alternate" hreflang="pl"x" href="https://h/pl?a=1&b="2"	 "/>` +
|
||||
`<xhtml:link rel="alternate" hreflang="en" href="https://h/en'x"/>` +
|
||||
"</url></urlset>\n"
|
||||
if xml != want {
|
||||
t.Fatalf("serialized:\n%q\nwant:\n%q", xml, want)
|
||||
}
|
||||
|
||||
// One alternate is not written; an empty urlset is self-closed.
|
||||
single := classes.Serialize("s", []classes.URLEntry{{Loc: "l", Changefreq: "c", Priority: "p", Alternates: []classes.Alternate{{Hreflang: "pl", Href: "x"}}}})
|
||||
if strings.Contains(single, "xhtml:link") || strings.Contains(single, "<lastmod>") {
|
||||
t.Fatalf("single alternate or empty lastmod written: %q", single)
|
||||
}
|
||||
if empty := classes.Serialize("s", nil); !strings.HasSuffix(empty, `sitemap.xsd"/>`+"\n") {
|
||||
t.Fatalf("empty urlset = %q", empty)
|
||||
}
|
||||
|
||||
// Through the generator: stored changefreq and priority, a resolved
|
||||
// alternate's locale and a url item with every special character.
|
||||
url := "/q?a=1&b=<x>&c=\"y\""
|
||||
changefreq := "daily&<weekly>"
|
||||
priority := "0.9\"'"
|
||||
gen := &classes.Generator{Root: "https://api.example.test", Settings: models.Settings{}, Resolver: fakeResolver{t: t, types: map[string]map[string]goldenResolved{
|
||||
"acme-post": {"1": {URL: "/p?x=1&y=2", Alternates: [][2]string{{"pl&<", "/p?x=1&y=2"}, {"en\"", "/en/p?z=<1>"}}}},
|
||||
}}}
|
||||
out, err := gen.Generate(context.Background(), []models.DefinitionItem{
|
||||
{Type: models.ItemTypeURL, URL: &url, Changefreq: &changefreq, Priority: &priority},
|
||||
{Type: "acme-post", Reference: text("1"), Changefreq: &changefreq, Priority: &priority},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := out[strings.Index(out, "<url>"):]
|
||||
for _, bad := range []string{"<x>", "&b", "&y", `"y"`, "<weekly>", "daily&<", `hreflang="pl&<"`, `en""`, "<1>"} {
|
||||
if strings.Contains(body, bad) {
|
||||
t.Fatalf("raw value %q reached the output:\n%s", bad, body)
|
||||
}
|
||||
}
|
||||
for _, good := range []string{
|
||||
"<changefreq>daily&<weekly></changefreq>",
|
||||
"<priority>0.9\"'</priority>",
|
||||
`hreflang="pl&<"`,
|
||||
`hreflang="en""`,
|
||||
"y=2",
|
||||
} {
|
||||
if !strings.Contains(body, good) {
|
||||
t.Fatalf("output lacks %q:\n%s", good, body)
|
||||
}
|
||||
}
|
||||
if strings.Count(body, "<url>") != 2 {
|
||||
t.Fatalf("urls = %d:\n%s", strings.Count(body, "<url>"), body)
|
||||
}
|
||||
}
|
||||
|
||||
func text(s string) *string { return &s }
|
||||
115
classes/tiers_test.go
Normal file
115
classes/tiers_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sitemap "git.golem15.com/golem15/sm-sitemap-plugin"
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
)
|
||||
|
||||
// TestTiers pins the priority tiers (D-13): each default rule of the PHP
|
||||
// getPriorityForUrl with paths from its list, trailing slashes, the first
|
||||
// match winning, the settings' values, tiers turned off, a host list that
|
||||
// replaces the defaults, and a bad rule failing the plugin's Boot.
|
||||
func TestTiers(t *testing.T) {
|
||||
tiers := pluginTiers(t)
|
||||
if len(tiers) != 5 {
|
||||
t.Fatalf("default rules = %d", len(tiers))
|
||||
}
|
||||
s := models.DefaultSettings()
|
||||
for url, want := range map[string]string{
|
||||
"https://h": "1.0",
|
||||
"https://h/": "1.0",
|
||||
"https://h/en": "1.0",
|
||||
"https://h/en/": "1.0",
|
||||
"https://h/uslugi/strony-www": "0.8",
|
||||
"https://h/en/services/web": "0.8",
|
||||
"https://h/realizacje/projekt/": "0.8",
|
||||
"https://h/en/work/x": "0.8",
|
||||
"https://h/blog/kategoria/wpis": "0.6",
|
||||
"https://h/en/blog/category/post/": "0.6",
|
||||
"https://h/blog": "0.4",
|
||||
"https://h/blog/kategoria": "0.4",
|
||||
"https://h/uslugi": "0.4",
|
||||
"https://h/en/work": "0.4",
|
||||
"https://h/polityka-prywatnosci": "0.2",
|
||||
"https://h/en/terms/": "0.2",
|
||||
"https://h/laboratorium-ai/zabawa-animacja": "0.2",
|
||||
"https://h/en/ai-lab/blob-playground": "0.2",
|
||||
"https://h/kontakt?x=1": "0.2",
|
||||
} {
|
||||
if got, ok := tiers.Priority(url, s); !ok || got != want {
|
||||
t.Errorf("Priority(%s) = %q %v, want %s", url, got, ok, want)
|
||||
}
|
||||
}
|
||||
for _, url := range []string{"https://h/other", "https://h/blog/a/b/c", "https://h/uslugi/a/b", "https://h/en/blogs/x", "https://h/kontakt/more"} {
|
||||
if got, ok := tiers.Priority(url, s); ok {
|
||||
t.Errorf("Priority(%s) = %q, want no rule", url, got)
|
||||
}
|
||||
}
|
||||
custom := s
|
||||
custom.PriorityBlog = text("0.65")
|
||||
if got, _ := tiers.Priority("https://h/blog/a/b", custom); got != "0.65" {
|
||||
t.Fatalf("custom blog priority = %q", got)
|
||||
}
|
||||
off := s
|
||||
off.EnablePriorityTiers = false
|
||||
if _, ok := tiers.Priority("https://h/", off); ok {
|
||||
t.Fatal("tiers apply while turned off")
|
||||
}
|
||||
|
||||
// First match wins: an exact rule listed before a pattern that also matches.
|
||||
ordered, err := classes.CompileTiers([]classes.TierConfig{
|
||||
{Setting: models.PriorityLegal, Exact: []string{"/blog"}},
|
||||
{Setting: models.PriorityCategories, Pattern: "^/blog$"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := ordered.Priority("https://h/blog/", s); got != "0.2" {
|
||||
t.Fatalf("first match = %q", got)
|
||||
}
|
||||
if empty, err := classes.CompileTiers(nil); err != nil || len(empty) != 0 {
|
||||
t.Fatalf("no rules = %v %v", empty, err)
|
||||
}
|
||||
if none, err := classes.LoadTiers(nil); err != nil || len(none) != 0 {
|
||||
t.Fatalf("LoadTiers(nil) = %v %v", none, err)
|
||||
}
|
||||
|
||||
// Every refused rule names its index; a bad RE2 pattern too.
|
||||
for name, rule := range map[string]classes.TierConfig{
|
||||
"unknown setting": {Setting: "priority_other", Exact: []string{""}},
|
||||
"neither": {Setting: models.PriorityHomepage},
|
||||
"both": {Setting: models.PriorityHomepage, Exact: []string{""}, Pattern: "^/$"},
|
||||
"lookahead": {Setting: models.PriorityBlog, Pattern: "^/(?=blog)"},
|
||||
"unbalanced": {Setting: models.PriorityBlog, Pattern: "^/(blog"},
|
||||
} {
|
||||
_, err := classes.CompileTiers([]classes.TierConfig{{Setting: models.PriorityHomepage, Exact: []string{""}}, rule})
|
||||
if err == nil || !strings.Contains(err.Error(), classes.TiersConfigKey+"[1]") {
|
||||
t.Errorf("%s: err = %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A bad host rule fails the plugin's Boot.
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "golem15.yaml"), []byte("sitemap:\n priority_tiers:\n - setting: priority_blog\n pattern: '^/(blog'\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := &sitemap.Plugin{}
|
||||
if err := cfg.MergePlugin("golem15.sitemap", p.ConfigFS()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.Boot(backpack.New(cfg)); err == nil || !strings.Contains(err.Error(), "priority_tiers[0]") {
|
||||
t.Fatalf("Boot with a bad pattern = %v", err)
|
||||
}
|
||||
}
|
||||
57
classes/urlto_test.go
Normal file
57
classes/urlto_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package classes_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
)
|
||||
|
||||
// TestURLTo runs every case of testdata/urlto.json through URLTo and
|
||||
// compares it with the line PHP's Url::to wrote to urlto.golden (SC-3), then
|
||||
// pins the helpers built on it: BackendURL and URLPath.
|
||||
func TestURLTo(t *testing.T) {
|
||||
var in struct {
|
||||
Root string `json:"root"`
|
||||
URLs []*string `json:"urls"`
|
||||
}
|
||||
if err := json.Unmarshal(readTestdata(t, "urlto.json"), &in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := strings.Split(strings.TrimSuffix(string(readTestdata(t, "urlto.golden")), "\n"), "\n")
|
||||
if len(in.URLs) < 50 || len(want) != len(in.URLs) {
|
||||
t.Fatalf("%d inputs, %d golden lines", len(in.URLs), len(want))
|
||||
}
|
||||
for i, u := range in.URLs {
|
||||
path := "<null>"
|
||||
input := ""
|
||||
if u != nil {
|
||||
path, input = *u, *u
|
||||
}
|
||||
if got := classes.URLTo(in.Root, input); got != want[i] {
|
||||
t.Errorf("case %d URLTo(%q) = %q, PHP %q", i, path, got, want[i])
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range []struct{ root, want string }{
|
||||
{"https://api.ex.pl", "https://api.ex.pl"},
|
||||
{"https://api.ex.pl/", "https://api.ex.pl"},
|
||||
{"http://h:8080/base", "http://h:8080/base"},
|
||||
} {
|
||||
if got := classes.BackendURL(tc.root); got != tc.want {
|
||||
t.Errorf("BackendURL(%q) = %q, want %q", tc.root, got, tc.want)
|
||||
}
|
||||
}
|
||||
for raw, want := range map[string]string{
|
||||
"https://h/a/b?x=1#f": "/a/b",
|
||||
"https://h": "",
|
||||
"https://h/": "/",
|
||||
"/rel/path": "/rel/path",
|
||||
"": "",
|
||||
} {
|
||||
if got := classes.URLPath(raw); got != want {
|
||||
t.Errorf("URLPath(%q) = %q, want %q", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user