Files
sm-sitemap-plugin/classes/news_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

127 lines
5.0 KiB
Go

package classes
import (
"bytes"
"strings"
"testing"
"time"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/compass"
)
// TestNewsLimits pins the News sitemap's window and cap (RESEARCH Pitfall
// 9): 48 hours and 1000 articles. The 47 h and 49 h boundaries on the
// database are TestNewsSitemap's.
func TestNewsLimits(t *testing.T) {
if NewsWindow != 48*time.Hour || MaxNewsEntries != 1000 || NewsFile != "sitemap-news.xml" {
t.Fatalf("window %s, cap %d, file %s", NewsWindow, MaxNewsEntries, NewsFile)
}
now := time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC)
for _, tc := range []struct {
age time.Duration
want bool
}{{0, true}, {47 * time.Hour, true}, {48 * time.Hour, true}, {48*time.Hour + time.Second, false}, {49 * time.Hour, false}} {
published := now.Add(-tc.age)
in := !published.Before(now.Add(-NewsWindow)) && !published.After(now)
if in != tc.want {
t.Errorf("published %s before now: in window %v, want %v", tc.age, in, tc.want)
}
}
}
// TestParseNewsSources pins the news_sources values: type:<key> and
// category:<type>:<id> (the last colon splits, so a type key may hold
// one); other forms and empty parts are ignored.
func TestParseNewsSources(t *testing.T) {
s := parseNewsSources([]string{"type:blog-post", "category:blog-category:7", "category:a:b:9", "type:", "category:x", "category::1", "category:x:", "other:1", ""})
if len(s.types) != 1 || !s.types["blog-post"] {
t.Fatalf("types = %v", s.types)
}
if len(s.parents) != 2 || !s.parents[models.Ref{Type: "blog-category", ID: "7"}] || !s.parents[models.Ref{Type: "a:b", ID: "9"}] {
t.Fatalf("parents = %v", s.parents)
}
if s.empty() || !parseNewsSources(nil).empty() || !parseNewsSources([]string{"bogus"}).empty() {
t.Fatal("empty() is wrong")
}
}
// TestNewsSourcesMatch pins P33-15: no source lists every article; else an
// article is listed by its type or by one of its parents.
func TestNewsSourcesMatch(t *testing.T) {
cat := models.Ref{Type: "blog-category", ID: "7"}
other := models.Ref{Type: "blog-category", ID: "8"}
all := parseNewsSources(nil)
byType := parseNewsSources([]string{"type:news"})
byCat := parseNewsSources([]string{"category:blog-category:7"})
for _, tc := range []struct {
name string
s newsSources
typ string
parents []models.Ref
want bool
}{
{"none listed", all, "blog-post", nil, true},
{"type listed", byType, "news", nil, true},
{"other type", byType, "blog-post", []models.Ref{cat}, false},
{"parent listed", byCat, "blog-post", []models.Ref{other, cat}, true},
{"other parent", byCat, "blog-post", []models.Ref{other}, false},
{"no parents", byCat, "blog-post", nil, false},
} {
if got := tc.s.matches(tc.typ, tc.parents); got != tc.want {
t.Errorf("%s: matches = %v, want %v", tc.name, got, tc.want)
}
}
}
// TestNewsLanguage pins the language codes Google reads.
func TestNewsLanguage(t *testing.T) {
for in, want := range map[string]string{
"pl": "pl", "pl-PL": "pl", "pl_PL": "pl", " EN ": "en", "en-GB": "en",
"zh": "zh-cn", "zh-Hans": "zh-cn", "zh-CN": "zh-cn", "zh-TW": "zh-tw", "zh_tw": "zh-tw", "zh-Hant": "zh-tw", "zh-Hant-HK": "zh-tw",
"": "",
} {
if got := NewsLanguage(in); got != want {
t.Errorf("NewsLanguage(%q) = %q, want %q", in, got, want)
}
}
}
// TestNewsPublication pins the publication name: the setting, else
// app.name, else nothing.
func TestNewsPublication(t *testing.T) {
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_APP__NAME=Acme Daily"}})
if err != nil {
t.Fatal(err)
}
app := backpack.New(cfg)
named := " The Acme Times "
if got := NewsPublication(app, models.Settings{NewsPublication: &named}); got != "The Acme Times" {
t.Fatalf("with the setting: %q", got)
}
blank := " "
if got := NewsPublication(app, models.Settings{NewsPublication: &blank}); got != "Acme Daily" {
t.Fatalf("with a blank setting: %q, want app.name", got)
}
if got := NewsPublication(nil, models.Settings{}); got != "" {
t.Fatalf("without an app: %q", got)
}
}
// TestWriteNewsURLSet pins the News file: the news namespace, every
// element in order, escaping and the UTC publication date.
func TestWriteNewsURLSet(t *testing.T) {
at := time.Date(2026, 11, 4, 9, 0, 0, 0, time.FixedZone("CET", 3600))
var buf bytes.Buffer
if err := WriteNewsURLSet(&buf, []NewsEntry{{Loc: "https://example.test/a?b&c", Publication: "A & B", Language: "pl", Published: at, Title: "<Grzyby>"}}); err != nil {
t.Fatal(err)
}
out := buf.String()
want := `<url><loc>https://example.test/a?b&amp;c</loc><news:news><news:publication><news:name>A &amp; B</news:name><news:language>pl</news:language></news:publication><news:publication_date>2026-11-04T08:00:00Z</news:publication_date><news:title>&lt;Grzyby&gt;</news:title></news:news></url>`
if !strings.Contains(out, `xmlns:news="`+NSNews+`"`) || !strings.Contains(out, want) {
t.Fatalf("news file:\n%s", out)
}
wellFormed(t, out)
}