feat(pdfengines): add PDF/UA (#714)

This commit is contained in:
Julien Neuhart
2023-11-05 20:14:36 +01:00
committed by GitHub
parent 9c3dfc78df
commit 143b7ce678
39 changed files with 1300 additions and 1013 deletions

View File

@@ -1,3 +1,3 @@
// Package pdfengines provides a module which gathers modules that implements
// the gotenberg.PDFEngine interface.
// Package pdfengines a way to gather and manage multiple modules that
// implement the gotenberg.PdfEngine interface.
package pdfengines

View File

@@ -10,30 +10,24 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one
// or more gotenberg.PDFEngine. It provides a sort of fallback mechanism: if an
// engine's method returns an error, it calls the same method from another
// engine.
type multiPDFEngines struct {
engines []gotenberg.PDFEngine
type multiPdfEngines struct {
engines []gotenberg.PdfEngine
}
// newMultiPDFEngines returns a multiPDFEngines. Arguments' order determines the
// order of the engines called.
func newMultiPDFEngines(engines ...gotenberg.PDFEngine) *multiPDFEngines {
return &multiPDFEngines{
func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
return &multiPdfEngines{
engines: engines,
}
}
// Merge tries to merge the given PDFs into a unique PDF thanks to its
// children. If the context is done, it stops and returns an error.
func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
}(engine)
@@ -53,13 +47,13 @@ func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inpu
// Convert converts the given PDF to a specific PDF format. thanks to its
// children. If the context is done, it stops and returns an error.
func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) {
errChan <- engine.Convert(ctx, logger, format, inputPath, outputPath)
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
}(engine)
select {
@@ -73,10 +67,10 @@ func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, fo
}
}
return fmt.Errorf("convert PDF to '%s' with multi PDF engines: %w", format, err)
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
}
// Interface guards.
var (
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil)
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
)

View File

