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, ""+classes.EscapeText(e.Info.URL+"?a&b")+"")
return nil
})
bus.Listen("acme.more", func(_ context.Context, e *classes.AddItem) error {
e.Extra = append(e.Extra, "")
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 := "https://h.test/adaily0.5https://h.test/a?a&b"
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, "https://h.test/a") {
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, ""); n != classes.MaxURLs {
t.Fatalf("%d url elements, want %d", n, classes.MaxURLs)
}
if !strings.Contains(xml, "https://api.example.test/u/50000") || strings.Contains(xml, "https://api.example.test/u/50001") {
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, "") != classes.MaxURLs || !strings.Contains(xml, "/u/50001") {
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, "") != 1 {
t.Fatalf("second run = %v, %d urls", err, strings.Count(again, ""))
}
if empty, err := (&classes.Generator{Root: "https://h"}).Generate(context.Background(), nil); err != nil || empty != "" {
t.Fatalf("no items = %q %v", empty, err)
}
}
// TestOnlyHTTPLocs pins the CR-01 output filter: a URL that Url::to keeps
// but that is not http or https (a javascript: URL FILTER_VALIDATE_URL
// accepts, mailto:, a bare fragment) is not written and not counted. A
// protocol-relative URL comes out of Url::to on the root host.
func TestOnlyHTTPLocs(t *testing.T) {
gen := &classes.Generator{Root: "https://api.example.test"}
xml, err := gen.Generate(context.Background(), urlItems(
"javascript://%0aalert(1)%0a//x",
"JavaScript://example.test/%0aalert(1)",
"//evil.test/x",
"mailto:a@example.test",
"#top",
"/ok",
"https://other.example.test/page",
))
if err != nil {
t.Fatal(err)
}
if n := strings.Count(xml, ""); n != 3 {
t.Fatalf("%d url elements, want 3:\n%s", n, xml)
}
if !strings.Contains(xml, "https://api.example.test/evil.test/x") {
t.Fatalf("the protocol-relative URL did not stay on the root host:\n%s", xml)
}
if strings.Contains(strings.ToLower(xml), "javascript") || strings.Contains(xml, "mailto:") || strings.Contains(xml, "#top") {
t.Fatalf("a non-http URL was written:\n%s", xml)
}
if !strings.Contains(xml, "https://api.example.test/ok") || !strings.Contains(xml, "https://other.example.test/page") {
t.Fatalf("an http(s) URL is missing:\n%s", xml)
}
}