package libreoffice import ( "context" "errors" "net/http" "testing" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/labstack/echo/v4" "go.uber.org/zap" ) func TestConvertHandler(t *testing.T) { tests := []struct { name string ctx *api.MockContext unoAPI uno.API engine gotenberg.PDFEngine expectErr bool expectHTTPErr bool expectHTTPStatus int expectOutputPathsCount int }{ { name: "nominal behavior", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectOutputPathsCount: 1, }, { name: "nominal behavior, but with 3 documents", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectOutputPathsCount: 3, }, { name: "cannot add output paths", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetCancelled(true) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, }, { name: "invalid form data: no documents", ctx: &api.MockContext{Context: &api.Context{}}, unoAPI: uno.APIMock{ ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, { name: "invalid form data: both nativePdfA1aFormat and nativePdfFormat are set", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "nativePdfA1aFormat": { "true", }, "nativePdfFormat": { gotenberg.FormatPDFA1a, }, }) ctx.SetLogger(zap.NewNop()) return ctx }(), unoAPI: uno.APIMock{ ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, { name: "invalid form data: both nativePdfA1aFormat and pdfFormat are set", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "nativePdfA1aFormat": { "true", }, "pdfFormat": { gotenberg.FormatPDFA1a, }, }) ctx.SetLogger(zap.NewNop()) return ctx }(), unoAPI: uno.APIMock{ ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, { name: "invalid form data: both nativePdfFormat and pdfFormat are set", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "nativePdfFormat": { gotenberg.FormatPDFA1a, }, "pdfFormat": { gotenberg.FormatPDFA1a, }, }) return ctx }(), unoAPI: uno.APIMock{ ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, { name: "convert to PDF fail", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return errors.New("foo") }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, }, { name: "invalid page ranges", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return uno.ErrMalformedPageRanges }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, }, { name: "convert 3 documents and merge them", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { return nil }, }, expectOutputPathsCount: 1, }, { name: "merge fail", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { return errors.New("foo") }, }, expectErr: true, }, { name: "convert 3 documents, merge them, and convert them to a PDF format", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, "pdfFormat": { gotenberg.FormatPDFA1a, }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, 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 3 documents, merge them, but convert them to PDF format fail", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, "pdfFormat": { gotenberg.FormatPDFA1a, }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, 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: "convert 3 documents, merge them, but PDF format not available", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, "pdfFormat": { "foo", }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, 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: "convert 3 documents and merge them, but cannot add output paths", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", "bar.docx": "/bar/bar.docx", "baz.docx": "/baz/baz.docx", }) ctx.SetValues(map[string][]string{ "merge": { "true", }, }) ctx.SetCancelled(true) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { return nil }, }, expectErr: true, }, { name: "convert to PDF format", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "pdfFormat": { gotenberg.FormatPDFA1a, }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { return nil }, }, expectOutputPathsCount: 1, }, { name: "convert to PDF format using nativePdfA1aFormat", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "nativePdfA1aFormat": { "true", }, }) ctx.SetLogger(zap.NewNop()) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ 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.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "pdfFormat": { gotenberg.FormatPDFA1a, }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, engine: gotenberg.PDFEngineMock{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { return errors.New("foo") }, }, expectErr: true, }, { name: "PDF format not available", ctx: func() *api.MockContext { ctx := &api.MockContext{Context: &api.Context{}} ctx.SetFiles(map[string]string{ "foo.docx": "/foo/foo.docx", }) ctx.SetValues(map[string][]string{ "pdfFormat": { "foo", }, }) return ctx }(), unoAPI: uno.APIMock{ PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error { return nil }, ExtensionsMock: func() []string { return []string{ ".docx", } }, }, 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, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := echo.New().NewContext(nil, nil) c.Set("context", tc.ctx.Context) err := convertRoute(tc.unoAPI, tc.engine).Handler(c) if tc.expectErr && err == nil { t.Fatal("expected error from convert handler, but got none") } if !tc.expectErr && err != nil { t.Fatalf("expected no error from convert handler, 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.expectHTTPErr && isHTTPErr { t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr) } if err != nil && tc.expectHTTPErr && 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 tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) } }) } }