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("

Extra HTTP headers

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ ExtraHttpHeaders: map[string]string{ "X-Foo": "Bar", }, }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "extra HTTP headers:", }, }, { scenario: "ErrOmitBackgroundWithoutPrintBackground", 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("

ErrOmitBackgroundWithoutPrintBackground

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ OmitBackground: true, }, noDeadline: false, start: true, expectError: true, expectedError: ErrOmitBackgroundWithoutPrintBackground, }, { scenario: "hide default white background", 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("

Hide default white background

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ OmitBackground: true, PrintBackground: true, }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "hide default white background", }, }, { scenario: "ErrInvalidEmulatedMediaType", 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("

ErrInvalidEmulatedMediaType

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ EmulatedMediaType: "foo", }, noDeadline: false, start: true, expectError: true, expectedError: ErrInvalidEmulatedMediaType, }, { scenario: "emulate a media type", 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("

ErrInvalidEvaluationExpression

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ WaitForExpression: "return undefined", }, noDeadline: false, start: true, expectError: true, expectedLogEntries: []string{ "wait until 'return undefined' is true before print", }, }, { scenario: "wait for expression", 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.globalVar === 'ready'", }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "wait until 'window.globalVar === 'ready'' is true before print", }, }, { scenario: "custom header and footer", 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("

Custom header and footer

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ HeaderTemplate: "

Header

", FooterTemplate: "

Footer

", }, noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "with custom header and/or footer", }, }, { scenario: "ErrInvalidPrinterSettings", 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("

ErrInvalidPrinterSettings

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ PaperWidth: 0, PaperHeight: 0, MarginTop: 1000000, MarginBottom: 1000000, MarginLeft: 1000000, MarginRight: 1000000, }, noDeadline: false, start: true, expectError: true, expectedError: ErrInvalidPrinterSettings, }, { scenario: "ErrPageRangesSyntaxError", 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("

ErrPageRangesSyntaxError

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: Options{ PageRanges: "foo", }, noDeadline: false, start: true, expectError: true, expectedError: ErrPageRangesSyntaxError, }, { scenario: "success (default options)", 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("

Default options

"), 0o755) if err != nil { t.Fatalf("expected no error but got: %v", err) } return fs }(), options: DefaultOptions(), noDeadline: false, start: true, expectError: false, expectedLogEntries: []string{ "JavaScript not disabled", "no extra HTTP headers", "navigate to", "default white background not hidden", "no emulated media type", "no wait delay", "no wait expression", "no custom header nor footer", }, }, } { t.Run(tc.scenario, func(t *testing.T) { core, recorded := observer.New(zapcore.DebugLevel) logger := zap.New(core) defer func() { err := os.RemoveAll(tc.fs.WorkingDirPath()) if err != nil { t.Fatalf("expected no error while cleaning up, but got: %v", err) } }() if tc.start { err := tc.browser.Start(logger) if err != nil { t.Fatalf("setup error: %v", err) } 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) } var ( ctx context.Context cancel context.CancelFunc ) if tc.noDeadline { ctx = context.Background() } else { ctx, cancel = context.WithTimeout(context.Background(), time.Duration(5)*time.Second) defer cancel() } err := tc.browser.pdf( ctx, logger, fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()), fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), tc.options, ) 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") } if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { t.Fatalf("expected error %v but got: %v", tc.expectedError, err) } for _, entry := range tc.expectedLogEntries { doExist := true for _, log := range recorded.All() { doExist = strings.Contains(log.Message, entry) if doExist { break } } if !doExist { t.Errorf("expected '%s' to exist as log entry", entry) } } }) } }