package chromium import ( "context" "errors" "fmt" "os" "regexp" "strings" "testing" "time" "github.com/google/uuid" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" ) func TestChromiumBrowser_Start(t *testing.T) { for _, tc := range []struct { scenario string browser browser expectError bool cleanup bool }{ { scenario: "successful start", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, }, ), expectError: false, cleanup: true, }, { scenario: "all browser arguments", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, incognito: true, allowInsecureLocalhost: true, ignoreCertificateErrors: true, disableWebSecurity: true, allowFileAccessFromFiles: true, hostResolverRules: "MAP forgery.docker.localhost traefik", proxyServer: "1.2.3.4", }, ), expectError: false, cleanup: true, }, { scenario: "browser already started", browser: func() browser { b := new(chromiumBrowser) b.isStarted.Store(true) return b }(), expectError: true, cleanup: false, }, { scenario: "browser start error", browser: func() browser { b := newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, }, ).(*chromiumBrowser) ctx, cancel := context.WithCancel(context.Background()) cancel() b.initialCtx = ctx return b }(), expectError: true, cleanup: false, }, } { t.Run(tc.scenario, func(t *testing.T) { logger := zap.NewNop() err := tc.browser.Start(logger) if tc.cleanup { defer func(b browser, logger *zap.Logger) { err = b.Stop(logger) if err != nil { t.Fatalf("expected no error while cleaning up, but got: %v", err) } }(tc.browser, logger) } if !tc.expectError && err != nil { t.Fatalf("expected no error but got: %v", err) } if tc.expectError && err == nil { t.Fatal("expected error but got none") } }) } } func TestChromiumBrowser_Stop(t *testing.T) { for _, tc := range []struct { scenario string browser browser setup func(browser browser, logger *zap.Logger) error expectError bool }{ { scenario: "successful stop", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, }, ), setup: func(b browser, logger *zap.Logger) error { return b.Start(logger) }, expectError: false, }, { scenario: "browser already stopped", browser: func() browser { b := new(chromiumBrowser) b.isStarted.Store(false) return b }(), expectError: false, }, } { t.Run(tc.scenario, func(t *testing.T) { logger := zap.NewNop() if tc.setup != nil { err := tc.setup(tc.browser, logger) if err != nil { t.Fatalf("setup error: %v", err) } } err := tc.browser.Stop(logger) if !tc.expectError && err != nil { t.Fatalf("expected no error but got: %v", err) } if tc.expectError && err == nil { t.Fatal("expected error but got none") } }) } } func TestChromiumBrowser_Healthy(t *testing.T) { for _, tc := range []struct { scenario string browser browser setup func(browser browser, logger *zap.Logger) error expectHealthy bool cleanup bool }{ { scenario: "healthy browser", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, }, ), setup: func(b browser, logger *zap.Logger) error { return b.Start(logger) }, expectHealthy: true, cleanup: true, }, { scenario: "browser not started", browser: func() browser { b := new(chromiumBrowser) b.isStarted.Store(false) return b }(), expectHealthy: false, cleanup: false, }, { scenario: "unhealthy browser", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, }, ), setup: func(b browser, logger *zap.Logger) error { _ = b.Start(logger) b.(*chromiumBrowser).cancelFunc() return nil }, expectHealthy: false, cleanup: true, }, } { t.Run(tc.scenario, func(t *testing.T) { logger := zap.NewNop() if tc.setup != nil { err := tc.setup(tc.browser, logger) if err != nil { t.Fatalf("setup error: %v", err) } } if tc.cleanup { defer func(b browser, logger *zap.Logger) { err := b.Stop(logger) if err != nil { t.Fatalf("expected no error while cleaning up, but got: %v", err) } }(tc.browser, logger) } healthy := tc.browser.Healthy(logger) if !tc.expectHealthy && healthy { t.Fatal("expected unhealthy browser but got an healthy one") } if tc.expectHealthy && !healthy { t.Fatal("expected a healthy browser but got an unhealthy one") } }) } } func TestChromiumBrowser_pdf(t *testing.T) { for _, tc := range []struct { scenario string browser browser fs *gotenberg.FileSystem options Options noDeadline bool start bool expectError bool expectedError error expectedLogEntries []string }{ { scenario: "browser not started", browser: func() browser { b := new(chromiumBrowser) b.isStarted.Store(false) return b }(), fs: gotenberg.NewFileSystem(), noDeadline: false, start: false, expectError: true, }, { scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list", browser: func() browser { b := new(chromiumBrowser) b.arguments = browserArguments{ allowList: regexp.MustCompile("^file:///[^tmp].*"), } b.isStarted.Store(true) return b }(), fs: gotenberg.NewFileSystem(), noDeadline: false, start: false, expectError: true, expectedError: ErrUrlNotAuthorized, }, { scenario: "ErrUrlNotAuthorized: main URL does match the denied list", browser: func() browser { b := new(chromiumBrowser) b.arguments = browserArguments{ allowList: regexp.MustCompile(""), denyList: regexp.MustCompile("^file:///tmp.*"), } b.isStarted.Store(true) return b }(), fs: gotenberg.NewFileSystem(), noDeadline: false, start: false, expectError: true, expectedError: ErrUrlNotAuthorized, }, { scenario: "ErrUrlNotAuthorized: main URL does match the denied list", browser: func() browser { b := new(chromiumBrowser) b.arguments = browserArguments{ allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), } b.isStarted.Store(true) return b }(), fs: gotenberg.NewFileSystem(), noDeadline: true, start: false, expectError: true, }, { scenario: "a request does not match the allowed list", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile("^file:///tmp.*"), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "'file:///etc/passwd' does not match the expression from the allowed list", }, }, { scenario: "a request does match the denied list", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile("^file:///[^tmp].*"), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "'file:///etc/passwd' matches the expression from the denied list", }, }, { scenario: "ErrConsoleExceptions", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ FailOnConsoleExceptions: true, }, noDeadline: false, start: true, expectError: true, expectedError: ErrConsoleExceptions, }, { scenario: "disable JavaScript", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), disableJavaScript: true, }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "disable JavaScript", "JavaScript disabled, skipping wait delay", "JavaScript disabled, skipping wait expression", }, }, { scenario: "extra HTTP headers", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("
Screen media type
"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ EmulatedMediaType: "screen", }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "emulate media type 'screen'", }, }, { scenario: "wait delay: context done", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ WaitDelay: time.Duration(10) * time.Second, }, noDeadline: false, start: true, expectError: true, expectedLogEntries: []string{ "wait '10s' before print", }, }, { scenario: "wait delay", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ WaitDelay: time.Duration(1) * time.Millisecond, }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "wait '1ms' before print", }, }, { scenario: "wait for expression: context done", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } html := ` ` err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(html), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ WaitForExpression: "window.status === 'ready'", }, noDeadline: false, start: true, expectError: true, expectedLogEntries: []string{ "wait until 'window.status === 'ready'' is true before print", }, }, { scenario: "ErrInvalidEvaluationExpression", browser: newChromiumBrowser( browserArguments{ binPath: os.Getenv("CHROMIUM_BIN_PATH"), wsUrlReadTimeout: 5 * time.Second, allowList: regexp.MustCompile(""), denyList: regexp.MustCompile(""), }, ), fs: func() *gotenberg.FileSystem { fs := gotenberg.NewFileSystem() err := os.MkdirAll(fs.WorkingDirPath(), 0o755) if err != nil { t.Fatalf(fmt.Sprintf("expected no error but got: %v", err)) } err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("