fix: chromium memory leaks (#705)

This commit is contained in:
Julien Neuhart
2023-10-23 17:52:30 +02:00
committed by GitHub
parent b5a59e4de0
commit 54daac329e
71 changed files with 4248 additions and 51761 deletions

View File

@@ -20,6 +20,15 @@ func (ctx *ContextMock) SetDirPath(path string) {
ctx.dirPath = path
}
// DirPath returns the context's working directory path.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetDirPath("/foo")
// dirPath := ctx.DirPath()
func (ctx *ContextMock) DirPath() string {
return ctx.dirPath
}
// SetValues sets the values.
//
// ctx := &api.ContextMock{Context: &api.Context{}}

View File

@@ -20,6 +20,18 @@ func TestContextMock_SetDirPath(t *testing.T) {
}
}
func TestContextMock_DirPath(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetDirPath("/foo")
actual := mock.DirPath()
expect := "/foo"
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestContextMock_SetValues(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetValues(map[string][]string{

View File

@@ -0,0 +1,301 @@
package chromium
import (
"context"
"errors"
"fmt"
"os"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
type browser interface {
gotenberg.Process
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
}
type browserArguments struct {
// Executor args.
binPath string
incognito bool
allowInsecureLocalhost bool
ignoreCertificateErrors bool
disableWebSecurity bool
allowFileAccessFromFiles bool
hostResolverRules string
proxyServer string
wsUrlReadTimeout time.Duration
// Tasks specific.
allowList *regexp.Regexp
denyList *regexp.Regexp
disableJavaScript bool
}
type chromiumBrowser struct {
initialCtx context.Context
ctx context.Context
cancelFunc context.CancelFunc
userProfileDirPath string
ctxMu sync.RWMutex
isStarted atomic.Bool
arguments browserArguments
fs *gotenberg.FileSystem
}
func newChromiumBrowser(arguments browserArguments) browser {
b := &chromiumBrowser{
initialCtx: context.Background(),
arguments: arguments,
fs: gotenberg.NewFileSystem(),
}
b.isStarted.Store(false)
return b
}
func (b *chromiumBrowser) Start(logger *zap.Logger) error {
if b.isStarted.Load() {
return errors.New("browser is already started")
}
debug := &debugLogger{logger: logger}
b.userProfileDirPath = b.fs.NewDirPath()
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.CombinedOutput(debug),
chromedp.ExecPath(b.arguments.binPath),
chromedp.NoSandbox,
// See:
// https://github.com/gotenberg/gotenberg/issues/327
// https://github.com/chromedp/chromedp/issues/904
chromedp.DisableGPU,
// See:
// https://github.com/puppeteer/puppeteer/issues/661
// https://github.com/puppeteer/puppeteer/issues/2410
chromedp.Flag("font-render-hinting", "none"),
chromedp.UserDataDir(b.userProfileDirPath),
)
if b.arguments.incognito {
opts = append(opts, chromedp.Flag("incognito", b.arguments.incognito))
}
if b.arguments.allowInsecureLocalhost {
// See https://github.com/gotenberg/gotenberg/issues/488.
opts = append(opts, chromedp.Flag("allow-insecure-localhost", true))
}
if b.arguments.ignoreCertificateErrors {
opts = append(opts, chromedp.IgnoreCertErrors)
}
if b.arguments.disableWebSecurity {
opts = append(opts, chromedp.Flag("disable-web-security", true))
}
if b.arguments.allowFileAccessFromFiles {
// See https://github.com/gotenberg/gotenberg/issues/356.
opts = append(opts, chromedp.Flag("allow-file-access-from-files", true))
}
if b.arguments.hostResolverRules != "" {
// See https://github.com/gotenberg/gotenberg/issues/488.
opts = append(opts, chromedp.Flag("host-resolver-rules", b.arguments.hostResolverRules))
}
if b.arguments.proxyServer != "" {
// See https://github.com/gotenberg/gotenberg/issues/376.
opts = append(opts, chromedp.ProxyServer(b.arguments.proxyServer))
}
// See https://github.com/gotenberg/gotenberg/issues/524.
opts = append(opts, chromedp.WSURLReadTimeout(b.arguments.wsUrlReadTimeout))
allocatorCtx, allocatorCancel := chromedp.NewExecAllocator(b.initialCtx, opts...)
ctx, cancel := chromedp.NewContext(allocatorCtx, chromedp.WithDebugf(debug.Printf))
err := chromedp.Run(ctx)
if err != nil {
cancel()
allocatorCancel()
return fmt.Errorf("run exec allocator: %w", err)
}
b.ctxMu.Lock()
defer b.ctxMu.Unlock()
// We have to keep the context around, as we need it to create a new tabs
// later.
b.ctx = ctx
b.cancelFunc = func() {
cancel()
allocatorCancel()
}
b.isStarted.Store(true)
return nil
}
func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
if !b.isStarted.Load() {
return errors.New("browser is already stopped")
}
// Always remove the user profile directory created by Chromium.
copyUserProfileDirPath := b.userProfileDirPath
defer func(userProfileDirPath string) {
go func() {
// FIXME: Chromium seems to recreate the user profile directory
// right after its deletion if we do not wait a certain amount
// of time before re-deleting it.
<-time.After(10 * time.Second)
err := os.RemoveAll(userProfileDirPath)
if err != nil {
logger.Error(fmt.Sprintf("remove Chromium's user profile directory: %s", err))
}
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
}()
}(copyUserProfileDirPath)
b.ctxMu.Lock()
defer b.ctxMu.Unlock()
b.cancelFunc()
b.ctx = nil
b.userProfileDirPath = ""
b.isStarted.Store(false)
return nil
}
func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
// Good to know: the supervisor does not call this method if no first start
// or if the process is restarting.
if !b.isStarted.Load() {
// Non-started browser but not restarting?
return false
}
b.ctxMu.RLock()
defer b.ctxMu.RUnlock()
taskCtx, cancel := chromedp.NewContext(b.ctx)
defer cancel()
err := chromedp.Run(taskCtx, chromedp.Navigate("about:blank"))
if err != nil {
logger.Error(fmt.Sprintf("browser health check failed: %s", err))
return false
}
return true
}
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
if !b.isStarted.Load() {
return errors.New("browser not started, cannot handle PDF conversion")
}
// We validate the "main" URL against our allow / deny lists.
if !b.arguments.allowList.MatchString(url) {
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
}
if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
}
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}
b.ctxMu.RLock()
defer b.ctxMu.RUnlock()
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Until(deadline))
defer timeoutCancel()
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
defer taskCancel()
// We validate all others requests against our allow / deny lists.
// If a request does not pass the validation, we make it fail.
listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
var (
consoleExceptions error
consoleExceptionsMu sync.RWMutex
)
// See https://github.com/gotenberg/gotenberg/issues/262.
if options.FailOnConsoleExceptions && !b.arguments.disableJavaScript {
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
}
tasks := chromedp.Tasks{
network.Enable(),
fetch.Enable(),
runtime.Enable(),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
forceExactColorsActionFunc(),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
printToPdfActionFunc(logger, outputPath, options),
}
err := chromedp.Run(taskCtx, tasks...)
if err != nil {
errMessage := err.Error()
if strings.Contains(errMessage, "Show invalid printer settings error (-32000)") || strings.Contains(errMessage, "content area is empty (-32602)") {
return ErrInvalidPrinterSettings
}
if strings.Contains(errMessage, "Page range syntax error") {
return ErrPageRangesSyntaxError
}
if strings.Contains(errMessage, "rpcc: message too large") {
return ErrRpccMessageTooLarge
}
return fmt.Errorf("print to PDF: %w", err)
}
// See https://github.com/gotenberg/gotenberg/issues/262.
consoleExceptionsMu.RLock()
defer consoleExceptionsMu.RUnlock()
if consoleExceptions != nil {
return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions)
}
return nil
}
// Interface guards.
var (
_ gotenberg.Process = (*chromiumBrowser)(nil)
_ browser = (*chromiumBrowser)(nil)
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import (
"errors"
"os"
"reflect"
"regexp"
"testing"
"time"
@@ -15,14 +14,6 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
type ProtoAPI struct {
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error
}
func (mod ProtoAPI) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
return mod.pdf(ctx, logger, URL, outputPath, options)
}
func TestDefaultOptions(t *testing.T) {
actual := DefaultOptions()
notExpect := Options{}
@@ -33,7 +24,7 @@ func TestDefaultOptions(t *testing.T) {
}
func TestChromium_Descriptor(t *testing.T) {
descriptor := Chromium{}.Descriptor()
descriptor := new(Chromium).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Chromium))
@@ -45,9 +36,9 @@ func TestChromium_Descriptor(t *testing.T) {
func TestChromium_Provision(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *gotenberg.Context
expectErr bool
scenario string
ctx *gotenberg.Context
expectError bool
}{
{
scenario: "no logger provider",
@@ -59,12 +50,12 @@ func TestChromium_Provision(t *testing.T) {
[]gotenberg.ModuleDescriptor{},
)
}(),
expectErr: true,
expectError: true,
},
{
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context {
mod := struct {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -84,12 +75,12 @@ func TestChromium_Provision(t *testing.T) {
},
)
}(),
expectErr: true,
expectError: true,
},
{
scenario: "no PDF engine provider",
ctx: func() *gotenberg.Context {
mod := struct {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -109,12 +100,12 @@ func TestChromium_Provision(t *testing.T) {
},
)
}(),
expectErr: true,
expectError: true,
},
{
scenario: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context {
mod := struct {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
@@ -138,12 +129,12 @@ func TestChromium_Provision(t *testing.T) {
},
)
}(),
expectErr: true,
expectError: true,
},
{
scenario: "provision success",
ctx: func() *gotenberg.Context {
mod := struct {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
@@ -155,7 +146,7 @@ func TestChromium_Provision(t *testing.T) {
return zap.NewNop(), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil
return new(gotenberg.PDFEngineMock), nil
}
return gotenberg.NewContext(
@@ -169,123 +160,233 @@ func TestChromium_Provision(t *testing.T) {
}(),
},
} {
mod := new(Chromium)
err := mod.Provision(tc.ctx)
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil {
t.Errorf("test %s: expected error but got: %v", tc.scenario, err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %s: expected no error but got: %v", tc.scenario, err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestChromium_Validate(t *testing.T) {
for i, tc := range []struct {
binPath string
expectErr bool
for _, tc := range []struct {
scenario string
binPath string
expectError bool
}{
{
expectErr: true,
scenario: "empty bin path",
binPath: "",
expectError: true,
},
{
binPath: "/foo",
expectErr: true,
scenario: "bin path does not exist",
binPath: "/foo",
expectError: true,
},
{
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
scenario: "valid bin path",
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
expectError: false,
},
} {
mod := new(Chromium)
mod.binPath = tc.binPath
err := mod.Validate()
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.args = browserArguments{
binPath: tc.binPath,
}
err := mod.Validate()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestChromium_Start(t *testing.T) {
for _, tc := range []struct {
scenario string
autoStart bool
supervisor *gotenberg.ProcessSupervisorMock
expectError bool
}{
{
scenario: "no auto-start",
autoStart: false,
expectError: false,
},
{
scenario: "auto-start success",
autoStart: true,
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
return nil
}},
expectError: false,
},
{
scenario: "auto-start failed",
autoStart: true,
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
return errors.New("foo")
}},
expectError: true,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.autoStart = tc.autoStart
mod.supervisor = tc.supervisor
err := mod.Start()
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 TestChromium_StartupMessage(t *testing.T) {
mod := new(Chromium)
mod.autoStart = true
autoStartMsg := mod.StartupMessage()
mod.autoStart = false
noAutoStartMsg := mod.StartupMessage()
if autoStartMsg == noAutoStartMsg {
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
}
}
func TestChromium_Stop(t *testing.T) {
for _, tc := range []struct {
scenario string
supervisor *gotenberg.ProcessSupervisorMock
expectError bool
}{
{
scenario: "stop success",
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
return nil
}},
expectError: false,
},
{
scenario: "stop failed",
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
return errors.New("foo")
}},
expectError: true,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.logger = zap.NewNop()
mod.supervisor = tc.supervisor
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
cancel()
err := mod.Stop(ctx)
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 TestChromium_Metrics(t *testing.T) {
metrics, err := new(Chromium).Metrics()
mod := new(Chromium)
mod.supervisor = &gotenberg.ProcessSupervisorMock{
ReqQueueSizeMock: func() int64 {
return 10
},
RestartsCountMock: func() int64 {
return 0
},
}
metrics, err := mod.Metrics()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if len(metrics) != 2 {
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
if len(metrics) != 4 {
t.Fatalf("expected %d metrics, but got %d", 4, len(metrics))
}
actual := metrics[0].Read()
if actual != 0 {
t.Errorf("expected %d Chromium instances, but got %f", 0, actual)
if actual != float64(1) {
t.Errorf("expected %f for chromium_active_instances_count, but got %f", float64(1), actual)
}
actual = metrics[1].Read()
if actual != 0 {
t.Errorf("expected %d Chromium failed starts, but got %f", 0, actual)
if actual != float64(0) {
t.Errorf("expected %f for chromium_failed_starts_count, but got %f", float64(0), actual)
}
actual = metrics[2].Read()
if actual != float64(10) {
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
}
actual = metrics[3].Read()
if actual != float64(0) {
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
}
}
func TestChromium_Checks(t *testing.T) {
tests := []struct {
name string
mod Chromium
tearUp func()
tearDown func()
for _, tc := range []struct {
scenario string
supervisor gotenberg.ProcessSupervisor
expectAvailabilityStatus health.AvailabilityStatus
}{
{
name: "ignore Chromium failed starts",
mod: Chromium{
failedStartsThreshold: 0,
},
},
{
name: "with Chromium failed starts threshold not reached",
mod: Chromium{
failedStartsThreshold: 1,
},
scenario: "healthy module",
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
return true
}},
expectAvailabilityStatus: health.StatusUp,
},
{
name: "with Chromium failed starts threshold reached",
mod: Chromium{
failedStartsThreshold: 1,
},
tearUp: func() {
failedStartsCount = 1
},
tearDown: func() {
failedStartsCount = 0
},
scenario: "unhealthy module",
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
return false
}},
expectAvailabilityStatus: health.StatusDown,
},
}
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.supervisor = tc.supervisor
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.tearUp != nil {
tc.tearUp()
}
checks, err := tc.mod.Checks()
checks, err := mod.Checks()
if err != nil {
t.Fatalf("expected no error from mod.Checks(), but got: %v", err)
}
if len(checks) == 0 {
return
}
if len(checks) != 1 {
t.Fatalf("expected 1 check from mod.Checks(), but got %d", len(checks))
t.Fatalf("expected no error but got: %v", err)
}
checker := health.NewChecker(checks...)
@@ -294,10 +395,6 @@ func TestChromium_Checks(t *testing.T) {
if result.Status != tc.expectAvailabilityStatus {
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
}
if tc.tearDown != nil {
tc.tearDown()
}
})
}
}
@@ -312,412 +409,76 @@ func TestChromium_Chromium(t *testing.T) {
}
func TestChromium_Routes(t *testing.T) {
for i, tc := range []struct {
for _, tc := range []struct {
scenario string
expectRoutes int
disableRoutes bool
}{
{
expectRoutes: 3,
scenario: "routes not disabled",
expectRoutes: 3,
disableRoutes: false,
},
{
scenario: "routes disabled",
expectRoutes: 0,
disableRoutes: true,
},
} {
mod := new(Chromium)
mod.disableRoutes = tc.disableRoutes
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.disableRoutes = tc.disableRoutes
routes, err := mod.Routes()
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
routes, err := mod.Routes()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectRoutes != len(routes) {
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
}
if tc.expectRoutes != len(routes) {
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
}
})
}
}
func TestChromium_PDF(t *testing.T) {
func TestChromium_Pdf(t *testing.T) {
for _, tc := range []struct {
name string
timeout time.Duration
cancel context.CancelFunc
URL string
options Options
userAgent string
incognito bool
allowInsecureLocalhost bool
ignoreCertificateErrors bool
disableWebSecurity bool
allowFileAccessFromFiles bool
hostResolverRules string
proxyServer string
allowList *regexp.Regexp
denyList *regexp.Regexp
disableJavaScript bool
expectErr bool
scenario string
supervisor gotenberg.ProcessSupervisor
browser browser
expectError bool
}{
{
name: "context has no deadline",
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
expectErr: true,
scenario: "PDF task success",
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
}},
expectError: false,
},
{
name: "URL does not match the expression from the allowed list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
allowList: regexp.MustCompile("file:///tmp/*"),
expectErr: true,
},
{
name: "URL does not match the expression from the denied list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
denyList: regexp.MustCompile("file:///tests/*"),
expectErr: true,
},
{
name: "with user agent",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
UserAgent: "foo",
},
},
{
name: "fail on console exceptions",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html",
options: Options{
FailOnConsoleExceptions: true,
},
expectErr: true,
},
{
name: "disable JavaScript",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
disableJavaScript: true,
},
{
name: "with extra HTTP headers",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
ExtraHTTPHeaders: map[string]string{
"foo": "bar",
},
},
},
{
name: "with extra link tags",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
options: Options{
ExtraLinkTags: []LinkTag{
{
Href: "font.woff",
},
{
Href: "style.css",
},
},
},
},
{
name: "with invalid emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{
EmulatedMediaType: "foo",
},
expectErr: true,
},
{
name: "with screen emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{
EmulatedMediaType: "screen",
},
},
{
name: "with print emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{
EmulatedMediaType: "print",
},
},
{
name: "with omit background but not print background",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
OmitBackground: true,
},
expectErr: true,
},
{
name: "with omit background and print background",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
OmitBackground: true,
PrintBackground: true,
},
},
{
name: "with extra script tags",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
options: Options{
ExtraScriptTags: []ScriptTag{
{
Src: "script.js",
},
},
},
},
{
name: "with wait delay",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
WaitDelay: time.Duration(1) * time.Nanosecond,
},
},
{
name: "with invalid wait window status",
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitWindowStatus: "foo",
},
expectErr: true,
},
{
name: "with wait window status",
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitWindowStatus: "ready",
},
},
{
name: "with wait for expression that should not happen",
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitForExpression: "window.status === 'foo'",
},
expectErr: true,
},
{
name: "with valid wait for expression",
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitForExpression: "window.status === 'ready'",
},
},
{
name: "with invalid wait for expression",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
WaitForExpression: "return undefined",
},
expectErr: true,
},
{
name: "with too big margin bottom",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
MarginBottom: 100,
},
expectErr: true,
},
{
name: "with invalid page ranges",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
PageRanges: "foo",
},
expectErr: true,
},
{
name: "with a lot of properties",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
userAgent: "foo",
incognito: true,
ignoreCertificateErrors: true,
allowInsecureLocalhost: true,
disableWebSecurity: true,
allowFileAccessFromFiles: true,
hostResolverRules: "foo",
proxyServer: "foo",
},
{
name: "with file using local and remote assets",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
},
{
name: "URL does match the expression from the allowed list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
allowList: regexp.MustCompile("file:///tests/*"),
},
{
name: "URL does match the expression from the denied list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
denyList: regexp.MustCompile("file:///etc/*"),
},
{
name: "with custom header and footer templates",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
HeaderTemplate: func() string {
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return string(b)
}(),
FooterTemplate: func() string {
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return string(b)
}(),
},
},
{
name: "with custom header template only",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
HeaderTemplate: func() string {
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return string(b)
}(),
FooterTemplate: DefaultOptions().FooterTemplate,
},
},
{
name: "with custom footer template only",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
HeaderTemplate: DefaultOptions().HeaderTemplate,
FooterTemplate: func() string {
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return string(b)
}(),
},
},
{
name: "without custom header and footer templates",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{
HeaderTemplate: DefaultOptions().HeaderTemplate,
FooterTemplate: DefaultOptions().FooterTemplate,
},
},
{
name: "with file using a .gif",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html",
},
{
name: "with allow file access from files",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample6/index.html",
allowFileAccessFromFiles: true,
},
{
name: "with file using a style attribute",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample7/index.html",
scenario: "PDF task error",
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return errors.New("PDF task error")
}},
expectError: true,
},
} {
func() {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.binPath = os.Getenv("CHROMIUM_BIN_PATH")
mod.userAgent = tc.userAgent
mod.incognito = tc.incognito
mod.allowInsecureLocalhost = tc.allowInsecureLocalhost
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
mod.disableWebSecurity = tc.disableWebSecurity
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
mod.hostResolverRules = tc.hostResolverRules
mod.proxyServer = tc.proxyServer
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
return task()
}}
mod.browser = tc.browser
if tc.allowList == nil {
tc.allowList = regexp.MustCompile("")
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.denyList == nil {
tc.denyList = regexp.MustCompile("")
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
mod.allowList = tc.allowList
mod.denyList = tc.denyList
mod.disableJavaScript = tc.disableJavaScript
mod.fs = gotenberg.NewFileSystem()
ctxFs := gotenberg.NewFileSystem()
outputDir, err := ctxFs.MkdirAll()
if err != nil {
t.Fatalf("test %s: expected error but got: %v", tc.name, err)
}
defer func() {
err := os.RemoveAll(ctxFs.WorkingDirPath())
if err != nil {
t.Fatalf("test %s: expected no error while cleaning up but got: %v", tc.name, err)
}
}()
if tc.timeout == 0 {
err = mod.PDF(context.Background(), zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
} else {
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
defer cancel()
err = mod.PDF(ctx, zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
}
if tc.expectErr && err == nil {
t.Errorf("test %s: expected error but got: %v", tc.name, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %s: expected no error but got: %v", tc.name, err)
}
}()
})
}
}
// Interface guards.
var (
_ API = (*ProtoAPI)(nil)
)

