A url item stored as javascript://%0aalert(1)%0a//x passed Url::to and reached <loc>, which sitemap.xsl renders as a link on the site origin. - The definitions form refuses, per row, a url that is not a path or an http(s) URL (classes.AllowedItemURL) and a urlPattern that is not a path (signpost.ValidItemPattern), keyed data.<i>.url / .urlPattern. - Generator.addItem drops a loc or alternate that is not http(s). - sitemap:import-winter drops such url items and clears such patterns with a report line; sitemap:import refuses such an entry.
184 lines
7.2 KiB
Go
184 lines
7.2 KiB
Go
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, "<image:image><image:loc>"+classes.EscapeText(e.Info.URL+"?a&b")+"</image:loc></image:image>")
|
|
return nil
|
|
})
|
|
bus.Listen("acme.more", func(_ context.Context, e *classes.AddItem) error {
|
|
e.Extra = append(e.Extra, "<x/>")
|
|
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 := "<url><loc>https://h.test/a</loc><changefreq>daily</changefreq><priority>0.5</priority><image:image><image:loc>https://h.test/a?a&b</image:loc></image:image><x/></url>"
|
|
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, "<loc>https://h.test/a</loc>") {
|
|
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, "<url>"); n != classes.MaxURLs {
|
|
t.Fatalf("%d url elements, want %d", n, classes.MaxURLs)
|
|
}
|
|
if !strings.Contains(xml, "<loc>https://api.example.test/u/50000</loc>") || strings.Contains(xml, "<loc>https://api.example.test/u/50001</loc>") {
|
|
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, "<url>") != classes.MaxURLs || !strings.Contains(xml, "/u/50001</loc>") {
|
|
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, "<url>") != 1 {
|
|
t.Fatalf("second run = %v, %d urls", err, strings.Count(again, "<url>"))
|
|
}
|
|
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, "<url>"); n != 3 {
|
|
t.Fatalf("%d url elements, want 3:\n%s", n, xml)
|
|
}
|
|
if !strings.Contains(xml, "<loc>https://api.example.test/evil.test/x</loc>") {
|
|
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, "<loc>https://api.example.test/ok</loc>") || !strings.Contains(xml, "<loc>https://other.example.test/page</loc>") {
|
|
t.Fatalf("an http(s) URL is missing:\n%s", xml)
|
|
}
|
|
}
|