Render bounds opening the tab and rendering the page with one deadline (TabOpenTimeout 5 s plus RenderTimeout 20 s), so a hung Chrome no longer holds the crawl's render lock until the job timeout.
167 lines
5.9 KiB
Go
167 lines
5.9 KiB
Go
package crawl
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
)
|
|
|
|
// TestAllowRequest pins what the headless browser may load: https on the
|
|
// site host and its www twin, data: and blob:; http only in trusted mode;
|
|
// other hosts, credentials in the URL and every other scheme refused.
|
|
func TestAllowRequest(t *testing.T) {
|
|
hosts := []string{"example.test", "www.example.test"}
|
|
for _, tc := range []struct {
|
|
url string
|
|
trusted bool
|
|
want bool
|
|
}{
|
|
{"https://example.test/a.js", false, true},
|
|
{"https://WWW.example.test/b.css", false, true},
|
|
{"https://example.test:8443/x", false, true},
|
|
{"data:image/png;base64,AAAA", false, true},
|
|
{"blob:https://example.test/uuid", false, true},
|
|
{"http://example.test/a.js", false, false},
|
|
{"http://example.test/a.js", true, true},
|
|
{"https://cdn.other.test/lib.js", false, false},
|
|
{"https://example.test.evil.test/x", false, false},
|
|
{"https://user:pw@example.test/x", false, false},
|
|
{"ws://example.test/socket", false, false},
|
|
{"file:///etc/passwd", false, false},
|
|
{"javascript:alert(1)", true, false},
|
|
{"://bad", false, false},
|
|
} {
|
|
if got := allowRequest(tc.url, hosts, tc.trusted); got != tc.want {
|
|
t.Errorf("allowRequest(%q, trusted=%v) = %v, want %v", tc.url, tc.trusted, got, tc.want)
|
|
}
|
|
}
|
|
if !allowRequest("https://[::1]/x", []string{"[::1]"}, false) {
|
|
t.Fatal("a bracketed IPv6 host must match")
|
|
}
|
|
}
|
|
|
|
// TestResolverRules pins Chrome's --host-resolver-rules: one MAP per
|
|
// allowed host, sorted, to its first IPv4 address (else the first one, in
|
|
// brackets for IPv6), a host without addresses left out, and MAP *
|
|
// ~NOTFOUND last, since Chrome takes the first matching rule.
|
|
func TestResolverRules(t *testing.T) {
|
|
got := resolverRules(map[string][]netip.Addr{
|
|
"www.example.test": {netip.MustParseAddr("2001:db8::1")},
|
|
"example.test": {netip.MustParseAddr("2001:db8::2"), netip.MustParseAddr("192.0.2.10"), netip.MustParseAddr("192.0.2.11")},
|
|
"empty.test": nil,
|
|
})
|
|
want := "MAP example.test 192.0.2.10, MAP www.example.test [2001:db8::1], MAP * ~NOTFOUND"
|
|
if got != want {
|
|
t.Fatalf("rules = %q\nwant %q", got, want)
|
|
}
|
|
if got := resolverRules(nil); got != "MAP * ~NOTFOUND" {
|
|
t.Fatalf("no hosts = %q", got)
|
|
}
|
|
if !strings.HasSuffix(got, "MAP * ~NOTFOUND") || strings.Count(got, "~NOTFOUND") != 1 {
|
|
t.Fatal("the catch-all must come once and last")
|
|
}
|
|
}
|
|
|
|
// TestNewRenderer pins the comparison's start: no renderer and no error
|
|
// without a Chrome path (skipped), an error for a relative path, a
|
|
// missing file, a directory or a file that is not executable (failed).
|
|
func TestNewRenderer(t *testing.T) {
|
|
site, _ := url.Parse("https://example.test")
|
|
cfg := func(path string) *compass.Config {
|
|
c, err := compass.Open(compass.Options{Dir: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if path != "" {
|
|
if err := c.Set(ChromePathConfigKey, path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
if r, err := NewRenderer(context.Background(), cfg(""), site, false, "ua"); r != nil || err != nil {
|
|
t.Fatalf("no path: %v %v, want skipped", r, err)
|
|
}
|
|
if r, err := NewRenderer(context.Background(), nil, site, false, "ua"); r != nil || err != nil {
|
|
t.Fatal("no config: skipped")
|
|
}
|
|
dir := t.TempDir()
|
|
plain := filepath.Join(dir, "chrome.txt")
|
|
if err := os.WriteFile(plain, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, path := range map[string]string{"relative": "bin/chrome", "missing": filepath.Join(dir, "nope"), "directory": dir, "not executable": plain} {
|
|
r, err := NewRenderer(context.Background(), cfg(path), site, false, "ua")
|
|
if r != nil || err == nil || !strings.Contains(err.Error(), "Chrome path") {
|
|
t.Errorf("%s: %v %v, want a Chrome path error", name, r, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenderMax pins the page cap: the default 200, a positive config
|
|
// value, and the default for 0 or less.
|
|
func TestRenderMax(t *testing.T) {
|
|
if RenderMax(nil) != DefaultRenderMax || DefaultRenderMax != 200 {
|
|
t.Fatal("default cap")
|
|
}
|
|
for v, want := range map[int]int{50: 50, 0: DefaultRenderMax, -1: DefaultRenderMax} {
|
|
c, _ := compass.Open(compass.Options{Dir: t.TempDir()})
|
|
_ = c.Set(RenderMaxConfigKey, v)
|
|
if got := RenderMax(c); got != want {
|
|
t.Errorf("RenderMax(%d) = %d, want %d", v, got, want)
|
|
}
|
|
}
|
|
if got := SiteHosts("WWW.Example.test"); len(got) != 2 || got[0] != "www.example.test" || got[1] != "example.test" {
|
|
t.Fatalf("SiteHosts = %v", got)
|
|
}
|
|
if got := SiteHosts("example.test"); got[1] != "www.example.test" || SiteHosts(" ") != nil {
|
|
t.Fatalf("SiteHosts = %v", got)
|
|
}
|
|
}
|
|
|
|
// TestRenderTabTimeout pins L3 WR-06: a tab that does not open answers an
|
|
// error within TabOpenTimeout (lowered for the test) plus a margin, so the
|
|
// crawl's render lock is released; the next Render is not held up by the
|
|
// first one's hung tab.
|
|
func TestRenderTabTimeout(t *testing.T) {
|
|
saved := TabOpenTimeout
|
|
TabOpenTimeout = 50 * time.Millisecond
|
|
t.Cleanup(func() { TabOpenTimeout = saved })
|
|
hung := make(chan struct{})
|
|
t.Cleanup(func() { close(hung) })
|
|
r := &chromeRenderer{
|
|
allowed: []string{"example.test"},
|
|
browserCtx: context.Background(),
|
|
newTab: func(parent context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithCancel(parent)
|
|
},
|
|
// A hung Chrome: opening the tab never answers, even when the tab
|
|
// is closed.
|
|
startTab: func(context.Context) error {
|
|
<-hung
|
|
return nil
|
|
},
|
|
}
|
|
for i := range 2 {
|
|
start := time.Now()
|
|
_, err := r.Render(t.Context(), "https://example.test/a")
|
|
if err == nil || !strings.Contains(err.Error(), "did not open a tab within 50ms") {
|
|
t.Fatalf("Render %d: %v, want the tab timeout", i, err)
|
|
}
|
|
if d := time.Since(start); d > time.Second {
|
|
t.Fatalf("Render %d returned after %v", i, d)
|
|
}
|
|
}
|
|
TabOpenTimeout = 5 * time.Second
|
|
if got := seconds(TabOpenTimeout); got != "5 s" {
|
|
t.Fatalf("seconds = %q", got)
|
|
}
|
|
}
|