View File

@@ -7,21 +7,21 @@ import (
"go.uber.org/zap"
)
// debugLogger is wrapper around a zap.Logger which is used for debugging
// debugLogger is wrapper around a [zap.Logger] which is used for debugging
// Chromium.
type debugLogger struct {
logger *zap.Logger
}
// Write logs the bytes in a debug message.
func (debug debugLogger) Write(p []byte) (n int, err error) {
func (debug *debugLogger) Write(p []byte) (n int, err error) {
debug.logger.Debug(string(p))
return len(p), nil
}
// Printf logs a debug message.
func (debug debugLogger) Printf(format string, v ...interface{}) {
func (debug *debugLogger) Printf(format string, v ...interface{}) {
debug.logger.Debug(fmt.Sprintf(format, v...))
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestDebugLogger_Write(t *testing.T) {
actual, err := debugLogger{logger: zap.NewNop()}.Write([]byte("foo"))
actual, err := (&debugLogger{logger: zap.NewNop()}).Write([]byte("foo"))
expected := len([]byte("foo"))
if actual != expected {
@@ -20,5 +20,5 @@ func TestDebugLogger_Write(t *testing.T) {
}
func TestDebugLogger_Printf(t *testing.T) {
debugLogger{logger: zap.NewNop()}.Printf("%s", "foo")
(&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo")
}

View File

@@ -1,4 +1,4 @@
// Package chromium provides a module which adds routes for converting HTML
// documents to PDF. Other modules may also retrieve the API provided by this
// documents to Pdf. Other modules may also retrieve the [Api] provided by this
// module.
package chromium

View File

@@ -0,0 +1,34 @@
package chromium
import (
"context"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
// ApiMock is a mock for the [Api] interface.
type ApiMock struct {
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
}
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
return api.PdfMock(ctx, logger, URL, outputPath, options)
}
// browserMock is a mock for the [browser] interface.
type browserMock struct {
gotenberg.ProcessMock
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
}
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return b.pdfMock(ctx, logger, url, outputPath, options)
}
// Interface guards.
var (
_ Api = (*ApiMock)(nil)
_ browser = (*browserMock)(nil)
)

View File

@@ -0,0 +1,34 @@
package chromium
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestApiMock(t *testing.T) {
mock := &ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
},
}
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
if err != nil {
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
}
}
func TestBrowserMock(t *testing.T) {
mock := &browserMock{
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
},
}
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
if err != nil {
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
}
}

View File

@@ -21,9 +21,9 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
// FormDataChromiumPDFOptions creates Options form the form data. Fallback to
// FormDataChromiumPdfOptions creates [Options] from the form data. Fallback to
// default value if the considered key is not present.
func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions()
var (
@@ -32,14 +32,14 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
waitWindowStatus string
waitForExpression string
userAgent string
extraHTTPHeaders map[string]string
extraHttpHeaders map[string]string
emulatedMediaType string
landscape, printBackground, omitBackground bool
scale, paperWidth, paperHeight float64
marginTop, marginBottom, marginLeft, marginRight float64
pageRanges string
headerTemplate, footerTemplate string
preferCSSPageSize bool
preferCssPageSize bool
)
form := ctx.FormData().
@@ -47,15 +47,15 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("userAgent", &userAgent, defaultOptions.UserAgent).
String("userAgent", &userAgent, ""). // FIXME: deprecated.
Custom("extraHttpHeaders", func(value string) error {
if value == "" {
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
return nil
}
err := json.Unmarshal([]byte(value), &extraHTTPHeaders)
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
if err != nil {
return fmt.Errorf("unmarshal extra HTTP headers: %w", err)
}
@@ -90,18 +90,26 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate).
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
Bool("preferCssPageSize", &preferCssPageSize, defaultOptions.PreferCssPageSize)
// FIXME: deprecated.
if userAgent != "" {
ctx.Log().Warn("'userAgent' is deprecated; prefer the 'extraHttpHeaders' form field instead")
if extraHttpHeaders == nil {
extraHttpHeaders = make(map[string]string)
}
extraHttpHeaders["User-Agent"] = userAgent
}
options := Options{
FailOnConsoleExceptions: failOnConsoleExceptions,
WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression,
UserAgent: userAgent,
ExtraHTTPHeaders: extraHTTPHeaders,
ExtraLinkTags: defaultOptions.ExtraLinkTags,
ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType,
ExtraScriptTags: defaultOptions.ExtraScriptTags,
Landscape: landscape,
PrintBackground: printBackground,
OmitBackground: omitBackground,
@@ -115,60 +123,36 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
PageRanges: pageRanges,
HeaderTemplate: headerTemplate,
FooterTemplate: footerTemplate,
PreferCSSPageSize: preferCSSPageSize,
PreferCssPageSize: preferCssPageSize,
}
return form, options
}
// convertURLRoute returns an api.Route which can convert a URL to PDF.
func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/url",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx)
form, options := FormDataChromiumPdfOptions(ctx)
var (
URL string
PDFformat string
url string
pdfFormat string
)
err := form.
MandatoryString("url", &URL).
String("pdfFormat", &PDFformat, "").
Custom("extraLinkTags", func(value string) error {
if value == "" {
return nil
}
err := json.Unmarshal([]byte(value), &options.ExtraLinkTags)
if err != nil {
return fmt.Errorf("unmarshal extra link tags: %w", err)
}
return nil
}).
Custom("extraScriptTags", func(value string) error {
if value == "" {
return nil
}
err := json.Unmarshal([]byte(value), &options.ExtraScriptTags)
if err != nil {
return fmt.Errorf("unmarshal extra script tags: %w", err)
}
return nil
}).
MandatoryString("url", &url).
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err)
}
@@ -178,32 +162,33 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
}
}
// convertHTMLRoute returns an api.Route which can convert an HTML file to PDF.
func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
// PDF.
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/html",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx)
form, options := FormDataChromiumPdfOptions(ctx)
var (
inputPath string
PDFformat string
pdfFormat string
)
err := form.
MandatoryPath("index.html", &inputPath).
String("pdfFormat", &PDFformat, "").
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
URL := fmt.Sprintf("file://%s", inputPath)
url := fmt.Sprintf("file://%s", inputPath)
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err)
}
@@ -213,27 +198,27 @@ func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
}
}
// convertMarkdownRoute returns an api.Route which can convert markdown files
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
// to PDF.
func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/markdown",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx)
form, options := FormDataChromiumPdfOptions(ctx)
var (
inputPath string
markdownPaths []string
PDFformat string
pdfFormat string
)
err := form.
MandatoryPath("index.html", &inputPath).
MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &PDFformat, "").
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
@@ -310,9 +295,9 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return fmt.Errorf("write template result: %w", err)
}
URL := fmt.Sprintf("file://%s", inputPath)
url := fmt.Sprintf("file://%s", inputPath)
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err)
}
@@ -322,19 +307,19 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
}
}
// convertURL is a stub which is called by the other methods of this file.
func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, PDFformat string, options Options) error {
// convertUrl is a stub which is called by the other methods of this file.
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url, pdfFormat string, options Options) error {
outputPath := ctx.GeneratePath(".pdf")
err := chromium.PDF(ctx, ctx.Log(), URL, outputPath, options)
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
if err != nil {
if errors.Is(err, ErrURLNotAuthorized) {
if errors.Is(err, ErrUrlNotAuthorized) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError(
http.StatusForbidden,
fmt.Sprintf("'%s' does not match the authorized URLs", URL),
fmt.Sprintf("'%s' does not match the authorized URLs", url),
),
)
}
@@ -403,11 +388,11 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
// Now, let's check if the client want to convert this result PDF
// to a specific PDF format.
if PDFformat != "" {
if pdfFormat != "" {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
err = engine.Convert(ctx, ctx.Log(), pdfFormat, convertInputPath, convertOutputPath)
if err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
@@ -415,7 +400,7 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError(
http.StatusBadRequest,
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", pdfFormat),
),
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,319 @@
package chromium
import (
"bufio"
"context"
"fmt"
"os"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
"go.uber.org/zap"
)
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options) chromedp.ActionFunc {
return func(ctx context.Context) error {
printToPdf := page.PrintToPDF().
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
WithLandscape(options.Landscape).
WithPrintBackground(options.PrintBackground).
WithScale(options.Scale).
WithPaperWidth(options.PaperWidth).
WithPaperHeight(options.PaperHeight).
WithMarginTop(options.MarginTop).
WithMarginBottom(options.MarginBottom).
WithMarginLeft(options.MarginLeft).
WithMarginRight(options.MarginRight).
WithPageRanges(options.PageRanges).
WithPreferCSSPageSize(options.PreferCssPageSize)
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
options.FooterTemplate != DefaultOptions().FooterTemplate
if !hasCustomHeaderFooter {
logger.Debug("no custom header nor footer")
printToPdf = printToPdf.WithDisplayHeaderFooter(false)
} else {
logger.Debug("with custom header and/or footer")
printToPdf = printToPdf.
WithDisplayHeaderFooter(true).
WithHeaderTemplate(options.HeaderTemplate).
WithFooterTemplate(options.FooterTemplate)
}
logger.Debug(fmt.Sprintf("print to Pdf with: %+v", printToPdf))
_, stream, err := printToPdf.Do(ctx)
if err != nil {
return fmt.Errorf("print to Pdf: %w", err)
}
reader := &streamReader{
ctx: ctx,
handle: stream,
r: nil,
pos: 0,
eof: false,
}
defer func() {
err := reader.Close()
if err != nil {
logger.Error(fmt.Sprintf("close reader: %s", err))
}
}()
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("open output path: %w", err)
}
defer func() {
err := file.Close()
if err != nil {
logger.Error(fmt.Sprintf("close output path: %s", err))
}
}()
buffer := bufio.NewReader(reader)
_, err = buffer.WriteTo(file)
if err != nil {
return fmt.Errorf("write result to output path: %w", err)
}
return nil
}
}
func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.ActionFunc {
return func(ctx context.Context) error {
// See https://github.com/gotenberg/gotenberg/issues/175.
if !disable {
logger.Debug("JavaScript not disabled")
return nil
}
logger.Debug("disable JavaScript")
err := emulation.SetScriptExecutionDisabled(true).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("disable JavaScript: %w", err)
}
}
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if len(extraHttpHeaders) == 0 {
logger.Debug("no extra HTTP headers")
return nil
}
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
headers := make(network.Headers, len(extraHttpHeaders))
for key, value := range extraHttpHeaders {
headers[key] = value
}
err := network.SetExtraHTTPHeaders(headers).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("set extra HTTP headers: %w", err)
}
}
func navigateActionFunc(logger *zap.Logger, url string) chromedp.ActionFunc {
return func(ctx context.Context) error {
logger.Debug(fmt.Sprintf("navigate to '%s'", url))
_, _, _, err := page.Navigate(url).Do(ctx)
if err != nil {
return fmt.Errorf("navigate to '%s': %w", url, err)
}
err = runBatch(
ctx,
waitForEventDomContentEventFired(ctx, logger),
waitForEventLoadEventFired(ctx, logger),
waitForEventNetworkIdle(ctx, logger),
waitForEventLoadingFinished(ctx, logger),
)
if err == nil {
return nil
}
return fmt.Errorf("wait for events: %w", err)
}
}
func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, printBackground bool) chromedp.ActionFunc {
return func(ctx context.Context) error {
// See https://github.com/gotenberg/gotenberg/issues/226.
if !omitBackground {
logger.Debug("default white background not hidden")
return nil
}
if !printBackground {
// See https://github.com/chromedp/chromedp/issues/1179#issuecomment-1284794416.
return fmt.Errorf("validate omit background: %w", ErrOmitBackgroundWithoutPrintBackground)
}
logger.Debug("hide default white background")
err := emulation.SetDefaultBackgroundColorOverride().WithColor(
&cdp.RGBA{
R: 0,
G: 0,
B: 0,
A: 0,
}).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("hide default white background: %w", err)
}
}
func forceExactColorsActionFunc() chromedp.ActionFunc {
return func(ctx context.Context) error {
// See:
// https://github.com/gotenberg/gotenberg/issues/354
// https://github.com/puppeteer/puppeteer/issues/2685
// https://github.com/chromedp/chromedp/issues/520
script := `
(() => {
const css = 'html { -webkit-print-color-adjust: exact !important; }';
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
})();
`
evaluate := chromedp.Evaluate(script, nil)
err := evaluate.Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("add CSS for exact colors: %w", err)
}
}
func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if mediaType == "" {
logger.Debug("no emulated media type")
return nil
}
if mediaType != "screen" && mediaType != "print" {
return fmt.Errorf("validate emulated media type '%s': %w", mediaType, ErrInvalidEmulatedMediaType)
}
logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType))
emulatedMedia := emulation.SetEmulatedMedia()
err := emulatedMedia.WithMedia(mediaType).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("emulate media type '%s': %w", mediaType, err)
}
}
func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, delay time.Duration) chromedp.ActionFunc {
return func(ctx context.Context) error {
if disableJavaScript {
logger.Debug("JavaScript disabled, skipping wait delay")
return nil
}
if delay <= 0 {
logger.Debug("no wait delay")
return nil
}
// We wait for a given amount of time so that JavaScript
// scripts have a chance to finish before printing the page.
logger.Debug(fmt.Sprintf("wait '%s' before print", delay))
select {
case <-ctx.Done():
return fmt.Errorf("wait delay: %w", ctx.Err())
case <-time.After(delay):
return nil
}
}
}
func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, expression string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if disableJavaScript {
logger.Debug("JavaScript disabled, skipping wait expression")
return nil
}
if expression == "" {
logger.Debug("no wait expression")
return nil
}
// We wait until the evaluation of the expression is true or
// until the context is done.
logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression))
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
for {
select {
case <-ctx.Done():
ticker.Stop()
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
case <-ticker.C:
var ok bool
evaluate := chromedp.Evaluate(expression, &ok)
err := evaluate.Do(ctx)
if err != nil {
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
}
if ok {
ticker.Stop()
return nil
}
continue
}
}
}
}

