test: add integration tests

This commit is contained in:
Julien Neuhart
2025-03-21 11:18:20 +01:00
parent 820b914291
commit 0febb60a3f
139 changed files with 7561 additions and 16381 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,837 +0,0 @@
package api
import (
"bytes"
"context"
"errors"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/dlclark/regexp2"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
func TestNewContext(t *testing.T) {
defaultAllowList, err := regexp2.Compile("", 0)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
defaultDenyList, err := regexp2.Compile("", 0)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
defaultDownloadFromCfg := downloadFromConfig{
allowList: defaultAllowList,
denyList: defaultDenyList,
maxRetry: 1,
disable: false,
}
for _, tc := range []struct {
scenario string
request *http.Request
bodyLimit int64
downloadFromCfg downloadFromConfig
downloadFromSrv *echo.Echo
expectContext *Context
expectError bool
expectHttpError bool
expectHttpStatus int
}{
{
scenario: "http.ErrNotMultipart",
request: httptest.NewRequest(http.MethodPost, "/", nil),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusUnsupportedMediaType,
},
{
scenario: "http.ErrMissingBoundary",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)
return req
}(),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusUnsupportedMediaType,
},
{
scenario: "malformed body",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("foo", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "request entity too large: form values",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("key", "value")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
bodyLimit: 1,
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusRequestEntityTooLarge,
},
{
scenario: "request entity too large: downloadFrom",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
bodyLimit: 45, // form values = 44 bytes.
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`)
c.Response().Header().Set(echo.HeaderContentType, "text/plain")
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusRequestEntityTooLarge,
},
{
scenario: "request entity too large: form files",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
_, err = part.Write([]byte("foo"))
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
bodyLimit: 1,
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusRequestEntityTooLarge,
},
{
scenario: "invalid downloadFrom form field: cannot unmarshal",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: no URL",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: filtered URL",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"https://foo.bar"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromCfg: func() downloadFromConfig {
denyList, err := regexp2.Compile("https://foo.bar", 0)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return downloadFromConfig{allowList: defaultAllowList, denyList: denyList, maxRetry: 1, disable: false}
}(),
expectError: true,
},
{
scenario: "invalid downloadFrom form field: unreachable URL",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: invalid status code",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
return c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: no 'Content-Disposition' header",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: malformed 'Content-Disposition' header",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentDisposition, ";;")
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "invalid downloadFrom form field: no filename parameter in 'Content-Disposition' header",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentDisposition, "inline;")
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
},
{
scenario: "success",
request: func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("foo", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
_, err = part.Write([]byte("foo"))
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}(),
downloadFromSrv: func() *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.GET("/", func(c echo.Context) error {
if c.Request().Header.Get("User-Agent") != "Gotenberg" {
t.Fatalf("expected 'Gotenberg' from header 'User-Agent', but got '%s'", c.Request().Header.Get("User-Agent"))
}
if c.Request().Header.Get("X-Foo") != "Bar" {
t.Fatalf("expected 'Bar' from header 'X-Foo', but got '%s'", c.Request().Header.Get("X-Foo"))
}
if c.Request().Header.Get("Gotenberg-Trace") != "123" {
t.Fatalf("expected '123' from header 'Gotenberg-Trace', but got '%s'", c.Request().Header.Get("Gotenberg-Trace"))
}
c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`)
c.Response().Header().Set(echo.HeaderContentType, "text/plain")
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
})
return srv
}(),
downloadFromCfg: defaultDownloadFromCfg,
expectContext: &Context{
values: map[string][]string{
"foo": {"foo"},
"downloadFrom": {
`[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`,
},
},
files: map[string]string{
"foo.txt": "foo.txt",
"bar.txt": "bar.txt", // downloadFrom.
},
},
expectError: false,
expectHttpError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.downloadFromSrv != nil {
go func() {
err := tc.downloadFromSrv.Start(":80")
if !errors.Is(err, http.ErrServerClosed) {
t.Error(err)
return
}
}()
defer func() {
err := tc.downloadFromSrv.Shutdown(context.TODO())
if err != nil {
t.Error(err)
}
}()
}
handler := func(c echo.Context) error {
ctx, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, tc.bodyLimit, tc.downloadFromCfg, "Gotenberg-Trace", "123")
defer cancel()
// Context already cancelled.
defer cancel()
if err != nil {
return err
}
if tc.expectContext != nil {
if !reflect.DeepEqual(tc.expectContext.values, ctx.values) {
t.Fatalf("expected context.values to be %v but got %v", tc.expectContext.values, ctx.values)
}
if len(tc.expectContext.files) != len(ctx.files) {
t.Fatalf("expected context.files to contain %d items but got %d", len(tc.expectContext.files), len(ctx.files))
}
for key, value := range tc.expectContext.files {
if !strings.HasSuffix(ctx.files[key], value) {
t.Fatalf("expected context.files to contain '%s' but got '%s'", value, ctx.files[key])
}
}
}
return nil
}
recorder := httptest.NewRecorder()
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(tc.request, recorder)
err := handler(c)
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
var httpErr HttpError
isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err)
}
if !tc.expectHttpError && isHttpError {
t.Errorf("expected no HTTP error but got one: %v", httpErr)
}
if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
}
}
})
}
}
func TestContext_Request(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/", nil)
recorder := httptest.NewRecorder()
c := echo.New().NewContext(request, recorder)
ctx := &Context{
echoCtx: c,
}
if !reflect.DeepEqual(ctx.Request(), c.Request()) {
t.Errorf("expected %v but got %v", ctx.Request(), c.Request())
}
}
func TestContext_FormData(t *testing.T) {
ctx := &Context{
values: map[string][]string{
"foo": {"foo"},
},
files: map[string]string{
"foo.txt": "/foo.txt",
},
}
actual := ctx.FormData()
expect := &FormData{
values: ctx.values,
files: ctx.files,
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got %+v", expect, actual)
}
}
func TestContext_CreateSubDirectory(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
expectError bool
}{
{
scenario: "failure",
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
return errors.New("cannot rename")
}}},
expectError: true,
},
{
scenario: "success",
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
return nil
}}},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.logger = zap.NewNop()
_, err := tc.ctx.CreateSubDirectory("foo")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestContext_GeneratePath(t *testing.T) {
ctx := &Context{
dirPath: "/foo",
}
path := ctx.GeneratePath(".pdf")
if !strings.HasPrefix(path, ctx.dirPath) {
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
}
}
func TestContext_Rename(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
expectError bool
}{
{
scenario: "failure",
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return errors.New("cannot rename")
}}},
expectError: true,
},
{
scenario: "success",
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}}},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.logger = zap.NewNop()
err := tc.ctx.Rename("", "")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestContext_AddOutputPaths(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
path string
expectCount int
expectError bool
}{
{
scenario: "ErrContextAlreadyClosed",
ctx: &Context{cancelled: true},
expectCount: 0,
expectError: true,
},
{
scenario: "ErrOutOfBoundsOutputPath",
ctx: &Context{dirPath: "/foo"},
path: "/bar/foo.txt",
expectCount: 0,
expectError: true,
},
{
scenario: "success",
ctx: &Context{dirPath: "/foo"},
path: "/foo/foo.txt",
expectCount: 1,
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.ctx.AddOutputPaths(tc.path)
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if len(tc.ctx.outputPaths) != tc.expectCount {
t.Errorf("expected %d output paths but got %d", tc.expectCount, len(tc.ctx.outputPaths))
}
})
}
}
func TestContext_Log(t *testing.T) {
expect := zap.NewNop()
ctx := Context{logger: expect}
actual := ctx.Log()
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContext_BuildOutputFile(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
expectError bool
}{
{
scenario: "ErrContextAlreadyClosed",
ctx: &Context{cancelled: true},
expectError: true,
},
{
scenario: "no output path",
ctx: &Context{},
expectError: true,
},
{
scenario: "success: one output path",
ctx: &Context{outputPaths: []string{"foo.txt"}},
expectError: false,
},
{
scenario: "cannot archive: invalid output paths",
ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
expectError: true,
},
{
scenario: "success: many output paths",
ctx: &Context{
outputPaths: []string{
"/tests/test/testdata/api/sample1.txt",
"/tests/test/testdata/api/sample1.txt",
},
},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
dirPath, err := fs.MkdirAll()
if err != nil {
t.Fatalf("expected no erro but got: %v", err)
}
defer func() {
err := os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("expected no error while cleaning up but got: %v", err)
}
}()
tc.ctx.dirPath = dirPath
tc.ctx.Context = context.Background()
tc.ctx.logger = zap.NewNop()
_, err = tc.ctx.BuildOutputFile()
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestContext_OutputFilename(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
outputPath string
expectOutputFilename string
}{
{
scenario: "with Gotenberg-Output-Filename header",
ctx: func() *Context {
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
c.Request().Header.Set("Gotenberg-Output-Filename", "foo")
return &Context{echoCtx: c}
}(),
outputPath: "/foo/bar.txt",
expectOutputFilename: "foo.txt",
},
{
scenario: "without custom filename",
ctx: func() *Context {
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
return &Context{echoCtx: c}
}(),
outputPath: "/foo/foo.txt",
expectOutputFilename: "foo.txt",
},
} {
t.Run(tc.scenario, func(t *testing.T) {
actual := tc.ctx.OutputFilename(tc.outputPath)
if actual != tc.expectOutputFilename {
t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual)
}
})
}
}

