- 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
142 lines
5.0 KiB
Go
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&y=<2></loc><lastmod>2026-11-04T17:30:00Z</lastmod>`,
|
|
`<xhtml:link rel="alternate" hreflang="x-default" href="https://example.test/b?"q""/>`,
|
|
} {
|
|
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&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)
|
|
}
|
|
}
|
|
}
|