View File

@@ -29,7 +29,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{
name: "nominal behavior",
ctx: func() *gotenberg.Context {
provider1 := struct {
provider1 := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}
@@ -42,7 +42,7 @@ func TestLibreOffice_Provision(t *testing.T) {
return uno.APIMock{}, nil
}
provider2 := struct {
provider2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineProviderMock
}{}
@@ -52,7 +52,7 @@ func TestLibreOffice_Provision(t *testing.T) {
}}
}
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil
return &gotenberg.PDFEngineMock{}, nil
}
return gotenberg.NewContext(
@@ -79,7 +79,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{
name: "no API from UNO API provider",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}
@@ -106,7 +106,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{
name: "no PDF engine provider",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}
@@ -133,7 +133,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{
name: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context {
provider1 := struct {
provider1 := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}
@@ -146,7 +146,7 @@ func TestLibreOffice_Provision(t *testing.T) {
return uno.APIMock{}, nil
}
provider2 := struct {
provider2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineProviderMock
}{}
@@ -156,7 +156,7 @@ func TestLibreOffice_Provision(t *testing.T) {
}}
}
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, errors.New("foo")
return &gotenberg.PDFEngineMock{}, errors.New("foo")
}
return gotenberg.NewContext(

View File

@@ -32,7 +32,7 @@ func TestUNO_Provider(t *testing.T) {
{
name: "nominal behavior",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}
@@ -68,7 +68,7 @@ func TestUNO_Provider(t *testing.T) {
{
name: "no API from UNO API provider",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
uno.ProviderMock
}{}

View File

@@ -270,7 +270,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -304,7 +304,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
@@ -341,7 +341,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -381,7 +381,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -421,7 +421,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -461,7 +461,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -493,7 +493,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -526,7 +526,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -558,7 +558,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
@@ -590,7 +590,7 @@ func TestConvertHandler(t *testing.T) {
}
},
},
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},

