Files
gotenberg/pkg/modules/chromium/browser_test.go

97 lines
3.7 KiB
Go

package chromium
import (
"context"
"log/slog"
"slices"
"strings"
"testing"
"github.com/dlclark/regexp2/v2"
)
// TestChromiumBrowser_Start_rejectsOverlappingStart guards against the latch
// reported in https://github.com/gotenberg/gotenberg/issues/1599. When the
// supervisor abandons a Start goroutine on request-deadline expiry, that
// goroutine keeps running and holds startMu (and the pinning proxy it started)
// until it unwinds. A second Start must be refused rather than proceeding to
// start the pinning proxy a second time.
func TestChromiumBrowser_Start_rejectsOverlappingStart(t *testing.T) {
b := &chromiumBrowser{initialCtx: context.Background()}
// Simulate a Start still in flight.
b.startMu.Lock()
defer b.startMu.Unlock()
err := b.Start(slog.New(slog.DiscardHandler))
if err == nil {
t.Fatal("expected an error when a start is already in progress, got nil")
}
if !strings.Contains(err.Error(), "already in progress") {
t.Fatalf("expected an 'already in progress' error, got %q", err)
}
// The guard must return before touching any startup resource, so no user
// profile directory is created and the browser stays not started.
if b.userProfileDirPath != "" {
t.Fatalf("expected no user profile directory to be created, got %q", b.userProfileDirPath)
}
if b.isStarted.Load() {
t.Fatal("expected the browser to stay not started")
}
}
// TestChromiumDisableFeatures guards the override described in
// https://github.com/gotenberg/gotenberg/issues/1656. Gotenberg replaces
// chromedp's --disable-features value rather than extending it, as
// chromedp.Flag keys its flags by switch name. Dropping one of chromedp's own
// entries while editing this list would silently re-enable it.
func TestChromiumDisableFeatures(t *testing.T) {
for _, feature := range []string{
// chromedp.DefaultExecAllocatorOptions.
"site-per-process",
"Translate",
"BlinkGenPropertyTrees",
// The address-bar popup WebUI, built even in headless.
"WebUIOmniboxPopup",
"WebUIOmniboxAimPopup",
} {
if !slices.Contains(strings.Split(chromiumDisableFeatures, ","), feature) {
t.Errorf("expected %q to be disabled, got %q", feature, chromiumDisableFeatures)
}
}
}
// A WebSocket the outbound policy rejects must reach the pinning proxy, so
// the implicit loopback proxy bypass is removed whenever a policy can reject
// a network URL. See GHSA-73vq-mh8c-27wg.
func TestBrowserArguments_restrictsNetwork(t *testing.T) {
re := func(patterns ...string) []*regexp2.Regexp {
var res []*regexp2.Regexp
for _, p := range patterns {
res = append(res, regexp2.MustCompile(p, regexp2.None))
}
return res
}
for _, tc := range []struct {
scenario string
args browserArguments
expect bool
}{
{scenario: "default deny-list only", args: browserArguments{denyList: re(defaultDenyListPattern)}, expect: false},
{scenario: "no lists", args: browserArguments{}, expect: false},
{scenario: "allow-list", args: browserArguments{allowList: re(`^https://example\.com`), denyList: re(defaultDenyListPattern)}, expect: true},
{scenario: "custom deny-list", args: browserArguments{denyList: re(`^http://internal`)}, expect: true},
{scenario: "default and custom deny-list", args: browserArguments{denyList: re(defaultDenyListPattern, `^http://internal`)}, expect: true},
{scenario: "deny private IPs", args: browserArguments{denyList: re(defaultDenyListPattern), denyPrivateIPs: true}, expect: true},
{scenario: "deny public IPs", args: browserArguments{denyPublicIPs: true}, expect: true},
} {
t.Run(tc.scenario, func(t *testing.T) {
if got := tc.args.restrictsNetwork(); got != tc.expect {
t.Fatalf("restrictsNetwork() = %t, want %t", got, tc.expect)
}
})
}
}