@@ -10,61 +10,63 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestMultiPDFEngines_Merge(t *testing.T) {
tests := []struct {
name string
engine *multiPDFEngines
ctx context.Context
expectMergeErr bool
func TestMultiPdfEngines_Merge(t *testing.T) {
for _, tc := range []struct {
scenario string
engine *multiPdfEngines
ctx context.Context
expectError bool
}{
{
name: "nominal behavior",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "nominal behavior",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
),
ctx: context.Background(),
ctx: context.Background(),
expectError: false,
},
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines(
&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
},
},
),
ctx: context.Background(),
ctx: context.Background(),
expectError: false,
},
{
name: "all engines return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "all engines return an error",
engine: newMultiPdfEngines(
&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")
},
},
),
ctx: context.Background(),
expectMergeErr: true,
ctx: context.Background(),
expectError: true,
},
{
name: "context expired",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "context expired",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -76,37 +78,35 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
return ctx
}(),
expectMergeErr: true,
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
if tc.expectMergeErr && err == nil {
t.Errorf("expected engine.Merge() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectMergeErr && err != nil {
t.Errorf("expected no error from engine.Merge(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestMultiPDFEngines_Convert(t *testing.T) {
tests := []struct {
name string
engine *multiPDFEngines
ctx context.Context
expectConvertErr bool
func TestMultiPdfEngines_Convert(t *testing.T) {
for _, tc := range []struct {
scenario string
engine *multiPdfEngines
ctx context.Context
expectError bool
}{
{
name: "nominal behavior",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "nominal behavior",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -114,15 +114,15 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
ctx: context.Background(),
},
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -130,27 +130,27 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
ctx: context.Background(),
},
{
name: "all engines return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "all engines return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
),
ctx: context.Background(),
expectConvertErr: true,
ctx: context.Background(),
expectError: true,
},
{
name: "context expired",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "context expired",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -161,20 +161,18 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
return ctx
}(),
expectConvertErr: true,
expectError: true,
},
}
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.Convert(tc.ctx, zap.NewNop(), gotenberg.PdfFormats{}, "", "")
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.engine.Convert(tc.ctx, zap.NewNop(), "", "", "")
if tc.expectConvertErr && err == nil {
t.Errorf("expected engine.Convert() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectConvertErr && err != nil {
t.Errorf("expected no error from engine.Convert(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}

View File

@@ -12,28 +12,28 @@ import (
)
func init() {
gotenberg.MustRegisterModule(PDFEngines{})
gotenberg.MustRegisterModule(new(PdfEngines))
}
// PDFEngines is a module which gathers available gotenberg.PDFEngine modules.
// The available gotenberg.PDFEngine modules can be either all
// gotenberg.PDFEngine modules or the modules selected by the user thanks to
// the "engines" flag.
// PdfEngines acts as an aggregator and manager for multiple PDF engine
// modules. It enables the selection and ordering of PDF engines based on user
// preferences passed via command-line flags. The [PdfEngines] module also
// implements the [gotenberg.PdfEngine] interface, providing a unified approach
// to PDF processing across the various engines it manages.
//
// PDFEngines wraps the gotenberg.PDFEngine modules in an internal struct which
// also implements gotenberg.PDFEngine. This struct provides a sort of fallback
// mechanism: if an engine's method returns an error, it calls the same method
// from another engine.
//
// This module implements the gotenberg.PDFEngineProvider interface.
type PDFEngines struct {
// When processing PDFs, [PdfEngines] will attempt to use the engines in the
// order they were defined. If the primary engine encounters an error,
// [PdfEngines] can fall back to the next available engine. It also implements
// the [api.Router] interface to expose relevant PDF processing routes if
// enabled.
type PdfEngines struct {
names []string
engines []gotenberg.PDFEngine
engines []gotenberg.PdfEngine
disableRoutes bool
}
// Descriptor returns a PDFEngines' module descriptor.
func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a PdfEngines' module descriptor.
func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "pdfengines",
FlagSet: func() *flag.FlagSet {
@@ -43,13 +43,13 @@ func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
return fs
}(),
New: func() gotenberg.Module { return new(PDFEngines) },
New: func() gotenberg.Module { return new(PdfEngines) },
}
}
// Provision gets either all gotenberg.PDFEngine modules or the modules
// Provision gets either all [gotenberg.PdfEngine] modules or the modules
// selected by the user thanks to the "engines" flag.
func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
names := flags.MustStringSlice("pdfengines-engines")
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
@@ -66,15 +66,15 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
logger = logger.Named("pdfengines")
engines, err := ctx.Modules(new(gotenberg.PDFEngine))
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
if err != nil {
return fmt.Errorf("get PDF engines: %w", err)
}
mod.engines = make([]gotenberg.PDFEngine, len(engines))
mod.engines = make([]gotenberg.PdfEngine, len(engines))
for i, engine := range engines {
mod.engines[i] = engine.(gotenberg.PDFEngine)
mod.engines[i] = engine.(gotenberg.PdfEngine)
}
if len(names) > 0 {
@@ -82,9 +82,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
mod.names = names
for i, name := range names {
logger.Warn("unoconv-pdfengine is deprecated; prefer api-pdfengine instead")
if name == "unoconv-pdfengine" {
mod.names[i] = "api-pdfengine"
// FIXME: deprecated.
if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
mod.names[i] = "libreoffice-pdfengine"
}
}
@@ -101,10 +102,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
return nil
}
// Validate validates there is at least one gotenberg.PDFEngine module
// available. It also validates that selected gotenberg.PDFEngine modules
// Validate validates there is at least one [gotenberg.PdfEngine] module
// available. It also validates that selected [gotenberg.PdfEngine] modules
// actually exist.
func (mod PDFEngines) Validate() error {
func (mod *PdfEngines) Validate() error {
if len(mod.engines) == 0 {
return errors.New("no PDF engine")
}
@@ -139,17 +140,17 @@ func (mod PDFEngines) Validate() error {
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
}
// SystemMessages returns one message with the selected gotenberg.PDFEngine
// SystemMessages returns one message with the selected [gotenberg.PdfEngine]
// modules.
func (mod PDFEngines) SystemMessages() []string {
func (mod *PdfEngines) SystemMessages() []string {
return []string{
strings.Join(mod.names[:], " "),
}
}
// PDFEngine returns a gotenberg.PDFEngine.
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
engines := make([]gotenberg.PDFEngine, len(mod.names))
// PdfEngine returns a [gotenberg.PdfEngine].
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
engines := make([]gotenberg.PdfEngine, len(mod.names))
for i, name := range mod.names {
for _, engine := range mod.engines {
@@ -160,20 +161,20 @@ func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
}
}
return newMultiPDFEngines(engines...), nil
return newMultiPdfEngines(engines...), nil
}
// Routes returns the HTTP routes.
func (mod PDFEngines) Routes() ([]api.Route, error) {
func (mod *PdfEngines) Routes() ([]api.Route, error) {
if mod.disableRoutes {
return nil, nil
}
engine, err := mod.PDFEngine()
engine, err := mod.PdfEngine()
if err != nil {
// Should not happen, unless our provider implementation
// changes in the future.
return nil, fmt.Errorf("get pdf engine: %w", err)
return nil, fmt.Errorf("get pdf mod: %w", err)
}
return []api.Route{
@@ -184,10 +185,10 @@ func (mod PDFEngines) Routes() ([]api.Route, error) {
// Interface guards.
var (
_ gotenberg.Module = (*PDFEngines)(nil)
_ gotenberg.Provisioner = (*PDFEngines)(nil)
_ gotenberg.Validator = (*PDFEngines)(nil)
_ gotenberg.SystemLogger = (*PDFEngines)(nil)
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
_ api.Router = (*PDFEngines)(nil)
_ gotenberg.Module = (*PdfEngines)(nil)
_ gotenberg.Provisioner = (*PdfEngines)(nil)
_ gotenberg.Validator = (*PdfEngines)(nil)
_ gotenberg.SystemLogger = (*PdfEngines)(nil)
_ gotenberg.PdfEngineProvider = (*PdfEngines)(nil)
_ api.Router = (*PdfEngines)(nil)
)

View File

@@ -11,26 +11,26 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestPDFEngines_Descriptor(t *testing.T) {
descriptor := PDFEngines{}.Descriptor()
func TestPdfEngines_Descriptor(t *testing.T) {
descriptor := new(PdfEngines).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(PDFEngines))
expect := reflect.TypeOf(new(PdfEngines))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestPDFEngines_Provision(t *testing.T) {
tests := []struct {
name string
ctx *gotenberg.Context
expectPDFEngineNames []string
expectProvisionErr bool
func TestPdfEngines_Provision(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *gotenberg.Context
expectedPdfEngines []string
expectError bool
}{
{
name: "no selection from user",
scenario: "no selection from user",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -48,7 +48,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
@@ -59,7 +59,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
@@ -67,10 +67,11 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"bar"},
expectedPdfEngines: []string{"bar"},
expectError: false,
},
{
name: "selection from user",
scenario: "selection from user",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -88,7 +89,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
@@ -100,7 +101,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
@@ -109,10 +110,10 @@ func TestPDFEngines_Provision(t *testing.T) {
return nil
}
fs := new(PDFEngines).Descriptor().FlagSet
fs := new(PdfEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
@@ -126,10 +127,11 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"b", "a"},
expectedPdfEngines: []string{"b", "a"},
expectError: false,
},
{
name: "user select deprecated unoconv-pdfengine",
scenario: "user select deprecated unoconv-pdfengine",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -147,19 +149,19 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "api-pdfengine", New: func() gotenberg.Module { return engine }}
return gotenberg.ModuleDescriptor{ID: "libreoffice-pdfengine", New: func() gotenberg.Module { return engine }}
}
engine.ValidateMock = func() error {
return nil
}
fs := new(PDFEngines).Descriptor().FlagSet
fs := new(PdfEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
@@ -172,22 +174,23 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"api-pdfengine"},
expectedPdfEngines: []string{"libreoffice-pdfengine"},
expectError: false,
},
{
name: "no logger provider",
scenario: "no logger provider",
ctx: func() *gotenberg.Context {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{},
)
}(),
expectProvisionErr: true,
expectError: true,
},
{
name: "no logger from logger provider",
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -204,17 +207,17 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
expectProvisionErr: true,
expectError: true,
},
{
name: "no valid PDF engines",
scenario: "no valid PDF engine",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -232,7 +235,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
@@ -243,7 +246,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
@@ -251,67 +254,66 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectProvisionErr: true,
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := new(PDFEngines)
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(PdfEngines)
err := mod.Provision(tc.ctx)
if tc.expectProvisionErr && err == nil {
t.Fatal("expected mod.Provision() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectProvisionErr && err != nil {
t.Fatalf("expected no error from mod.Provision(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if len(tc.expectPDFEngineNames) != len(mod.names) {
t.Errorf("expected %d names but got %d", len(tc.expectPDFEngineNames), len(mod.names))
if len(tc.expectedPdfEngines) != len(mod.names) {
t.Fatalf("expected %d names but got %d", len(tc.expectedPdfEngines), len(mod.names))
}
for index, name := range mod.names {
if name != tc.expectPDFEngineNames[index] {
t.Errorf("expected name at index %d to be %s, but got: %s", index, name, tc.expectPDFEngineNames[index])
if name != tc.expectedPdfEngines[index] {
t.Fatalf("expected scenario at index %d to be %s, but got: %s", index, name, tc.expectedPdfEngines[index])
}
}
})
}
}
func TestPDFEngines_Validate(t *testing.T) {
tests := []struct {
name string
names []string
engines []gotenberg.PDFEngine
expectValidateErr bool
func TestPdfEngines_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
names []string
engines []gotenberg.PdfEngine
expectError bool
}{
{
name: "existing PDF engine",
names: []string{"foo"},
engines: func() []gotenberg.PDFEngine {
scenario: "existing PDF engine",
names: []string{"foo"},
engines: func() []gotenberg.PdfEngine {
engine := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine,
}
}(),
expectError: false,
},
{
name: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PDFEngine {
scenario: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PdfEngine {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
@@ -319,67 +321,65 @@ func TestPDFEngines_Validate(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine1,
engine2,
}
}(),
expectValidateErr: true,
expectError: true,
},
{
name: "no PDF engine",
expectValidateErr: true,
scenario: "no PDF engine",
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := PDFEngines{
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := PdfEngines{
names: tc.names,
engines: tc.engines,
}
err := mod.Validate()
if tc.expectValidateErr && err == nil {
t.Errorf("expected mod.Validate() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectValidateErr && err != nil {
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestPDFEngines_SystemMessages(t *testing.T) {
mod := new(PDFEngines)
func TestPdfEngines_SystemMessages(t *testing.T) {
mod := new(PdfEngines)
mod.names = []string{"foo", "bar"}
messages := mod.SystemMessages()
if len(messages) != 1 {
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
t.Errorf("expected one and only one message, but got %d", len(messages))
}
expect := strings.Join(mod.names[:], " ")
if messages[0] != expect {
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
t.Errorf("expected message '%s', but got '%s'", expect, messages[0])
}
}
func TestPDFEngines_PDFEngine(t *testing.T) {
mod := PDFEngines{
func TestPdfEngines_PdfEngine(t *testing.T) {
mod := PdfEngines{
names: []string{"foo", "bar"},
engines: func() []gotenberg.PDFEngine {
engines: func() []gotenberg.PdfEngine {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
@@ -387,57 +387,53 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine1,
engine2,
}
}(),
}
_, err := mod.PDFEngine()
_, err := mod.PdfEngine()
if err != nil {
t.Errorf("expected no error from mod.PDFEngine, but got: %v", err)
t.Errorf("expected no error but got: %v", err)
}
}
func TestPDFEngines_Routes(t *testing.T) {
tests := []struct {
name string
mod PDFEngines
expectRoutesCount int
func TestPdfEngines_Routes(t *testing.T) {
for _, tc := range []struct {
scenario string
expectRoutes int
disableRoutes bool
}{
{
name: "route not disabled",
mod: PDFEngines{
engines: []gotenberg.PDFEngine{
&gotenberg.PDFEngineMock{},
},
},
expectRoutesCount: 2,
scenario: "routes not disabled",
expectRoutes: 2,
disableRoutes: false,
},
{
name: "route disabled",
mod: PDFEngines{
disableRoutes: true,
},
scenario: "routes disabled",
expectRoutes: 0,
disableRoutes: true,
},
}
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(PdfEngines)
mod.disableRoutes = tc.disableRoutes
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
routes, err := tc.mod.Routes()
routes, err := mod.Routes()
if err != nil {
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectRoutesCount != len(routes) {
t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
if tc.expectRoutes != len(routes) {
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
}
})
}

View File

@@ -11,8 +11,8 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
// mergeRoute returns an api.Route which can merge PDFs.
func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/merge",
@@ -23,17 +23,38 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// Let's get the data from the form and validate them.
var (
inputPaths []string
PDFformat string
pdfFormat string
pdfa string
pdfua bool
)
err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfFormat", &PDFformat, "").
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
var actualPdfArchive string
if pdfFormat != "" {
// FIXME: deprecated
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
}
if pdfa != "" {
actualPdfArchive = pdfa
}
pdfFormats := gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
// Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath(".pdf")
@@ -45,21 +66,21 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// So far so good, the PDFs are merged into one unique PDF.
// Now, let's check if the client want to convert this result PDF
// to a specific PDF format.
if PDFformat != "" {
// to specific PDF formats.
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError(
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 one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
),
)
}
@@ -84,9 +105,9 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
}
}
// convertRoute returns an api.Route which can convert a PDF to a specific PDF
// format.
func convertRoute(engine gotenberg.PDFEngine) api.Route {
// convertRoute returns an [api.Route] which can convert a PDF to a specific
// PDF format.
func convertRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/convert",
@@ -97,33 +118,64 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
// Let's get the data from the form and validate them.
var (
inputPaths []string
PDFformat string
pdfFormat string
pdfa string
pdfua bool
)
err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("pdfFormat", &PDFformat).
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
// Alright, let's merge the PDFs.
var actualPdfArchive string
if pdfFormat != "" {
// FIXME: deprecated.
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
}
if pdfa != "" {
actualPdfArchive = pdfa
}
pdfFormats := gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
zeroValued := gotenberg.PdfFormats{}
if pdfFormats == zeroValued {
return api.WrapError(
errors.New("no PDF formats"),
api.NewSentinelHTTPError(
http.StatusBadRequest,
"Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided",
),
)
}
// Alright, let's convert the PDFs.
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), PDFformat, inputPath, outputPaths[i])
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
if err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError(
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 one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
),
)
}

View File

@@ -14,385 +14,408 @@ import (
)
func TestMergeHandler(t *testing.T) {
tests := []struct {
name string
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
engine gotenberg.PDFEngine
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
engine gotenberg.PdfEngine
expectError bool
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
}{
{
name: "nominal behavior",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
scenario: "missing at least one mandatory file",
ctx: &api.ContextMock{Context: new(api.Context)},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
name: "invalid form data: no PDF",
ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "merge fail",
scenario: "error from PDF engine",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
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")
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
name: "nominal behavior with a PDF format",
scenario: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
},
{
name: "invalid PDF format",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetCancelled(true)
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
{
scenario: "success",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error from PDF engine (convert)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success with every PDF/A & PDF/UA form fields",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
err := mergeRoute(tc.engine).Handler(c)
if tc.expectErr && err == nil {
t.Fatal("expected error from merge handler, but got none")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectErr && err != nil {
t.Fatalf("expected no error from merge handler, but got: %v", err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from merge handler, but got: %v", err)
if tc.expectHttpError && !isHTTPErr {
t.Errorf("expected an HTTP error but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from merge handler, but got one: %v", httpErr)
if !tc.expectHttpError && isHTTPErr {
t.Errorf("expected no HTTP error but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
if err != nil && tc.expectHttpError && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from merge handler, but got %d", tc.expectHTTPStatus, status)
if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from merge handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
}
}
func TestConvertHandler(t *testing.T) {
tests := []struct {
name string
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
engine gotenberg.PDFEngine
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
engine gotenberg.PdfEngine
expectError bool
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
}{
{
name: "nominal behavior",
scenario: "missing at least one mandatory file",
ctx: &api.ContextMock{Context: new(api.Context)},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "no PDF formats",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
},
},
expectOutputPathsCount: 1,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
name: "nominal behavior, but with 3 PDFs",
scenario: "error from PDF engine",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"bar.pdf": "/bar/bar.pdf",
"baz.pdf": "/baz/baz.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 3,
},
{
name: "invalid form data: no PDF",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "invalid form data: no PDF format",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
name: "PDF format not available",
scenario: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
})
ctx.SetCancelled(true)
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
{
scenario: "success with every PDF/A & PDF/UA form fields (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "success with every PDF/A & PDF/UA form fields (many files)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
err := convertRoute(tc.engine).Handler(c)
if tc.expectErr && err == nil {
t.Fatal("expected error from convert handler, but got none")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectErr && err != nil {
t.Fatalf("expected no error from convert handler, but got: %v", err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from convert handler, but got: %v", err)
if tc.expectHttpError && !isHTTPErr {
t.Errorf("expected an HTTP error but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr)
if !tc.expectHttpError && isHTTPErr {
t.Errorf("expected no HTTP error but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
if err != nil && tc.expectHttpError && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from convert handler, but got %d", tc.expectHTTPStatus, status)
if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
}