View File

@@ -35,7 +35,7 @@ func TestUNO_Provision(t *testing.T) {
{
name: "nominal behavior",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -61,7 +61,7 @@ func TestUNO_Provision(t *testing.T) {
{
name: "threshold from deprecated flag --unoconv-disable-listener",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -107,7 +107,7 @@ func TestUNO_Provision(t *testing.T) {
{
name: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}

View File

@@ -1,3 +1,3 @@
// Package logging provides a module which creates a zap.Logger for other
// Package logging provides a module which creates a [zap.Logger] for other
// modules.
package logging

View File

@@ -15,7 +15,7 @@ import (
)
func init() {
gotenberg.MustRegisterModule(Logging{})
gotenberg.MustRegisterModule(new(Logging))
}
const (
@@ -31,15 +31,16 @@ const (
textLoggingFormat = "text"
)
// Logging is a module which implements the gotenberg.LoggerProvider interface.
// Logging is a module which implements the [gotenberg.LoggerProvider]
// interface.
type Logging struct {
level string
format string
fieldsPrefix string
}
// Descriptor returns a Logging's module descriptor.
func (Logging) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a [Logging]'s module descriptor.
func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "logging",
FlagSet: func() *flag.FlagSet {
@@ -66,7 +67,7 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
}
// Validate validates the log level and format.
func (log Logging) Validate() error {
func (log *Logging) Validate() error {
var err error
switch log.level {
@@ -92,8 +93,8 @@ func (log Logging) Validate() error {
return err
}
// Logger returns a zap.Logger.
func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
// Logger returns a [zap.Logger].
func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
if logger == nil {
lvl, err := newLogLevel(log.level)
if err != nil {

View File

@@ -13,7 +13,7 @@ import (
)
func TestLogging_Descriptor(t *testing.T) {
descriptor := Logging{}.Descriptor()
descriptor := new(Logging).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Logging))
@@ -56,66 +56,68 @@ func TestLogging_Provision(t *testing.T) {
expectFieldsPrefix: "",
},
} {
var flags []string
t.Run(tc.scenario, func(t *testing.T) {
var flags []string
if tc.level != "" {
flags = append(flags, "--log-level", tc.level)
}
if tc.level != "" {
flags = append(flags, "--log-level", tc.level)
}
if tc.format != "" {
flags = append(flags, "--log-format", tc.format)
}
if tc.format != "" {
flags = append(flags, "--log-format", tc.format)
}
if tc.fieldsPrefix != "" {
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
}
if tc.fieldsPrefix != "" {
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
}
logging := new(Logging)
fs := logging.Descriptor().FlagSet
logging := new(Logging)
fs := logging.Descriptor().FlagSet
err := fs.Parse(flags)
if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
}
err := fs.Parse(flags)
if err != nil {
t.Fatalf("expected no error while parsing flags but got: %v", err)
}
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
err = logging.Provision(ctx)
if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
}
err = logging.Provision(ctx)
if err != nil {
t.Fatalf("expected no error while provisioning but got: %v", err)
}
if logging.level != tc.expectLevel {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectLevel, logging.level)
}
if logging.level != tc.expectLevel {
t.Errorf("expected logging level '%s' but got '%s'", tc.expectLevel, logging.level)
}
if logging.format != tc.expectFormat {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFormat, logging.format)
}
if logging.format != tc.expectFormat {
t.Errorf("expected logging format '%s' but got '%s'", tc.expectFormat, logging.format)
}
if logging.fieldsPrefix != tc.expectFieldsPrefix {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFieldsPrefix, logging.fieldsPrefix)
}
if logging.fieldsPrefix != tc.expectFieldsPrefix {
t.Errorf("expected logging fields prefix '%s' but got '%s'", tc.expectFieldsPrefix, logging.fieldsPrefix)
}
})
}
}
func TestLogging_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
level string
format string
expectErr bool
scenario string
level string
format string
expectError bool
}{
{
scenario: "invalid level",
level: "foo",
expectErr: true,
scenario: "invalid level",
level: "foo",
expectError: true,
},
{
scenario: "invalid format",
level: debugLoggingLevel,
format: "foo",
expectErr: true,
scenario: "invalid format",
level: debugLoggingLevel,
format: "foo",
expectError: true,
},
{
scenario: "valid level and format",
@@ -129,11 +131,11 @@ func TestLogging_Validate(t *testing.T) {
err := logging.Validate()
if tc.expectErr && err == nil {
if tc.expectError && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
}
if !tc.expectErr && err != nil {
if !tc.expectError && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
}
}
@@ -145,18 +147,18 @@ func TestLogging_Logger(t *testing.T) {
level string
format string
fieldsPrefix string
expectErr bool
expectError bool
}{
{
scenario: "invalid level",
level: "foo",
expectErr: true,
scenario: "invalid level",
level: "foo",
expectError: true,
},
{
scenario: "invalid format",
level: debugLoggingLevel,
format: "foo",
expectErr: true,
scenario: "invalid format",
level: debugLoggingLevel,
format: "foo",
expectError: true,
},
{
scenario: "valid level and format",
@@ -164,24 +166,26 @@ func TestLogging_Logger(t *testing.T) {
format: autoLoggingFormat,
},
} {
logging := new(Logging)
logging.level = tc.level
logging.format = tc.format
logging.fieldsPrefix = tc.fieldsPrefix
t.Run(tc.scenario, func(t *testing.T) {
logging := new(Logging)
logging.level = tc.level
logging.format = tc.format
logging.fieldsPrefix = tc.fieldsPrefix
_, err := logging.Logger(gotenberg.ModuleMock{
DescriptorMock: func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
},
_, err := logging.Logger(&gotenberg.ModuleMock{
DescriptorMock: func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
},
})
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
if tc.expectErr && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
}
if !tc.expectErr && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
}
}
}
@@ -208,44 +212,46 @@ func TestCustomCore(t *testing.T) {
level: zapcore.ErrorLevel,
},
} {
core, obsvr := observer.New(tc.level)
lgr := zap.New(customCore{
Core: core,
fieldsPrefix: tc.fieldsPrefix,
}).With(zap.String("a_field", "a value"))
t.Run(tc.scenario, func(t *testing.T) {
core, obsvr := observer.New(tc.level)
lgr := zap.New(customCore{
Core: core,
fieldsPrefix: tc.fieldsPrefix,
}).With(zap.String("a_field", "a value"))
lgr.Debug("a debug message", zap.String("another_field", "another value"))
lgr.Debug("a debug message", zap.String("another_field", "another value"))
entries := obsvr.TakeAll()
entries := obsvr.TakeAll()
if tc.expectEntry && len(entries) == 0 {
t.Fatalf("%s: expected an entry", tc.scenario)
}
if !tc.expectEntry && len(entries) != 0 {
t.Fatalf("%s: expected no entry", tc.scenario)
}
var prefix string
if tc.fieldsPrefix != "" {
prefix = tc.fieldsPrefix + "_"
}
for _, entry := range entries {
fields := entry.Context
if len(fields) != 2 {
t.Fatalf("expected 2 fields but got %d", len(fields))
if tc.expectEntry && len(entries) == 0 {
t.Fatal("expected an entry")
}
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
if !tc.expectEntry && len(entries) != 0 {
t.Fatal("expected no entry")
}
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
var prefix string
if tc.fieldsPrefix != "" {
prefix = tc.fieldsPrefix + "_"
}
}
for _, entry := range entries {
fields := entry.Context
if len(fields) != 2 {
t.Fatalf("expected 2 fields but got %d", len(fields))
}
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
}
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
}
}
})
}
}
@@ -254,7 +260,7 @@ func Test_newLogLevel(t *testing.T) {
scenario string
level string
expectZapLevel zapcore.Level
expectErr bool
expectError bool
}{
{
scenario: "error level",
@@ -280,30 +286,32 @@ func Test_newLogLevel(t *testing.T) {
scenario: "invalid level",
level: "foo",
expectZapLevel: zapcore.InvalidLevel,
expectErr: true,
expectError: true,
},
} {
actual, err := newLogLevel(tc.level)
t.Run(tc.scenario, func(t *testing.T) {
actual, err := newLogLevel(tc.level)
if tc.expectErr && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if !tc.expectErr && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectZapLevel != actual {
t.Errorf("%s: expected %d level but got %d", tc.scenario, tc.expectZapLevel, actual)
}
if tc.expectZapLevel != actual {
t.Errorf("expected %d level but got %d", tc.expectZapLevel, actual)
}
})
}
}
func Test_newLogEncoder(t *testing.T) {
for _, tc := range []struct {
scenario string
format string
expectErr bool
scenario string
format string
expectError bool
}{
{
scenario: "auto format",
@@ -318,19 +326,21 @@ func Test_newLogEncoder(t *testing.T) {
format: jsonLoggingFormat,
},
{
scenario: "invalid format",
format: "foo",
expectErr: true,
scenario: "invalid format",
format: "foo",
expectError: true,
},
} {
_, err := newLogEncoder(tc.format)
t.Run(tc.scenario, func(t *testing.T) {
_, err := newLogEncoder(tc.format)
if tc.expectErr && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if !tc.expectErr && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
}
if !tc.expectError && err != nil {
t.Errorf("expected no error but got: %v", err)
}
})
}
}

View File

@@ -40,7 +40,7 @@ func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
return nil
}
// Merge merges the given PDFs into a unique PDF.
// Merge merges the given PDFs into a unique Pdf.
func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
if err == nil {
@@ -50,9 +50,9 @@ func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
}
// Convert is not available for this PDF engine.
// Convert is not available for this Pdf engine.
func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
return fmt.Errorf("convert PDF to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
return fmt.Errorf("convert Pdf to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
}
// Interface guards.

View File

@@ -20,7 +20,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{
name: "nominal behavior",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -31,12 +31,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -47,12 +47,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{
name: "all engines return an error",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
@@ -64,7 +64,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{
name: "context expired",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -105,7 +105,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{
name: "nominal behavior",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -116,12 +116,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
},
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -132,12 +132,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{
name: "all engines return an error",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
},
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
@@ -149,7 +149,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{
name: "context expired",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},

View File

@@ -32,7 +32,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{
name: "no selection from user",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -45,7 +45,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil
}
engine := struct {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
@@ -72,7 +72,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{
name: "selection from user",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -85,7 +85,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil
}
engine1 := struct {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
@@ -97,7 +97,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return nil
}
engine2 := struct {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
@@ -131,7 +131,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{
name: "user select deprecated unoconv-pdfengine",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -144,7 +144,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil
}
engine := struct {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
@@ -189,7 +189,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{
name: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -216,7 +216,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{
name: "no valid PDF engines",
ctx: func() *gotenberg.Context {
provider := struct {
provider := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
@@ -229,7 +229,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil
}
engine := struct {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
@@ -292,7 +292,7 @@ func TestPDFEngines_Validate(t *testing.T) {
name: "existing PDF engine",
names: []string{"foo"},
engines: func() []gotenberg.PDFEngine {
engine := struct {
engine := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
@@ -309,7 +309,7 @@ func TestPDFEngines_Validate(t *testing.T) {
name: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PDFEngine {
engine1 := struct {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
@@ -317,7 +317,7 @@ func TestPDFEngines_Validate(t *testing.T) {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
}
engine2 := struct {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
@@ -377,7 +377,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
mod := PDFEngines{
names: []string{"foo", "bar"},
engines: func() []gotenberg.PDFEngine {
engine1 := struct {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
@@ -385,7 +385,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
}
engine2 := struct {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
@@ -416,7 +416,7 @@ func TestPDFEngines_Routes(t *testing.T) {
name: "route not disabled",
mod: PDFEngines{
engines: []gotenberg.PDFEngine{
gotenberg.PDFEngineMock{},
&gotenberg.PDFEngineMock{},
},
},
expectRoutesCount: 2,

View File

@@ -33,7 +33,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -57,7 +57,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
@@ -79,7 +79,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -104,7 +104,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -129,7 +129,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -152,7 +152,7 @@ func TestMergeHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -226,7 +226,7 @@ func TestConvertHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -250,7 +250,7 @@ func TestConvertHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
@@ -302,7 +302,7 @@ func TestConvertHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
@@ -324,7 +324,7 @@ func TestConvertHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
@@ -349,7 +349,7 @@ func TestConvertHandler(t *testing.T) {
return ctx
}(),
engine: gotenberg.PDFEngineMock{
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},