View File

@@ -1425,36 +1425,36 @@ func TestFormData_Content(t *testing.T) {
scenario: "file does exist without file extension",
form: &FormData{
files: map[string]string{
"foo": "/tests/test/testdata/api/sample1.txt",
"foo": "testdata/sample.txt",
},
},
filename: "foo",
defaultValue: "",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
{
scenario: "file does exist with an uppercase file extension",
form: &FormData{
files: map[string]string{
"foo.TXT": "/tests/test/testdata/api/sample1.txt",
"foo.TXT": "testdata/sample.txt",
},
},
filename: "foo.txt",
defaultValue: "",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
{
scenario: "file does exist without a lowercase file extension",
form: &FormData{
files: map[string]string{
"foo.txt": "/tests/test/testdata/api/sample1.txt",
"foo.txt": "testdata/sample.txt",
},
},
filename: "foo.txt",
defaultValue: "",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
} {
@@ -1519,33 +1519,33 @@ func TestFormData_MandatoryContent(t *testing.T) {
scenario: "mandatory file does exist without file extension",
form: &FormData{
files: map[string]string{
"foo": "/tests/test/testdata/api/sample1.txt",
"foo": "testdata/sample.txt",
},
},
filename: "foo",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
{
scenario: "mandatory file does exist with an uppercase file extension",
form: &FormData{
files: map[string]string{
"foo.TXT": "/tests/test/testdata/api/sample1.txt",
"foo.TXT": "testdata/sample.txt",
},
},
filename: "foo.txt",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
{
scenario: "mandatory file does exist without a lowercase file extension",
form: &FormData{
files: map[string]string{
"foo.txt": "/tests/test/testdata/api/sample1.txt",
"foo.txt": "testdata/sample.txt",
},
},
filename: "foo.txt",
expect: "foo",
expect: "This is a text from a text file.",
expectError: false,
},
} {

View File

@@ -1,584 +0,0 @@
package api
import (
"bytes"
"context"
"errors"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
func TestParseError(t *testing.T) {
for i, tc := range []struct {
err error
expectStatus int
expectMessage string
}{
{
err: echo.ErrInternalServerError,
expectStatus: http.StatusInternalServerError,
expectMessage: http.StatusText(http.StatusInternalServerError),
},
{
err: gotenberg.ErrFiltered,
expectStatus: http.StatusForbidden,
expectMessage: http.StatusText(http.StatusForbidden),
},
{
err: gotenberg.ErrMaximumQueueSizeExceeded,
expectStatus: http.StatusTooManyRequests,
expectMessage: http.StatusText(http.StatusTooManyRequests),
},
{
err: gotenberg.ErrPdfSplitModeNotSupported,
expectStatus: http.StatusBadRequest,
expectMessage: "At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues",
},
{
err: gotenberg.ErrPdfFormatNotSupported,
expectStatus: http.StatusBadRequest,
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
},
{
err: gotenberg.ErrPdfEngineMetadataValueNotSupported,
expectStatus: http.StatusBadRequest,
expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues",
},
{
err: WrapError(
errors.New("foo"),
NewSentinelHttpError(http.StatusBadRequest, "foo"),
),
expectStatus: http.StatusBadRequest,
expectMessage: "foo",
},
} {
actualStatus, actualMessage := ParseError(tc.err)
if actualStatus != tc.expectStatus {
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, actualStatus)
}
if actualMessage != tc.expectMessage {
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, actualMessage)
}
}
}
func TestHttpErrorHandler(t *testing.T) {
for i, tc := range []struct {
err error
expectStatus int
expectMessage string
}{
{
err: echo.ErrInternalServerError,
expectStatus: http.StatusInternalServerError,
expectMessage: http.StatusText(http.StatusInternalServerError),
},
{
err: context.DeadlineExceeded,
expectStatus: http.StatusServiceUnavailable,
expectMessage: http.StatusText(http.StatusServiceUnavailable),
},
{
err: WrapError(
errors.New("foo"),
NewSentinelHttpError(http.StatusBadRequest, "foo"),
),
expectStatus: http.StatusBadRequest,
expectMessage: "foo",
},
} {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
c.Set("logger", zap.NewNop())
handler := httpErrorHandler()
handler(tc.err, c)
contentType := recorder.Header().Get(echo.HeaderContentType)
if contentType != echo.MIMETextPlainCharsetUTF8 {
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8, contentType)
}
// Note: we cannot test the trace header in the response here, as it is set in the trace middleware.
if recorder.Code != tc.expectStatus {
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
}
if recorder.Body.String() != tc.expectMessage {
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, recorder.Body.String())
}
}
}
func TestLatencyMiddleware(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
err := latencyMiddleware()(
func(c echo.Context) error {
return nil
},
)(c)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
startTime := c.Get("startTime").(time.Time)
now := time.Now()
if now.Before(startTime) {
t.Errorf("expected start time %s to be < %s", startTime, now)
}
}
func TestRootPathMiddleware(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
err := rootPathMiddleware("foo")(
func(c echo.Context) error {
return nil
},
)(c)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
rootPath := c.Get("rootPath").(string)
if rootPath != "foo" {
t.Errorf("expected '%s' but got '%s", "foo", rootPath)
}
}
func TestTraceMiddleware(t *testing.T) {
for i, tc := range []struct {
trace string
}{
{
trace: "foo",
},
{
trace: "",
},
} {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
if tc.trace != "" {
c.Request().Header.Set("Gotenberg-Trace", tc.trace)
}
err := traceMiddleware("Gotenberg-Trace")(
func(c echo.Context) error {
return nil
},
)(c)
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
trace := c.Get("trace").(string)
if trace == "" {
t.Errorf("test %d: expected non empty trace in context", i)
}
if tc.trace != "" && trace != tc.trace {
t.Errorf("test %d: expected context trace '%s' but got '%s'", i, tc.trace, trace)
}
if tc.trace == "" && trace == tc.trace {
t.Errorf("test %d: expected context trace different from '%s' but got '%s'", i, tc.trace, trace)
}
responseTrace := recorder.Header().Get("Gotenberg-Trace")
if tc.trace != "" && responseTrace != tc.trace {
t.Errorf("test %d: expected header trace '%s' but got '%s'", i, tc.trace, responseTrace)
}
if tc.trace == "" && responseTrace == tc.trace {
t.Errorf("test %d: expected header trace different from '%s' but got '%s'", i, tc.trace, responseTrace)
}
}
}
func TestBasicAuthMiddleware(t *testing.T) {
for _, tc := range []struct {
scenario string
request *http.Request
username string
password string
expectError bool
}{
{
scenario: "invalid basic auth",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth("invalid", "invalid")
return req
}(),
username: "foo",
password: "bar",
expectError: true,
},
{
scenario: "valid basic auth",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth("foo", "bar")
return req
}(),
username: "foo",
password: "bar",
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
recorder := httptest.NewRecorder()
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(tc.request, recorder)
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
return nil
})(c)
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 TestLoggerMiddleware(t *testing.T) {
for i, tc := range []struct {
request *http.Request
next echo.HandlerFunc
skipLogging bool
}{
{
request: httptest.NewRequest(http.MethodGet, "/", nil),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return errors.New("foo")
}
}(),
},
{
request: httptest.NewRequest(http.MethodGet, "/health", nil),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return nil
}
}(),
skipLogging: true,
},
{
request: httptest.NewRequest(http.MethodGet, "/health", nil),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return nil
}
}(),
},
} {
recorder := httptest.NewRecorder()
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(tc.request, recorder)
c.Set("startTime", time.Now())
c.Set("trace", "foo")
c.Set("rootPath", "/")
var disableLoggingForPaths []string
if tc.skipLogging {
disableLoggingForPaths = append(disableLoggingForPaths, tc.request.RequestURI)
}
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
if err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestContextMiddleware(t *testing.T) {
buildMultipartFormDataRequest := func() *http.Request {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
defer func() {
err := writer.Close()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
}()
err := writer.WriteField("foo", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req
}
for i, tc := range []struct {
request *http.Request
next echo.HandlerFunc
expectErr bool
expectStatus int
expectContentType string
expectFilename string
}{
{
request: httptest.NewRequest(http.MethodGet, "/", nil),
expectErr: true,
},
{
request: buildMultipartFormDataRequest(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return ErrAsyncProcess
}
}(),
expectStatus: http.StatusNoContent,
},
{
request: buildMultipartFormDataRequest(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return ErrNoOutputFile
}
}(),
expectStatus: http.StatusOK,
},
{
request: buildMultipartFormDataRequest(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return errors.New("foo")
}
}(),
expectErr: true,
},
{
request: buildMultipartFormDataRequest(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return nil
}
}(),
expectErr: true,
},
{
request: func() *http.Request {
req := buildMultipartFormDataRequest()
req.Header.Set("Gotenberg-Output-Filename", "foo")
return req
}(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Get("context").(*Context)
ctx.outputPaths = []string{
"/tests/test/testdata/api/sample2.pdf",
}
return nil
}
}(),
expectStatus: http.StatusOK,
expectContentType: "application/pdf",
expectFilename: "foo.pdf",
},
{
request: buildMultipartFormDataRequest(),
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Get("context").(*Context)
ctx.outputPaths = []string{
"/tests/test/testdata/api/sample1.txt",
"/tests/test/testdata/api/sample2.pdf",
}
return nil
}
}(),
expectStatus: http.StatusOK,
expectContentType: "application/zip",
},
} {
recorder := httptest.NewRecorder()
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(tc.request, recorder)
c.Set("logger", zap.NewNop())
c.Set("traceHeader", "Gotenberg-Trace")
c.Set("trace", "foo")
c.Set("startTime", time.Now())
err := contextMiddleware(gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, 0, downloadFromConfig{})(tc.next)(c)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if err != nil {
continue
}
if recorder.Code != tc.expectStatus {
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
}
if tc.expectStatus == http.StatusNoContent {
continue
}
contentType := recorder.Header().Get(echo.HeaderContentType)
if contentType != tc.expectContentType {
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, tc.expectContentType, contentType)
}
contentDisposition := recorder.Header().Get(echo.HeaderContentDisposition)
if !strings.Contains(contentDisposition, tc.expectFilename) {
t.Errorf("test %d: expected %s '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectFilename)
}
}
}
func TestHardTimeoutMiddleware(t *testing.T) {
for i, tc := range []struct {
next echo.HandlerFunc
timeout time.Duration
expectErr bool
expectHardTimeout bool
}{
{
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return nil
}
}(),
timeout: time.Duration(100) * time.Millisecond,
},
{
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
panic("foo")
}
}(),
timeout: time.Duration(100) * time.Millisecond,
expectErr: true,
expectHardTimeout: true,
},
{
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
return errors.New("foo")
}
}(),
timeout: time.Duration(100) * time.Millisecond,
expectErr: true,
},
{
next: func() echo.HandlerFunc {
return func(c echo.Context) error {
time.Sleep(time.Duration(200) * time.Millisecond)
return nil
}
}(),
timeout: time.Duration(100) * time.Millisecond,
expectErr: true,
expectHardTimeout: true,
},
} {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
c.Set("logger", zap.NewNop())
err := hardTimeoutMiddleware(tc.timeout)(tc.next)(c)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
var isHardTimeout bool
if err != nil {
isHardTimeout = strings.Contains(err.Error(), "hard timeout")
}
if tc.expectHardTimeout && !isHardTimeout {
t.Errorf("test %d: expected hard timeout error but got: %v", i, err)
}
if !tc.expectHardTimeout && isHardTimeout {
t.Errorf("test %d: expected no hard timeout error but got one: %v", i, err)
}
}
}

