Files
sm-sitemap-plugin/classes/writer_test.go
Jakub Zych 1048c01fd9 test: unit tests for the writer, News, IndexNow, HTML groups, slugs, lastmod and slots
- XML escaping and namespaces, no priority, the image cap and file names
- News window and cap, sources and languages, the publication name
- IndexNow batches of 10 000, answer outcomes, keys and the host filter
- HTML sitemap group order, tree order and paging
- default slugs, the effective type setting, lastmod config, slot edges
2026-11-04 19:36:10 +01:00

142 lines
5.0 KiB
Go

package classes
import (
"bytes"
"encoding/xml"
"strings"
"testing"
"time"
)
// TestWriteURLSet pins a sitemap file: the sitemap namespace, the xhtml
// namespace only when an entry has alternates, escaped locs and
// attributes, lastmod in UTC RFC 3339 and never a priority or changefreq,
// and well-formed XML.
func TestWriteURLSet(t *testing.T) {
at := time.Date(2026, 11, 4, 18, 30, 0, 0, time.FixedZone("CET", 3600))
var buf bytes.Buffer
err := WriteURLSet(&buf, []FileEntry{
{Loc: "https://example.test/a?x=1&y=<2>", LastMod: &at},
{Loc: "https://example.test/b", Alternates: []FileAlternate{{Hreflang: "en", Href: "https://example.test/b"}, {Hreflang: "x-default", Href: `https://example.test/b?"q"`}}},
})
if err != nil {
t.Fatal(err)
}
out := buf.String()
for _, want := range []string{
xml.Header,
`<urlset xmlns="` + NSSitemap + `" xmlns:xhtml="` + NSXHTML + `">`,
`<loc>https://example.test/a?x=1&amp;y=&lt;2&gt;</loc><lastmod>2026-11-04T17:30:00Z</lastmod>`,
`<xhtml:link rel="alternate" hreflang="x-default" href="https://example.test/b?&#34;q&#34;"/>`,
} {
if !strings.Contains(out, want) {
t.Fatalf("file lacks %s:\n%s", want, out)
}
}
for _, never := range []string{"<priority>", "<changefreq>"} {
if strings.Contains(out, never) {
t.Fatalf("file has %s:\n%s", never, out)
}
}
wellFormed(t, out)
buf.Reset()
if err := WriteURLSet(&buf, []FileEntry{{Loc: "https://example.test/"}}); err != nil {
t.Fatal(err)
}
if strings.Contains(buf.String(), "xmlns:xhtml") || strings.Contains(buf.String(), "<lastmod>") {
t.Fatalf("no alternates and no lastmod, yet:\n%s", buf.String())
}
wellFormed(t, buf.String())
}
// TestWriteIndex pins the index: one sitemap per file, lastmod only when
// known.
func TestWriteIndex(t *testing.T) {
at := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
var buf bytes.Buffer
if err := WriteIndex(&buf, []IndexFile{{Loc: "https://example.test/sitemap-pages-1.xml", LastMod: &at}, {Loc: "https://example.test/sitemap-posts-1.xml"}}); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, `<sitemapindex xmlns="`+NSSitemap+`">`) ||
!strings.Contains(out, "<sitemap><loc>https://example.test/sitemap-pages-1.xml</loc><lastmod>2026-01-02T03:04:05Z</lastmod></sitemap>") ||
!strings.Contains(out, "<sitemap><loc>https://example.test/sitemap-posts-1.xml</loc></sitemap>") {
t.Fatalf("index:\n%s", out)
}
wellFormed(t, out)
}
// TestWriteImageURLSet pins the image file: the image namespace, only
// image:image and image:loc, escaping, and the MaxImagesPerURL cap.
func TestWriteImageURLSet(t *testing.T) {
many := make([]string, MaxImagesPerURL+5)
for i := range many {
many[i] = "https://cdn.example.test/i.jpg?n=" + strings.Repeat("1", i%3+1)
}
var buf bytes.Buffer
if err := WriteImageURLSet(&buf, []ImageEntry{{Loc: "https://example.test/a", Images: []string{"https://cdn.example.test/c&d.webp"}}, {Loc: "https://example.test/b", Images: many}}); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, `xmlns:image="`+NSImage+`"`) || !strings.Contains(out, "<image:image><image:loc>https://cdn.example.test/c&amp;d.webp</image:loc></image:image>") {
t.Fatalf("image file:\n%s", out)
}
if n := strings.Count(out, "<image:image>"); n != 1+MaxImagesPerURL {
t.Fatalf("%d images, want 1 + the %d cap", n, MaxImagesPerURL)
}
for _, never := range []string{"image:caption", "image:title", "image:license"} {
if strings.Contains(out, never) {
t.Fatalf("image file has %s", never)
}
}
wellFormed(t, out)
}
// TestFileNames pins FileName and ParseFileName as inverses and the names
// ParseFileName refuses.
func TestFileNamesRoundTrip(t *testing.T) {
for _, tc := range []struct {
grp string
n int
}{{"pages", 1}, {"posts", 12}, {"news-2", 999999999}, {"a", 3}} {
name := FileName(tc.grp, tc.n)
grp, n, ok := ParseFileName(name)
if !ok || grp != tc.grp || n != tc.n {
t.Errorf("ParseFileName(%q) = %q %d %v", name, grp, n, ok)
}
}
for _, bad := range []string{"sitemap.xml", "sitemap-pages-0.xml", "sitemap-pages-01.xml", "sitemap-Pages-1.xml", "sitemap--1.xml", "sitemap-pages-1.xml.gz", "sitemap-pages-1234567890.xml", "../sitemap-pages-1.xml", "sitemap-pages-1"} {
if _, _, ok := ParseFileName(bad); ok {
t.Errorf("ParseFileName(%q) accepted", bad)
}
}
}
// TestValidSlug pins the group slug rule.
func TestValidSlug(t *testing.T) {
for s, want := range map[string]bool{
"pages": true, "news-2": true, "0abc": true, strings.Repeat("a", 64): true,
"": false, "-pages": false, "pages-": false, "Pages": false, "pa ges": false, "pa_ges": false, "żółw": false, strings.Repeat("a", 65): false,
} {
if got := ValidSlug(s); got != want {
t.Errorf("ValidSlug(%q) = %v, want %v", s, got, want)
}
}
}
// wellFormed parses out as XML to its end.
func wellFormed(t *testing.T, out string) {
t.Helper()
d := xml.NewDecoder(strings.NewReader(out))
for {
_, err := d.Token()
if err != nil {
if err.Error() == "EOF" {
return
}
t.Fatalf("not well-formed: %v\n%s", err, out)
}
}
}