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) } }