Files
sm-sitemap-plugin/classes/serialize_test.go
Jakub Zych 4e55939a1f 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
2026-10-12 19:38:04 +02:00

96 lines
4.1 KiB
Go

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&amp;b&lt;c&gt;d\"e'f&#13;g\th\ni" {
t.Fatalf("EscapeText = %q", got)
}
if got := classes.EscapeAttr(raw); got != "a&amp;b&lt;c&gt;d&quot;e'f&#13;g&#9;h&#10;i" {
t.Fatalf("EscapeAttr = %q", got)
}
if got := classes.EscapeText("zażółć — &amp;"); got != "zażółć — &amp;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&amp;y=&lt;2&gt;</loc><lastmod>2026-01-02T03:04:05+00:00</lastmod><changefreq>weekly&amp;&lt;&gt;</changefreq><priority>0.5&#13;</priority>" +
`<xhtml:link rel="alternate" hreflang="pl&quot;x" href="https://h/pl?a=1&amp;b=&quot;2&quot;&#9;&#10;"/>` +
`<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&amp;&lt;weekly&gt;</changefreq>",
"<priority>0.9\"'</priority>",
`hreflang="pl&amp;&lt;"`,
`hreflang="en&quot;"`,
"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 }