View File

@@ -1,193 +0,0 @@
package api
import (
"reflect"
"testing"
"github.com/alexliesenfeld/health"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
func TestContextMock_SetDirPath(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_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{
"foo": {"foo"},
})
actual := mock.values
expect := map[string][]string{
"foo": {"foo"},
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetFiles(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetFiles(map[string]string{
"foo": "/foo",
})
actual := mock.files
expect := map[string]string{
"foo": "/foo",
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetCancelled(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetCancelled(true)
actual := mock.cancelled
if !actual {
t.Errorf("expected %t but got %t", true, actual)
}
}
func TestContextMock_OutputPaths(t *testing.T) {
mock := ContextMock{
&Context{
outputPaths: []string{"/foo"},
},
}
actual := mock.OutputPaths()
expect := []string{"/foo"}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetLogger(t *testing.T) {
mock := ContextMock{&Context{}}
expect := zap.NewNop()
mock.SetLogger(expect)
actual := mock.logger
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContextMock_SetEchoContext(t *testing.T) {
mock := ContextMock{&Context{}}
expect := echo.New().NewContext(nil, nil)
mock.SetEchoContext(expect)
actual := mock.echoCtx
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContextMock_SetMkdirAll(t *testing.T) {
mock := ContextMock{&Context{}}
expect := new(gotenberg.OsMkdirAll)
mock.SetMkdirAll(expect)
actual := mock.mkdirAll
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContextMock_SetPathRename(t *testing.T) {
mock := ContextMock{&Context{}}
expect := new(gotenberg.OsPathRename)
mock.SetPathRename(expect)
actual := mock.pathRename
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestRouterMock(t *testing.T) {
mock := &RouterMock{
RoutesMock: func() ([]Route, error) {
return nil, nil
},
}
_, err := mock.Routes()
if err != nil {
t.Errorf("expected no error from RouterMock.Routes, but got: %v", err)
}
}
func TestMiddlewareProviderMock(t *testing.T) {
mock := &MiddlewareProviderMock{
MiddlewaresMock: func() ([]Middleware, error) {
return nil, nil
},
}
_, err := mock.Middlewares()
if err != nil {
t.Errorf("expected no error from MiddlewareProviderMock.Middlewares, but got: %v", err)
}
}
func TestHealthCheckerMock(t *testing.T) {
mock := &HealthCheckerMock{
ChecksMock: func() ([]health.CheckerOption, error) {
return nil, nil
},
ReadyMock: func() error {
return nil
},
}
_, err := mock.Checks()
if err != nil {
t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err)
}
err = mock.Ready()
if err != nil {
t.Errorf("expected no error from HealthCheckerMock.Ready, but got: %v", err)
}
}

1
pkg/modules/api/testdata/sample.txt vendored Normal file
View File

@@ -0,0 +1 @@
This is a text from a text file.