diff --git a/pkg/modules/api/api.go b/pkg/modules/api/api.go index 65f07cc6..bb4b9d42 100644 --- a/pkg/modules/api/api.go +++ b/pkg/modules/api/api.go @@ -22,12 +22,12 @@ import ( ) func init() { - gotenberg.MustRegisterModule(API{}) + gotenberg.MustRegisterModule(new(Api)) } -// API is a module which provides an HTTP server. Other modules may add routes, +// Api is a module which provides an HTTP server. Other modules may add routes, // middlewares or health checks. -type API struct { +type Api struct { port int readTimeout time.Duration writeTimeout time.Duration @@ -44,7 +44,7 @@ type API struct { srv *echo.Echo } -// Router is a module interface which adds routes to the [API]. +// Router is a module interface which adds routes to the [Api]. type Router interface { Routes() ([]Route, error) } @@ -72,7 +72,7 @@ type Route struct { Handler echo.HandlerFunc } -// MiddlewareProvider is a module interface which adds middlewares to the [API]. +// MiddlewareProvider is a module interface which adds middlewares to the [Api]. type MiddlewareProvider interface { Middlewares() ([]Middleware, error) } @@ -99,7 +99,7 @@ const ( VeryHighPriority ) -// Middleware is a middleware which can be added to the [API]'s middlewares +// Middleware is a middleware which can be added to the [Api]'s middlewares // chain. // // middleware := Middleware{ @@ -149,8 +149,8 @@ type HealthChecker interface { Checks() ([]health.CheckerOption, error) } -// Descriptor returns an [API]'s module descriptor. -func (API) Descriptor() gotenberg.ModuleDescriptor { +// Descriptor returns an [Api]'s module descriptor. +func (a *Api) Descriptor() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ ID: "api", FlagSet: func() *flag.FlagSet { @@ -176,12 +176,12 @@ func (API) Descriptor() gotenberg.ModuleDescriptor { return fs }(), - New: func() gotenberg.Module { return new(API) }, + New: func() gotenberg.Module { return new(Api) }, } } // Provision sets the module properties. -func (a *API) Provision(ctx *gotenberg.Context) error { +func (a *Api) Provision(ctx *gotenberg.Context) error { flags := ctx.ParsedFlags() a.port = flags.MustInt("api-port") a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout") @@ -297,7 +297,7 @@ func (a *API) Provision(ctx *gotenberg.Context) error { } // Validate validates the module properties. -func (a API) Validate() error { +func (a *Api) Validate() error { var err error if a.port < 1 || a.port > 65535 { @@ -369,7 +369,7 @@ func (a API) Validate() error { } // Start starts the HTTP server. -func (a *API) Start() error { +func (a *Api) Start() error { a.srv = echo.New() a.srv.HideBanner = true a.srv.HidePort = true @@ -465,19 +465,19 @@ func (a *API) Start() error { } // StartupMessage returns a custom startup message. -func (a API) StartupMessage() string { +func (a *Api) StartupMessage() string { return fmt.Sprintf("server listening on port %d", a.port) } // Stop stops the HTTP server. -func (a API) Stop(ctx context.Context) error { +func (a *Api) Stop(ctx context.Context) error { return a.srv.Shutdown(ctx) } // Interface guards. var ( - _ gotenberg.Module = (*API)(nil) - _ gotenberg.Provisioner = (*API)(nil) - _ gotenberg.Validator = (*API)(nil) - _ gotenberg.App = (*API)(nil) + _ gotenberg.Module = (*Api)(nil) + _ gotenberg.Provisioner = (*Api)(nil) + _ gotenberg.Validator = (*Api)(nil) + _ gotenberg.App = (*Api)(nil) ) diff --git a/pkg/modules/api/api_test.go b/pkg/modules/api/api_test.go index 7b7427e1..3eea5616 100644 --- a/pkg/modules/api/api_test.go +++ b/pkg/modules/api/api_test.go @@ -18,81 +18,30 @@ import ( "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" ) -type ProtoModule struct { - descriptor func() gotenberg.ModuleDescriptor -} - -func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor { - return mod.descriptor() -} - -type ProtoValidator struct { - ProtoModule - validate func() error -} - -func (mod ProtoValidator) Validate() error { - return mod.validate() -} - -type ProtoRouter struct { - ProtoValidator - routes func() ([]Route, error) -} - -func (mod ProtoRouter) Routes() ([]Route, error) { - return mod.routes() -} - -type ProtoMiddlewareProvider struct { - ProtoValidator - middlewares func() ([]Middleware, error) -} - -func (mod ProtoMiddlewareProvider) Middlewares() ([]Middleware, error) { - return mod.middlewares() -} - -type ProtoHealthChecker struct { - ProtoValidator - checks func() ([]health.CheckerOption, error) -} - -func (mod ProtoHealthChecker) Checks() ([]health.CheckerOption, error) { - return mod.checks() -} - -type ProtoLoggerProvider struct { - ProtoModule - logger func(mod gotenberg.Module) (*zap.Logger, error) -} - -func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) { - return factory.logger(mod) -} - -func TestAPI_Descriptor(t *testing.T) { - descriptor := API{}.Descriptor() +func TestApi_Descriptor(t *testing.T) { + descriptor := new(Api).Descriptor() actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(API)) + expect := reflect.TypeOf(new(Api)) if actual != expect { t.Errorf("expected '%s' but got '%s'", expect, actual) } } -func TestAPI_Provision(t *testing.T) { - for i, tc := range []struct { +func TestApi_Provision(t *testing.T) { + for _, tc := range []struct { + scenario string ctx *gotenberg.Context - setEnv func(i int) + setEnv func() expectPort int expectMiddlewares []Middleware - expectErr bool + expectError bool }{ { + scenario: "port from env: non-existing environment variable", ctx: func() *gotenberg.Context { - fs := new(API).Descriptor().FlagSet + fs := new(Api).Descriptor().FlagSet err := fs.Parse([]string{"--api-port-from-env=FOO"}) if err != nil { t.Fatalf("expected no error but got: %v", err) @@ -105,11 +54,12 @@ func TestAPI_Provision(t *testing.T) { nil, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "port from env: empty environment variable", ctx: func() *gotenberg.Context { - fs := new(API).Descriptor().FlagSet + fs := new(Api).Descriptor().FlagSet err := fs.Parse([]string{"--api-port-from-env=PORT"}) if err != nil { t.Fatalf("expected no error but got: %v", err) @@ -122,17 +72,18 @@ func TestAPI_Provision(t *testing.T) { nil, ) }(), - setEnv: func(i int) { + setEnv: func() { err := os.Setenv("PORT", "") if err != nil { - t.Fatalf("test %d: expected no error but got: %v", i, err) + t.Fatalf("expected no error but got: %v", err) } }, - expectErr: true, + expectError: true, }, { + scenario: "port from env: invalid environment variable value", ctx: func() *gotenberg.Context { - fs := new(API).Descriptor().FlagSet + fs := new(Api).Descriptor().FlagSet err := fs.Parse([]string{"--api-port-from-env=PORT"}) if err != nil { t.Fatalf("expected no error but got: %v", err) @@ -145,235 +96,228 @@ func TestAPI_Provision(t *testing.T) { nil, ) }(), - setEnv: func(i int) { + setEnv: func() { err := os.Setenv("PORT", "foo") - if err != nil { - t.Fatalf("test %d: expected no error but got: %v", i, err) - } - }, - expectErr: true, - }, - { - ctx: func() *gotenberg.Context { - fs := new(API).Descriptor().FlagSet - err := fs.Parse([]string{"--api-port-from-env=PORT"}) if err != nil { t.Fatalf("expected no error but got: %v", err) } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - setEnv: func(i int) { - err := os.Setenv("PORT", "1337") - if err != nil { - t.Fatalf("test %d: expected no error but got: %v", i, err) - } }, - expectPort: 1337, - expectErr: true, + expectError: true, }, { + scenario: "no valid routers", ctx: func() *gotenberg.Context { - mod := struct{ ProtoRouter }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + gotenberg.ValidatorMock + RouterMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.validate = func() error { + mod.ValidateMock = func() error { return errors.New("foo") } - mod.routes = func() ([]Route, error) { + mod.RoutesMock = func() ([]Route, error) { return nil, nil } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "cannot retrieve routes from router", ctx: func() *gotenberg.Context { - mod := struct{ ProtoMiddlewareProvider }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + RouterMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.validate = func() error { + mod.RoutesMock = func() ([]Route, error) { + return nil, errors.New("foo") + } + return gotenberg.NewContext( + gotenberg.ParsedFlags{ + FlagSet: new(Api).Descriptor().FlagSet, + }, + []gotenberg.ModuleDescriptor{ + mod.Descriptor(), + }, + ) + }(), + expectError: true, + }, + { + scenario: "no valid middleware providers", + ctx: func() *gotenberg.Context { + mod := &struct { + gotenberg.ModuleMock + gotenberg.ValidatorMock + MiddlewareProviderMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { + return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} + } + mod.ValidateMock = func() error { return errors.New("foo") } - mod.middlewares = func() ([]Middleware, error) { + mod.MiddlewaresMock = func() ([]Middleware, error) { return nil, nil } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "cannot retrieve middlewares from middleware provider", ctx: func() *gotenberg.Context { - mod := struct{ ProtoMiddlewareProvider }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + MiddlewareProviderMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.validate = func() error { - return nil - } - mod.middlewares = func() ([]Middleware, error) { + mod.MiddlewaresMock = func() ([]Middleware, error) { return nil, errors.New("foo") } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "no valid health checkers", ctx: func() *gotenberg.Context { - mod := struct{ ProtoRouter }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + gotenberg.ValidatorMock + HealthCheckerMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.validate = func() error { - return nil - } - mod.routes = func() ([]Route, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectErr: true, - }, - { - ctx: func() *gotenberg.Context { - mod := struct{ ProtoHealthChecker }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.validate = func() error { + mod.ValidateMock = func() error { return errors.New("foo") } - mod.checks = func() ([]health.CheckerOption, error) { + mod.ChecksMock = func() ([]health.CheckerOption, error) { return nil, nil } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "cannot retrieve health checks from health checker", ctx: func() *gotenberg.Context { - mod := struct{ ProtoHealthChecker }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + HealthCheckerMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.validate = func() error { - return nil - } - mod.checks = func() ([]health.CheckerOption, error) { + mod.ChecksMock = func() ([]health.CheckerOption, error) { return nil, errors.New("foo") } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "no logger provider", ctx: func() *gotenberg.Context { return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{}, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "no logger from logger provider", ctx: func() *gotenberg.Context { - mod := struct{ ProtoLoggerProvider }{} - mod.descriptor = func() gotenberg.ModuleDescriptor { + mod := &struct { + gotenberg.ModuleMock + gotenberg.LoggerProviderMock + }{} + mod.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} } - mod.logger = func(_ gotenberg.Module) (*zap.Logger, error) { + mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { return nil, errors.New("foo") } - return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: new(Api).Descriptor().FlagSet, }, []gotenberg.ModuleDescriptor{ mod.Descriptor(), }, ) }(), - expectErr: true, + expectError: true, }, { + scenario: "success", ctx: func() *gotenberg.Context { - mod1 := struct{ ProtoRouter }{} - mod1.descriptor = func() gotenberg.ModuleDescriptor { + mod1 := &struct { + gotenberg.ModuleMock + RouterMock + }{} + mod1.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }} } - mod1.validate = func() error { - return nil - } - mod1.routes = func() ([]Route, error) { + mod1.RoutesMock = func() ([]Route, error) { return []Route{{}}, nil } - mod2 := struct{ ProtoMiddlewareProvider }{} - mod2.descriptor = func() gotenberg.ModuleDescriptor { + mod2 := &struct { + gotenberg.ModuleMock + MiddlewareProviderMock + }{} + mod2.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }} } - mod2.validate = func() error { - return nil - } - mod2.middlewares = func() ([]Middleware, error) { + mod2.MiddlewaresMock = func() ([]Middleware, error) { return []Middleware{ { Priority: VeryLowPriority, @@ -393,28 +337,37 @@ func TestAPI_Provision(t *testing.T) { }, nil } - mod3 := struct{ ProtoHealthChecker }{} - mod3.descriptor = func() gotenberg.ModuleDescriptor { + mod3 := &struct { + gotenberg.ModuleMock + HealthCheckerMock + }{} + mod3.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }} } - mod3.validate = func() error { - return nil - } - mod3.checks = func() ([]health.CheckerOption, error) { + mod3.ChecksMock = func() ([]health.CheckerOption, error) { return []health.CheckerOption{health.WithDisabledAutostart()}, nil } - mod4 := struct{ ProtoLoggerProvider }{} - mod4.descriptor = func() gotenberg.ModuleDescriptor { + mod4 := &struct { + gotenberg.ModuleMock + gotenberg.LoggerProviderMock + }{} + mod4.DescriptorMock = func() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ID: "qux", New: func() gotenberg.Module { return mod4 }} } - mod4.logger = func(_ gotenberg.Module) (*zap.Logger, error) { + mod4.LoggerMock = func(_ gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil } + fs := new(Api).Descriptor().FlagSet + err := fs.Parse([]string{"--api-port-from-env=PORT"}) + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + return gotenberg.NewContext( gotenberg.ParsedFlags{ - FlagSet: new(API).Descriptor().FlagSet, + FlagSet: fs, }, []gotenberg.ModuleDescriptor{ mod1.Descriptor(), @@ -424,6 +377,13 @@ func TestAPI_Provision(t *testing.T) { }, ) }(), + setEnv: func() { + err := os.Setenv("PORT", "1337") + if err != nil { + t.Fatalf("expected no error but got: %v", err) + } + }, + expectPort: 1337, expectMiddlewares: []Middleware{ { Priority: VeryHighPriority, @@ -441,52 +401,93 @@ func TestAPI_Provision(t *testing.T) { Priority: VeryLowPriority, }, }, + expectError: false, }, } { - if tc.setEnv != nil { - tc.setEnv(i) - } + t.Run(tc.scenario, func(t *testing.T) { + if tc.setEnv != nil { + tc.setEnv() + } - mod := new(API) - err := mod.Provision(tc.ctx) + mod := new(Api) + err := mod.Provision(tc.ctx) - if tc.expectPort != 0 && mod.port != tc.expectPort { - t.Errorf("test %d: expected port %d but got %d", i, tc.expectPort, mod.port) - } + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } - if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) { - t.Errorf("test %d: expected %+v, but got: %+v", i, tc.expectMiddlewares, mod.externalMiddlewares) - } + if tc.expectError && err == nil { + t.Fatal("expected error but got none") + } - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) - } + if tc.expectPort != 0 && mod.port != tc.expectPort { + t.Errorf("expected port %d but got %d", tc.expectPort, mod.port) + } - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } + if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) { + t.Errorf("expected %+v, but got: %+v", tc.expectMiddlewares, mod.externalMiddlewares) + } + }) } } -func TestAPI_Validate(t *testing.T) { - for i, tc := range []struct { +func TestApi_Validate(t *testing.T) { + for _, tc := range []struct { + scenario string port int rootPath string traceHeader string routes []Route middlewares []Middleware - expectErr bool + expectError bool }{ { - port: 0, - expectErr: true, + scenario: "invalid port (< 1)", + port: 0, + rootPath: "/foo/", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, }, { - port: 65536, - rootPath: "foo", - expectErr: true, + scenario: "invalid port (> 65535)", + port: 65536, + rootPath: "/foo/", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, }, { + scenario: "invalid root path: missing / prefix", + port: 10, + rootPath: "foo/", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, + }, + { + scenario: "invalid root path: missing / suffix", + port: 10, + rootPath: "/foo", + traceHeader: "foo", + routes: nil, + middlewares: nil, + expectError: true, + }, + { + scenario: "invalid trace header", + port: 10, + rootPath: "/foo/", + traceHeader: "", + routes: nil, + middlewares: nil, + expectError: true, + }, + { + scenario: "invalid route: empty path", port: 10, rootPath: "/foo/", traceHeader: "foo", @@ -495,9 +496,11 @@ func TestAPI_Validate(t *testing.T) { Path: "", }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid route: missing / prefix in path", port: 10, rootPath: "/foo/", traceHeader: "foo", @@ -506,9 +509,11 @@ func TestAPI_Validate(t *testing.T) { Path: "foo", }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid multipart route: no /forms prefix in path", port: 10, rootPath: "/foo/", traceHeader: "foo", @@ -518,34 +523,40 @@ func TestAPI_Validate(t *testing.T) { IsMultipart: true, }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid route: no method", port: 10, rootPath: "/foo/", traceHeader: "foo", routes: []Route{ { - Path: "/forms/foo", - IsMultipart: true, + Path: "/foo", + Method: "", }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid route: nil handler", port: 10, rootPath: "/foo/", traceHeader: "foo", routes: []Route{ { - Method: http.MethodPost, - Path: "/forms/foo", - IsMultipart: true, + Method: http.MethodPost, + Path: "/foo", + Handler: nil, }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid route: path already existing", port: 10, rootPath: "/foo/", traceHeader: "foo", @@ -561,20 +572,25 @@ func TestAPI_Validate(t *testing.T) { Handler: func(_ echo.Context) error { return nil }, }, }, - expectErr: true, + middlewares: nil, + expectError: true, }, { + scenario: "invalid middleware: nil handler", port: 10, rootPath: "/foo/", traceHeader: "foo", + routes: nil, middlewares: []Middleware{ { Priority: HighPriority, + Handler: nil, }, }, - expectErr: true, + expectError: true, }, { + scenario: "success", port: 10, rootPath: "/foo/", traceHeader: "foo", @@ -584,6 +600,12 @@ func TestAPI_Validate(t *testing.T) { Path: "/foo", Handler: func(_ echo.Context) error { return nil }, }, + { + Method: http.MethodGet, + Path: "/forms/foo", + Handler: func(_ echo.Context) error { return nil }, + IsMultipart: true, + }, }, middlewares: []Middleware{ { @@ -599,28 +621,29 @@ func TestAPI_Validate(t *testing.T) { }, }, } { - mod := API{ - port: tc.port, - rootPath: tc.rootPath, - traceHeader: tc.traceHeader, - routes: tc.routes, - externalMiddlewares: tc.middlewares, - } + t.Run(tc.scenario, func(t *testing.T) { + mod := Api{ + port: tc.port, + rootPath: tc.rootPath, + traceHeader: tc.traceHeader, + routes: tc.routes, + externalMiddlewares: tc.middlewares, + } - err := mod.Validate() + err := mod.Validate() + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } - 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 tc.expectError && err == nil { + t.Fatal("expected error but got none") + } + }) } } -func TestAPI_Start(t *testing.T) { - mod := new(API) +func TestApi_Start(t *testing.T) { + mod := new(Api) mod.port = 3000 mod.rootPath = "/" mod.disableHealthCheckLogging = true @@ -705,7 +728,7 @@ func TestAPI_Start(t *testing.T) { } // "multipart/form-data" request. - multipartRequest := func(URL string) *http.Request { + multipartRequest := func(url string) *http.Request { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -732,7 +755,7 @@ func TestAPI_Start(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - req := httptest.NewRequest(http.MethodPost, URL, body) + req := httptest.NewRequest(http.MethodPost, url, body) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) return req @@ -758,8 +781,8 @@ func TestAPI_Start(t *testing.T) { } } -func TestAPI_StartupMessage(t *testing.T) { - mod := API{ +func TestApi_StartupMessage(t *testing.T) { + mod := Api{ port: 3000, } @@ -771,8 +794,8 @@ func TestAPI_StartupMessage(t *testing.T) { } } -func TestAPI_Stop(t *testing.T) { - mod := API{ +func TestApi_Stop(t *testing.T) { + mod := &Api{ port: 3000, routes: []Route{ { @@ -794,21 +817,3 @@ func TestAPI_Stop(t *testing.T) { t.Errorf("expected no error but got: %v", err) } } - -// Interface guards. -var ( - _ gotenberg.Module = (*ProtoModule)(nil) - _ gotenberg.Validator = (*ProtoValidator)(nil) - _ gotenberg.Module = (*ProtoValidator)(nil) - _ Router = (*ProtoRouter)(nil) - _ gotenberg.Module = (*ProtoRouter)(nil) - _ gotenberg.Validator = (*ProtoRouter)(nil) - _ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil) - _ gotenberg.Module = (*ProtoMiddlewareProvider)(nil) - _ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil) - _ HealthChecker = (*ProtoHealthChecker)(nil) - _ gotenberg.Module = (*ProtoHealthChecker)(nil) - _ gotenberg.Validator = (*ProtoHealthChecker)(nil) - _ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil) - _ gotenberg.Module = (*ProtoLoggerProvider)(nil) -) diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go index 7b650e88..48afed70 100644 --- a/pkg/modules/api/context.go +++ b/pkg/modules/api/context.go @@ -93,21 +93,21 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst if errors.Is(err, http.ErrNotMultipart) { return nil, cancel, WrapError( fmt.Errorf("get multipart form: %w", err), - NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"), + NewSentinelHttpError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"), ) } if errors.Is(err, http.ErrMissingBoundary) { return nil, cancel, WrapError( fmt.Errorf("get multipart form: %w", err), - NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"), + NewSentinelHttpError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"), ) } if strings.Contains(err.Error(), io.EOF.Error()) { return nil, cancel, WrapError( fmt.Errorf("get multipart form: %w", err), - NewSentinelHTTPError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"), + NewSentinelHttpError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"), ) } @@ -179,19 +179,19 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst } } - ctx.Log().Debug(fmt.Sprintf("form data values: %+v", ctx.values)) - ctx.Log().Debug(fmt.Sprintf("form data files: %+v", ctx.files)) + ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values)) + ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files)) return ctx, cancel, err } // Request returns the [http.Request]. -func (ctx Context) Request() *http.Request { +func (ctx *Context) Request() *http.Request { return ctx.echoCtx.Request() } // FormData return a [FormData]. -func (ctx Context) FormData() *FormData { +func (ctx *Context) FormData() *FormData { return &FormData{ values: ctx.values, files: ctx.files, @@ -201,7 +201,7 @@ func (ctx Context) FormData() *FormData { // GeneratePath generates a path within the context's working directory. It // does not create a file. -func (ctx Context) GeneratePath(extension string) string { +func (ctx *Context) GeneratePath(extension string) string { return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension) } @@ -224,13 +224,13 @@ func (ctx *Context) AddOutputPaths(paths ...string) error { } // Log returns the context [zap.Logger]. -func (ctx Context) Log() *zap.Logger { +func (ctx *Context) Log() *zap.Logger { return ctx.logger } // BuildOutputFile builds the output file according to the output paths // registered in the context. If many output paths, an archive is created. -func (ctx Context) BuildOutputFile() (string, error) { +func (ctx *Context) BuildOutputFile() (string, error) { if ctx.cancelled { return "", ErrContextAlreadyClosed } @@ -268,7 +268,7 @@ func (ctx Context) BuildOutputFile() (string, error) { // OutputFilename returns the filename based on the given output path or the // "Gotenberg-Output-Filename" header's value. -func (ctx Context) OutputFilename(outputPath string) string { +func (ctx *Context) OutputFilename(outputPath string) string { filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename") if filename == "" { diff --git a/pkg/modules/api/context_test.go b/pkg/modules/api/context_test.go index a363159e..4528822b 100644 --- a/pkg/modules/api/context_test.go +++ b/pkg/modules/api/context_test.go @@ -19,138 +19,134 @@ import ( ) func TestNewContext(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string request *http.Request - expectErr bool - expectHTTPErr bool - expectHTTPStatus int + expectError bool + expectHttpError bool + expectHttpStatus int }{ { + scenario: "http.ErrNotMultipart", request: httptest.NewRequest(http.MethodPost, "/", nil), - expectErr: true, - expectHTTPErr: true, - expectHTTPStatus: http.StatusUnsupportedMediaType, + 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 }(), - expectErr: true, - expectHTTPErr: true, - expectHTTPStatus: http.StatusUnsupportedMediaType, + 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 }(), - expectErr: true, - expectHTTPErr: true, - expectHTTPStatus: http.StatusBadRequest, + 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) } - req := httptest.NewRequest(http.MethodPost, "/", body) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req }(), + expectError: false, + expectHttpError: false, }, } { - handler := func(c echo.Context) error { - _, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second) - defer cancel() - // Context already cancelled. - defer cancel() + t.Run(tc.scenario, func(t *testing.T) { + handler := func(c echo.Context) error { + _, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second) + defer cancel() + // Context already cancelled. + defer cancel() - if err != nil { - return err + if err != nil { + return err + } + + return nil } - return nil - } + recorder := httptest.NewRecorder() - recorder := httptest.NewRecorder() + srv := echo.New() + srv.HideBanner = true + srv.HidePort = true - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true + c := srv.NewContext(tc.request, recorder) + err := handler(c) - c := srv.NewContext(tc.request, recorder) - err := handler(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 httpErr HTTPError - isHTTPErr := errors.As(err, &httpErr) - - if tc.expectHTTPErr && !isHTTPErr { - t.Errorf("test %d: expected HTTP error but got: %v", i, err) - } - - if !tc.expectHTTPErr && isHTTPErr { - t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr) - } - - if err != nil && tc.expectHTTPErr && isHTTPErr { - status, _ := httpErr.HTTPError() - if status != tc.expectHTTPStatus { - t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status) + 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) + } + } + }) } } @@ -159,7 +155,7 @@ func TestContext_Request(t *testing.T) { recorder := httptest.NewRecorder() c := echo.New().NewContext(request, recorder) - ctx := Context{ + ctx := &Context{ echoCtx: c, } @@ -169,7 +165,7 @@ func TestContext_Request(t *testing.T) { } func TestContext_FormData(t *testing.T) { - ctx := Context{ + ctx := &Context{ values: map[string][]string{ "foo": {"foo"}, }, @@ -190,7 +186,7 @@ func TestContext_FormData(t *testing.T) { } func TestContext_GeneratePath(t *testing.T) { - ctx := Context{ + ctx := &Context{ dirPath: "/foo", } @@ -202,40 +198,49 @@ func TestContext_GeneratePath(t *testing.T) { } func TestContext_AddOutputPaths(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string ctx *Context path string expectCount int - expectErr bool + expectError bool }{ { - ctx: &Context{cancelled: true}, - expectErr: true, + scenario: "ErrContextAlreadyClosed", + ctx: &Context{cancelled: true}, + expectCount: 0, + expectError: true, }, { - ctx: &Context{dirPath: "/foo"}, - path: "/bar/foo.txt", - expectErr: 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, }, } { - err := tc.ctx.AddOutputPaths(tc.path) + t.Run(tc.scenario, func(t *testing.T) { + err := tc.ctx.AddOutputPaths(tc.path) - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) - } + if tc.expectError && err == nil { + t.Fatal("expected error but got none", err) + } - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } - if len(tc.ctx.outputPaths) != tc.expectCount { - t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectCount, len(tc.ctx.outputPaths)) - } + if len(tc.ctx.outputPaths) != tc.expectCount { + t.Errorf("expected %d output paths but got %d", tc.expectCount, len(tc.ctx.outputPaths)) + } + }) } } @@ -250,45 +255,53 @@ func TestContext_Log(t *testing.T) { } func TestContext_BuildOutputFile(t *testing.T) { - for i, tc := range []struct { - ctx *Context - expectErr bool + for _, tc := range []struct { + scenario string + ctx *Context + expectError bool }{ { - ctx: &Context{cancelled: true}, - expectErr: true, + scenario: "ErrContextAlreadyClosed", + ctx: &Context{cancelled: true}, + expectError: true, }, { - ctx: &Context{}, - expectErr: true, + scenario: "no output path", + ctx: &Context{}, + expectError: true, }, { - ctx: &Context{outputPaths: []string{"foo.txt"}}, + scenario: "success: one output path", + ctx: &Context{outputPaths: []string{"foo.txt"}}, + expectError: false, }, { - ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}}, - expectErr: true, + 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, }, } { - func() { + t.Run(tc.scenario, func(t *testing.T) { fs := gotenberg.NewFileSystem() dirPath, err := fs.MkdirAll() if err != nil { - t.Fatalf("%d: expected no erro but got: %v", i, err) + t.Fatalf("expected no erro but got: %v", err) } defer func() { err := os.RemoveAll(fs.WorkingDirPath()) if err != nil { - t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err) + t.Fatalf("expected no error while cleaning up but got: %v", err) } }() @@ -297,34 +310,36 @@ func TestContext_BuildOutputFile(t *testing.T) { _, err = tc.ctx.BuildOutputFile() - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) + if tc.expectError && err == nil { + t.Fatal("expected error but got none", err) } - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) } - }() + }) } } func TestContext_OutputFilename(t *testing.T) { - for i, tc := range []struct { + 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} @@ -333,10 +348,12 @@ func TestContext_OutputFilename(t *testing.T) { expectOutputFilename: "foo.txt", }, } { - actual := tc.ctx.OutputFilename(tc.outputPath) + t.Run(tc.scenario, func(t *testing.T) { + actual := tc.ctx.OutputFilename(tc.outputPath) - if actual != tc.expectOutputFilename { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectOutputFilename, actual) - } + if actual != tc.expectOutputFilename { + t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual) + } + }) } } diff --git a/pkg/modules/api/errors.go b/pkg/modules/api/errors.go index 7552dd9f..efb7604d 100644 --- a/pkg/modules/api/errors.go +++ b/pkg/modules/api/errors.go @@ -2,66 +2,66 @@ package api // Credits: https://www.joeshaw.org/error-handling-in-go-http-applications. -// HTTPError is an interface allowing to retrieve the HTTP details of an error. -type HTTPError interface { - HTTPError() (int, string) +// HttpError is an interface allowing to retrieve the HTTP details of an error. +type HttpError interface { + HttpError() (int, string) } -// SentinelHTTPError is the HTTP sidekick of an error. -type SentinelHTTPError struct { +// SentinelHttpError is the HTTP sidekick of an error. +type SentinelHttpError struct { status int message string } -// NewSentinelHTTPError creates a [SentinelHTTPError]. The message will be sent +// NewSentinelHttpError creates a [SentinelHttpError]. The message will be sent // as the response's body if returned from a handler, so make sure to not leak // sensible information. -func NewSentinelHTTPError(status int, message string) SentinelHTTPError { - return SentinelHTTPError{ +func NewSentinelHttpError(status int, message string) SentinelHttpError { + return SentinelHttpError{ status: status, message: message, } } // Error returns the message. -func (err SentinelHTTPError) Error() string { +func (err SentinelHttpError) Error() string { return err.message } -// HTTPError returns the status and message. -func (err SentinelHTTPError) HTTPError() (int, string) { +// HttpError returns the status and message. +func (err SentinelHttpError) HttpError() (int, string) { return err.status, err.message } // sentinelWrappedError contains both the error which will logged and the -// sidekick [SentinelHTTPError]. +// sidekick [SentinelHttpError]. type sentinelWrappedError struct { error - sentinel SentinelHTTPError + sentinel SentinelHttpError } func (w sentinelWrappedError) Is(err error) bool { return w.sentinel == err } -func (w sentinelWrappedError) HTTPError() (int, string) { - return w.sentinel.HTTPError() +func (w sentinelWrappedError) HttpError() (int, string) { + return w.sentinel.HttpError() } -// WrapError wraps the given error with a [SentinelHTTPError]. The wrapped -// error will be displayed in a log, while the [SentinelHTTPError] will be sent +// WrapError wraps the given error with a [SentinelHttpError]. The wrapped +// error will be displayed in a log, while the [SentinelHttpError] will be sent // in the response. // // return api.WrapError( // // This first error will be logged. // fmt.Errorf("my action: %w", err), // // The HTTP error will be sent as a response. -// api.NewSentinelHTTPError( +// api.NewSentinelHttpError( // http.StatusForbidden, // "Hey, you did something wrong!" // ), // ) -func WrapError(err error, sentinel SentinelHTTPError) error { +func WrapError(err error, sentinel SentinelHttpError) error { return sentinelWrappedError{ error: err, sentinel: sentinel, @@ -70,8 +70,8 @@ func WrapError(err error, sentinel SentinelHTTPError) error { // Interface guards. var ( - _ error = (*SentinelHTTPError)(nil) - _ HTTPError = (*SentinelHTTPError)(nil) + _ error = (*SentinelHttpError)(nil) + _ HttpError = (*SentinelHttpError)(nil) _ error = (*sentinelWrappedError)(nil) - _ HTTPError = (*sentinelWrappedError)(nil) + _ HttpError = (*sentinelWrappedError)(nil) ) diff --git a/pkg/modules/api/errors_test.go b/pkg/modules/api/errors_test.go index 9e0eca24..ebfdb8af 100644 --- a/pkg/modules/api/errors_test.go +++ b/pkg/modules/api/errors_test.go @@ -7,9 +7,9 @@ import ( "testing" ) -func TestNewSentinelHTTPError(t *testing.T) { - actual := NewSentinelHTTPError(http.StatusInternalServerError, "foo") - expect := SentinelHTTPError{ +func TestNewSentinelHttpError(t *testing.T) { + actual := NewSentinelHttpError(http.StatusInternalServerError, "foo") + expect := SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", } @@ -19,8 +19,8 @@ func TestNewSentinelHTTPError(t *testing.T) { } } -func TestSentinelHTTPError_Error(t *testing.T) { - err := SentinelHTTPError{ +func TestSentinelHttpError_Error(t *testing.T) { + err := SentinelHttpError{ message: "foo", } @@ -32,11 +32,11 @@ func TestSentinelHTTPError_Error(t *testing.T) { } } -func TestSentinelHTTPError_HTTPError(t *testing.T) { - actualStatus, actualMessage := SentinelHTTPError{ +func TestSentinelHttpError_HttpError(t *testing.T) { + actualStatus, actualMessage := SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", - }.HTTPError() + }.HttpError() expectStatus := http.StatusInternalServerError expectMessage := "foo" @@ -51,7 +51,7 @@ func TestSentinelHTTPError_HTTPError(t *testing.T) { } func TestSentinelWrappedError_Is(t *testing.T) { - errSentinel := SentinelHTTPError{} + errSentinel := SentinelHttpError{} err := sentinelWrappedError{ error: errors.New("foo"), @@ -63,19 +63,19 @@ func TestSentinelWrappedError_Is(t *testing.T) { } } -func TestSentinelWrappedError_HTTPError(t *testing.T) { - expectStatus, expectMessage := SentinelHTTPError{ +func TestSentinelWrappedError_HttpError(t *testing.T) { + expectStatus, expectMessage := SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", - }.HTTPError() + }.HttpError() actualStatus, actualMessage := sentinelWrappedError{ error: errors.New("foo"), - sentinel: SentinelHTTPError{ + sentinel: SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", }, - }.HTTPError() + }.HttpError() if actualStatus != expectStatus { t.Errorf("expected %d but got %d", expectStatus, actualStatus) @@ -91,13 +91,13 @@ func TestWrapError(t *testing.T) { expect := sentinelWrappedError{ error: errFoo, - sentinel: SentinelHTTPError{ + sentinel: SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", }, } - actual := WrapError(errFoo, SentinelHTTPError{ + actual := WrapError(errFoo, SentinelHttpError{ status: http.StatusInternalServerError, message: "foo", }) diff --git a/pkg/modules/api/formdata.go b/pkg/modules/api/formdata.go index 37a2835d..2ceb5e15 100644 --- a/pkg/modules/api/formdata.go +++ b/pkg/modules/api/formdata.go @@ -24,7 +24,7 @@ type FormData struct { } // Validate returns nil or an error related to the [FormData] values, with a -// [SentinelHTTPError] (status code 400, errors' details as message) wrapped +// [SentinelHttpError] (status code 400, errors' details as message) wrapped // inside. // // var foo string @@ -32,14 +32,14 @@ type FormData struct { // err := ctx.FormData(). // MandatoryString("foo", &foo, "bar"). // Validate() -func (form FormData) Validate() error { +func (form *FormData) Validate() error { if form.errors == nil { return nil } return WrapError( form.errors, - NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)), + NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)), ) } diff --git a/pkg/modules/api/formdata_test.go b/pkg/modules/api/formdata_test.go index 8773c9d0..c11706cd 100644 --- a/pkg/modules/api/formdata_test.go +++ b/pkg/modules/api/formdata_test.go @@ -9,49 +9,61 @@ import ( ) func TestFormData_Validate(t *testing.T) { - for i, tc := range []struct { - form FormData - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expectError bool }{ { - form: FormData{ + scenario: "errors", + form: &FormData{ errors: errors.New("foo"), }, - expectErr: true, + expectError: true, }, { - form: FormData{ + scenario: "success", + form: &FormData{ errors: nil, }, + expectError: false, }, } { - err := tc.form.Validate() + t.Run(tc.scenario, func(t *testing.T) { + err := tc.form.Validate() - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) - } + if tc.expectError && err == nil { + t.Fatal("expected error but got none", err) + } - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } + if !tc.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } + }) } } func TestFormData_String(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue string expect string }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: "", + expect: "", }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: "foo", expect: "foo", }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -59,8 +71,11 @@ func TestFormData_String(t *testing.T) { }, }, }, + defaultValue: "", + expect: "", }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -68,34 +83,41 @@ func TestFormData_String(t *testing.T) { }, }, }, - expect: "foo", + defaultValue: "", + expect: "foo", }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.String("foo", &actual, tc.defaultValue) + tc.form.String("foo", &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if tc.form.errors != nil { + t.Errorf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryString(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect string - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect string + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: "", + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -103,9 +125,11 @@ func TestFormData_MandatoryString(t *testing.T) { }, }, }, - expectErr: true, + expect: "", + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -113,43 +137,54 @@ func TestFormData_MandatoryString(t *testing.T) { }, }, }, - expect: "foo", + expect: "foo", + expectError: false, }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.MandatoryString("foo", &actual) + tc.form.MandatoryString("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Bool(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue bool expect bool - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: false, + expect: false, + expectError: false, }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: true, expect: true, + expectError: false, }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -157,8 +192,12 @@ func TestFormData_Bool(t *testing.T) { }, }, }, + defaultValue: false, + expect: false, + expectError: false, }, { + scenario: "key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -166,9 +205,12 @@ func TestFormData_Bool(t *testing.T) { }, }, }, - expectErr: true, + defaultValue: false, + expect: false, + expectError: true, }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -176,38 +218,46 @@ func TestFormData_Bool(t *testing.T) { }, }, }, - expect: true, + defaultValue: false, + expect: true, + expectError: false, }, } { - var actual bool + t.Run(tc.scenario, func(t *testing.T) { + var actual bool - tc.form.Bool("foo", &actual, tc.defaultValue) + tc.form.Bool("foo", &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected %t but got %t", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %t but got %t", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryBool(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect bool - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect bool + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: false, + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -215,9 +265,11 @@ func TestFormData_MandatoryBool(t *testing.T) { }, }, }, - expectErr: true, + expect: false, + expectError: true, }, { + scenario: "mandatory value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -225,9 +277,11 @@ func TestFormData_MandatoryBool(t *testing.T) { }, }, }, - expectErr: true, + expect: false, + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -235,43 +289,54 @@ func TestFormData_MandatoryBool(t *testing.T) { }, }, }, - expect: true, + expect: true, + expectError: false, }, } { - var actual bool + t.Run(tc.scenario, func(t *testing.T) { + var actual bool - tc.form.MandatoryBool("foo", &actual) + tc.form.MandatoryBool("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected %t but got %t", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %t but got %t", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Int(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue int expect int - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: 0, + expect: 0, + expectError: false, }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: 2, expect: 2, + expectError: false, }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -279,8 +344,12 @@ func TestFormData_Int(t *testing.T) { }, }, }, + defaultValue: 0, + expect: 0, + expectError: false, }, { + scenario: "key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -288,9 +357,12 @@ func TestFormData_Int(t *testing.T) { }, }, }, - expectErr: true, + defaultValue: 0, + expect: 0, + expectError: true, }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -298,38 +370,46 @@ func TestFormData_Int(t *testing.T) { }, }, }, - expect: 3, + defaultValue: 0, + expect: 3, + expectError: false, }, } { - var actual int + t.Run(tc.scenario, func(t *testing.T) { + var actual int - tc.form.Int("foo", &actual, tc.defaultValue) + tc.form.Int("foo", &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected %d but got %d", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %d but got %d", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryInt(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect int - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect int + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: 0, + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -337,9 +417,11 @@ func TestFormData_MandatoryInt(t *testing.T) { }, }, }, - expectErr: true, + expect: 0, + expectError: true, }, { + scenario: "mandatory value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -347,9 +429,11 @@ func TestFormData_MandatoryInt(t *testing.T) { }, }, }, - expectErr: true, + expect: 0, + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -357,43 +441,54 @@ func TestFormData_MandatoryInt(t *testing.T) { }, }, }, - expect: 2, + expect: 2, + expectError: false, }, } { - var actual int + t.Run(tc.scenario, func(t *testing.T) { + var actual int - tc.form.MandatoryInt("foo", &actual) + tc.form.MandatoryInt("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected %d but got %d", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %d but got %d", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Float64(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue float64 expect float64 - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: 0.0, + expect: 0.0, + expectError: false, }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: 2.5, expect: 2.5, + expectError: false, }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -401,8 +496,12 @@ func TestFormData_Float64(t *testing.T) { }, }, }, + defaultValue: 0.0, + expect: 0.0, + expectError: false, }, { + scenario: "key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -410,9 +509,12 @@ func TestFormData_Float64(t *testing.T) { }, }, }, - expectErr: true, + defaultValue: 0.0, + expect: 0.0, + expectError: true, }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -420,38 +522,46 @@ func TestFormData_Float64(t *testing.T) { }, }, }, - expect: 3.5, + defaultValue: 0.0, + expect: 3.5, + expectError: false, }, } { - var actual float64 + t.Run(tc.scenario, func(t *testing.T) { + var actual float64 - tc.form.Float64("foo", &actual, tc.defaultValue) + tc.form.Float64("foo", &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected %f but got %f", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %f but got %f", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryFloat64(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect float64 - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect float64 + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: 0.0, + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -459,9 +569,11 @@ func TestFormData_MandatoryFloat64(t *testing.T) { }, }, }, - expectErr: true, + expect: 0.0, + expectError: true, }, { + scenario: "mandatory value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -469,9 +581,11 @@ func TestFormData_MandatoryFloat64(t *testing.T) { }, }, }, - expectErr: true, + expect: 0.0, + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -479,43 +593,53 @@ func TestFormData_MandatoryFloat64(t *testing.T) { }, }, }, - expect: 2.5, + expect: 2.5, + expectError: false, }, } { - var actual float64 + t.Run(tc.scenario, func(t *testing.T) { + var actual float64 - tc.form.MandatoryFloat64("foo", &actual) + tc.form.MandatoryFloat64("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected %f but got %f", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected %f but got %f", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Duration(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue time.Duration expect time.Duration - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: time.Duration(0), + expect: time.Duration(0), + expectError: false, }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: time.Duration(1) * time.Second, expect: time.Duration(1) * time.Second, }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -523,8 +647,12 @@ func TestFormData_Duration(t *testing.T) { }, }, }, + defaultValue: time.Duration(0), + expect: time.Duration(0), + expectError: false, }, { + scenario: "key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -532,9 +660,12 @@ func TestFormData_Duration(t *testing.T) { }, }, }, - expectErr: true, + defaultValue: time.Duration(0), + expect: time.Duration(0), + expectError: true, }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -542,38 +673,46 @@ func TestFormData_Duration(t *testing.T) { }, }, }, - expect: time.Duration(1) * time.Second, + defaultValue: time.Duration(0), + expect: time.Duration(1) * time.Second, + expectError: false, }, } { - var actual time.Duration + t.Run(tc.scenario, func(t *testing.T) { + var actual time.Duration - tc.form.Duration("foo", &actual, tc.defaultValue) + tc.form.Duration("foo", &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryDuration(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect time.Duration - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect time.Duration + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: time.Duration(0), + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -581,9 +720,11 @@ func TestFormData_MandatoryDuration(t *testing.T) { }, }, }, - expectErr: true, + expect: time.Duration(0), + expectError: true, }, { + scenario: "mandatory value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -591,9 +732,11 @@ func TestFormData_MandatoryDuration(t *testing.T) { }, }, }, - expectErr: true, + expect: time.Duration(0), + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -601,43 +744,54 @@ func TestFormData_MandatoryDuration(t *testing.T) { }, }, }, - expect: time.Duration(1) * time.Second, + expect: time.Duration(1) * time.Second, + expectError: false, }, } { - var actual time.Duration + t.Run(tc.scenario, func(t *testing.T) { + var actual time.Duration - tc.form.MandatoryDuration("foo", &actual) + tc.form.MandatoryDuration("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Custom(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData defaultValue map[string]string expect map[string]string - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: nil, + expect: nil, + expectError: false, }, { + scenario: "key does not exist, fallback to default value", form: &FormData{}, defaultValue: map[string]string{"foo": "foo"}, expect: map[string]string{"foo": "foo"}, + expectError: false, }, { + scenario: "key does exist, but empty value, fallback to default value", form: &FormData{ values: map[string][]string{ "foo": { @@ -645,8 +799,12 @@ func TestFormData_Custom(t *testing.T) { }, }, }, + defaultValue: nil, + expect: nil, + expectError: false, }, { + scenario: "key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -654,9 +812,12 @@ func TestFormData_Custom(t *testing.T) { }, }, }, - expectErr: true, + defaultValue: nil, + expect: nil, + expectError: true, }, { + scenario: "key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -664,51 +825,59 @@ func TestFormData_Custom(t *testing.T) { }, }, }, - expect: map[string]string{"foo": "foo"}, + defaultValue: nil, + expect: map[string]string{"foo": "foo"}, + expectError: false, }, } { - var actual map[string]string + t.Run(tc.scenario, func(t *testing.T) { + var actual map[string]string - tc.form.Custom("foo", func(value string) error { - if value == "" { - actual = tc.defaultValue + tc.form.Custom("foo", func(value string) error { + if value == "" { + actual = tc.defaultValue + + return nil + } + + err := json.Unmarshal([]byte(value), &actual) + if err != nil { + return err + } return nil + }) + + if !reflect.DeepEqual(actual, tc.expect) { + t.Errorf("expected %+v but got: %+v", tc.expect, actual) } - err := json.Unmarshal([]byte(value), &actual) - if err != nil { - return err + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) } - return nil + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } }) - - if !reflect.DeepEqual(actual, tc.expect) { - t.Errorf("test %d: expected %+v but got: %+v", i, tc.expect, actual) - } - - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } - - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } } } func TestFormData_MandatoryCustom(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect map[string]string - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect map[string]string + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory key", + form: &FormData{}, + expect: nil, + expectError: true, }, { + scenario: "mandatory value is empty", form: &FormData{ values: map[string][]string{ "foo": { @@ -716,9 +885,11 @@ func TestFormData_MandatoryCustom(t *testing.T) { }, }, }, - expectErr: true, + expect: nil, + expectError: true, }, { + scenario: "mandatory key does exist, but value is invalid", form: &FormData{ values: map[string][]string{ "foo": { @@ -726,9 +897,11 @@ func TestFormData_MandatoryCustom(t *testing.T) { }, }, }, - expectErr: true, + expect: nil, + expectError: true, }, { + scenario: "mandatory key does exist with a value", form: &FormData{ values: map[string][]string{ "foo": { @@ -736,50 +909,59 @@ func TestFormData_MandatoryCustom(t *testing.T) { }, }, }, - expect: map[string]string{"foo": "foo"}, + expect: map[string]string{"foo": "foo"}, + expectError: false, }, } { - var actual map[string]string + t.Run(tc.scenario, func(t *testing.T) { + var actual map[string]string - tc.form.MandatoryCustom("foo", func(value string) error { - err := json.Unmarshal([]byte(value), &actual) - if err != nil { - return err + tc.form.MandatoryCustom("foo", func(value string) error { + err := json.Unmarshal([]byte(value), &actual) + if err != nil { + return err + } + + return nil + }) + + if !reflect.DeepEqual(actual, tc.expect) { + t.Errorf("expected %+v but got: %+v", tc.expect, actual) } - return nil + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } + + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } }) - - if !reflect.DeepEqual(actual, tc.expect) { - t.Errorf("test %d: expected %+v but got: %+v", i, tc.expect, actual) - } - - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } - - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } } } func TestFormData_Path(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect string + for _, tc := range []struct { + scenario string + form *FormData + expect string }{ { - form: &FormData{}, + scenario: "no file, fallback to zero value", + form: &FormData{}, + expect: "", }, { + scenario: "file does not exist, fallback to zero value", form: &FormData{ files: map[string]string{ "bar": "/bar", }, }, + expect: "", }, { + scenario: "file does exist", form: &FormData{ files: map[string]string{ "foo": "/foo", @@ -788,85 +970,107 @@ func TestFormData_Path(t *testing.T) { expect: "/foo", }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.Path("foo", &actual) + tc.form.Path("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if tc.form.errors != nil { + t.Errorf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryPath(t *testing.T) { - for i, tc := range []struct { - form *FormData - expect string - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + expect string + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory file: no file", + form: &FormData{}, + expect: "", + expectError: true, }, { + scenario: "missing mandatory file: file does not exist", form: &FormData{ files: map[string]string{ "bar": "/bar", }, }, - expectErr: true, + expect: "", + expectError: true, }, { + scenario: "mandatory file does exist", form: &FormData{ files: map[string]string{ "foo": "/foo", }, }, - expect: "/foo", + expect: "/foo", + expectError: false, }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.MandatoryPath("foo", &actual) + tc.form.MandatoryPath("foo", &actual) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Content(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData filename string defaultValue string expect string - expectErr bool + expectError bool }{ { - form: &FormData{}, + scenario: "no file, fallback to zero value", + form: &FormData{}, + filename: "", + defaultValue: "", + expect: "", + expectError: false, }, { + scenario: "file does not exist, fallback to zero value", form: &FormData{ files: map[string]string{ "bar": "/bar", }, }, - filename: "foo", + filename: "foo", + defaultValue: "", + expect: "", + expectError: false, }, { + scenario: "file does not exist, fallback to default value", form: &FormData{ files: map[string]string{ "bar": "/bar", @@ -875,177 +1079,201 @@ func TestFormData_Content(t *testing.T) { filename: "foo", defaultValue: "foo", expect: "foo", + expectError: false, }, { + scenario: "file does not exist, cannot read its content", form: &FormData{ files: map[string]string{ "foo": "/foo", }, }, - filename: "foo", - expectErr: true, + filename: "foo", + defaultValue: "", + expect: "", + expectError: true, }, { + scenario: "file does exist without file extension", form: &FormData{ files: map[string]string{ "foo": "/tests/test/testdata/api/sample1.txt", }, }, - filename: "foo", - expect: "foo", + filename: "foo", + defaultValue: "", + expect: "foo", + 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", }, }, - filename: "foo.txt", - expect: "foo", + filename: "foo.txt", + defaultValue: "", + expect: "foo", + 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", }, }, - filename: "foo.txt", - expect: "foo", + filename: "foo.txt", + defaultValue: "", + expect: "foo", + expectError: false, }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.Content(tc.filename, &actual, tc.defaultValue) + tc.form.Content(tc.filename, &actual, tc.defaultValue) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryContent(t *testing.T) { - for i, tc := range []struct { - form *FormData - filename string - expect string - expectErr bool + for _, tc := range []struct { + scenario string + form *FormData + filename string + expect string + expectError bool }{ { - form: &FormData{}, - filename: "foo", - expectErr: true, + scenario: "missing mandatory file: no file", + form: &FormData{}, + filename: "foo", + expect: "", + expectError: true, }, { + scenario: "missing mandatory file: file does not exist", form: &FormData{ files: map[string]string{ "bar": "/bar", }, }, - filename: "foo", - expectErr: true, + filename: "foo", + expect: "", + expectError: true, }, { + scenario: "mandatory file does exist, cannot read its content", form: &FormData{ files: map[string]string{ "foo": "/foo", }, }, - filename: "foo", - expectErr: true, + filename: "foo", + expect: "", + expectError: true, }, { + scenario: "mandatory file does exist without file extension", form: &FormData{ files: map[string]string{ "foo": "/tests/test/testdata/api/sample1.txt", }, }, - filename: "foo", - expect: "foo", + filename: "foo", + expect: "foo", + 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", }, }, - filename: "foo.txt", - expect: "foo", + filename: "foo.txt", + expect: "foo", + 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", }, }, - filename: "foo.txt", - expect: "foo", + filename: "foo.txt", + expect: "foo", + expectError: false, }, } { - var actual string + t.Run(tc.scenario, func(t *testing.T) { + var actual string - tc.form.MandatoryContent(tc.filename, &actual) + tc.form.MandatoryContent(tc.filename, &actual) - if actual != tc.expect { - t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual) - } + if actual != tc.expect { + t.Errorf("expected '%s' but got '%s'", tc.expect, actual) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_Paths(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData extensions []string expect []string expectCount int }{ { - form: &FormData{}, + scenario: "no file, fallback to zero value", + form: &FormData{}, + extensions: nil, + expect: nil, + expectCount: 0, }, { + scenario: "no file with given file extension, fallback to zero value", form: &FormData{ files: map[string]string{ "foo.zip": "/foo.zip", "foo.pdf": "/foo.pdf", }, }, - extensions: []string{".txt"}, + extensions: []string{".txt"}, + expect: nil, + expectCount: 0, }, { + scenario: "files do exist with given file extension", form: &FormData{ files: map[string]string{ "foo.zip": "/foo.zip", - "b.pdf": "/b.pdf", - "a.pdf": "/a.pdf", - }, - }, - extensions: []string{".pdf"}, - expect: []string{ - "/a.pdf", - "/b.pdf", - }, - expectCount: 2, - }, - { - form: &FormData{ - files: map[string]string{ - "foo.zip": "/foo.zip", - "b.PDF": "/b.PDF", + "b.pdf": "/b.PDF", "a.pdf": "/a.pdf", }, }, @@ -1057,62 +1285,58 @@ func TestFormData_Paths(t *testing.T) { expectCount: 2, }, } { - var actual []string + t.Run(tc.scenario, func(t *testing.T) { + var actual []string - tc.form.Paths(tc.extensions, &actual) + tc.form.Paths(tc.extensions, &actual) - if !reflect.DeepEqual(actual, tc.expect) { - t.Errorf("test %d: expected %v but got: %v", i, tc.expect, actual) - } + if !reflect.DeepEqual(actual, tc.expect) { + t.Errorf("expected %v but got: %v", tc.expect, actual) + } - if len(actual) != tc.expectCount { - t.Errorf("test %d: expected %d files but got %d", i, tc.expectCount, len(actual)) - } + if len(actual) != tc.expectCount { + t.Errorf("expected %d files but got %d", tc.expectCount, len(actual)) + } - if tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if tc.form.errors != nil { + t.Errorf("expected no error but got: %v", tc.form.errors) + } + }) } } func TestFormData_MandatoryPaths(t *testing.T) { - for i, tc := range []struct { + for _, tc := range []struct { + scenario string form *FormData extensions []string expect []string expectCount int - expectErr bool + expectError bool }{ { - form: &FormData{}, - expectErr: true, + scenario: "missing mandatory files: no file", + form: &FormData{}, + extensions: nil, + expect: nil, + expectCount: 0, + expectError: true, }, { + scenario: "missing mandatory files: no file with given file extension", form: &FormData{ files: map[string]string{ "foo.zip": "/foo.zip", "foo.pdf": "/foo.pdf", }, }, - extensions: []string{".txt"}, - expectErr: true, - }, - { - form: &FormData{ - files: map[string]string{ - "foo.zip": "/foo.zip", - "b.pdf": "/b.pdf", - "a.pdf": "/a.pdf", - }, - }, - extensions: []string{".pdf"}, - expect: []string{ - "/a.pdf", - "/b.pdf", - }, - expectCount: 2, + extensions: []string{".txt"}, + expect: nil, + expectCount: 0, + expectError: true, }, { + scenario: "mandatory files do exist with given file extension", form: &FormData{ files: map[string]string{ "foo.zip": "/foo.zip", @@ -1126,27 +1350,30 @@ func TestFormData_MandatoryPaths(t *testing.T) { "/b.PDF", }, expectCount: 2, + expectError: false, }, } { - var actual []string + t.Run(tc.scenario, func(t *testing.T) { + var actual []string - tc.form.MandatoryPaths(tc.extensions, &actual) + tc.form.MandatoryPaths(tc.extensions, &actual) - if !reflect.DeepEqual(actual, tc.expect) { - t.Errorf("test %d: expected %v but got: %v", i, tc.expect, actual) - } + if !reflect.DeepEqual(actual, tc.expect) { + t.Errorf("expected %v but got: %v", tc.expect, actual) + } - if len(actual) != tc.expectCount { - t.Errorf("test %d: expected %d files but got %d", i, tc.expectCount, len(actual)) - } + if len(actual) != tc.expectCount { + t.Errorf("expected %d files but got %d", tc.expectCount, len(actual)) + } - if tc.expectErr && tc.form.errors == nil { - t.Errorf("test %d: expected error but got: %v", i, tc.form.errors) - } + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } - if !tc.expectErr && tc.form.errors != nil { - t.Errorf("test %d: expected no error but got: %v", i, tc.form.errors) - } + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) } } diff --git a/pkg/modules/api/middlewares.go b/pkg/modules/api/middlewares.go index 74b54c35..df7e8778 100644 --- a/pkg/modules/api/middlewares.go +++ b/pkg/modules/api/middlewares.go @@ -32,9 +32,9 @@ func ParseError(err error) (int, string) { return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable) } - var httpErr HTTPError + var httpErr HttpError if errors.As(err, &httpErr) { - return httpErr.HTTPError() + return httpErr.HttpError() } // Default 500 status code. diff --git a/pkg/modules/api/middlewares_test.go b/pkg/modules/api/middlewares_test.go index a59a3c19..b273013e 100644 --- a/pkg/modules/api/middlewares_test.go +++ b/pkg/modules/api/middlewares_test.go @@ -36,7 +36,7 @@ func TestParseError(t *testing.T) { { err: WrapError( errors.New("foo"), - NewSentinelHTTPError(http.StatusBadRequest, "foo"), + NewSentinelHttpError(http.StatusBadRequest, "foo"), ), expectStatus: http.StatusBadRequest, expectMessage: "foo", @@ -73,7 +73,7 @@ func TestHttpErrorHandler(t *testing.T) { { err: WrapError( errors.New("foo"), - NewSentinelHTTPError(http.StatusBadRequest, "foo"), + NewSentinelHttpError(http.StatusBadRequest, "foo"), ), expectStatus: http.StatusBadRequest, expectMessage: "foo", diff --git a/pkg/modules/api/mocks.go b/pkg/modules/api/mocks.go index b0949e4f..670ca20e 100644 --- a/pkg/modules/api/mocks.go +++ b/pkg/modules/api/mocks.go @@ -1,6 +1,7 @@ package api import ( + "github.com/alexliesenfeld/health" "github.com/labstack/echo/v4" "go.uber.org/zap" ) @@ -63,7 +64,7 @@ func (ctx *ContextMock) SetCancelled(cancelled bool) { // // ctx := &api.ContextMock{Context: &api.Context{}} // outputPaths := ctx.OutputPaths() -func (ctx ContextMock) OutputPaths() []string { +func (ctx *ContextMock) OutputPaths() []string { return ctx.outputPaths } @@ -82,3 +83,37 @@ func (ctx *ContextMock) SetLogger(logger *zap.Logger) { func (ctx *ContextMock) SetEchoContext(c echo.Context) { ctx.Context.echoCtx = c } + +// RouterMock is a mock for the [Router] interface. +type RouterMock struct { + RoutesMock func() ([]Route, error) +} + +func (router *RouterMock) Routes() ([]Route, error) { + return router.RoutesMock() +} + +// MiddlewareProviderMock is a mock for the [MiddlewareProvider] interface. +type MiddlewareProviderMock struct { + MiddlewaresMock func() ([]Middleware, error) +} + +func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, error) { + return provider.MiddlewaresMock() +} + +// HealthCheckerMock is mock for the [HealthChecker] interface. +type HealthCheckerMock struct { + ChecksMock func() ([]health.CheckerOption, error) +} + +func (mod *HealthCheckerMock) Checks() ([]health.CheckerOption, error) { + return mod.ChecksMock() +} + +// Interface guards. +var ( + _ Router = (*RouterMock)(nil) + _ MiddlewareProvider = (*MiddlewareProviderMock)(nil) + _ HealthChecker = (*HealthCheckerMock)(nil) +) diff --git a/pkg/modules/api/mocks_test.go b/pkg/modules/api/mocks_test.go index 585f45fc..20e8bd67 100644 --- a/pkg/modules/api/mocks_test.go +++ b/pkg/modules/api/mocks_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/alexliesenfeld/health" "github.com/labstack/echo/v4" "go.uber.org/zap" ) @@ -115,3 +116,42 @@ func TestContextMock_SetEchoContext(t *testing.T) { 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 + }, + } + + _, err := mock.Checks() + if err != nil { + t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err) + } +} diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 39c06b3e..fbc4107a 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -304,7 +304,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route { if markdownFilesNotFoundErr != nil { return api.WrapError( fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr), ), @@ -340,7 +340,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, ErrUrlNotAuthorized) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusForbidden, fmt.Sprintf("'%s' does not match the authorized URLs", url), ), @@ -350,7 +350,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, "omitBackground requires printBackground set to true", ), @@ -367,7 +367,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression), ), @@ -377,7 +377,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, ErrInvalidPrinterSettings) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, "Chromium does not handle the provided settings; please check for aberrant form values", ), @@ -387,7 +387,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, ErrPageRangesSyntaxError) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, fmt.Sprintf("Chromium does not handle the page ranges '%s' (nativePageRanges)", options.PageRanges), ), @@ -397,7 +397,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, ErrConsoleExceptions) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusConflict, fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")), ), @@ -421,7 +421,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, 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), ), diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index da552e2c..d215730d 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -259,19 +259,19 @@ func TestConvertUrlRoute(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } @@ -350,19 +350,19 @@ func TestConvertHtmlRoute(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } @@ -557,19 +557,19 @@ func TestConvertMarkdownRoute(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } @@ -779,19 +779,19 @@ func TestConvertUrl(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index 74bef367..7f749f63 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -110,7 +110,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), - api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)), + api.NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)), ) } @@ -141,7 +141,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, 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), ), @@ -181,7 +181,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, 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), ), diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go index 602e972c..b3d42b43 100644 --- a/pkg/modules/libreoffice/routes_test.go +++ b/pkg/modules/libreoffice/routes_test.go @@ -525,19 +525,19 @@ func TestConvertRoute(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index f1a04302..ecf002f6 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -78,7 +78,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, 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), ), @@ -154,7 +154,7 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route { if pdfFormats == zeroValued { return api.WrapError( errors.New("no PDF formats"), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, "Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided", ), @@ -173,7 +173,7 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { return api.WrapError( fmt.Errorf("convert PDF: %w", err), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, 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), ), diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go index f209334a..cf71be10 100644 --- a/pkg/modules/pdfengines/routes_test.go +++ b/pkg/modules/pdfengines/routes_test.go @@ -193,19 +193,19 @@ func TestMergeHandler(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } @@ -396,19 +396,19 @@ func TestConvertHandler(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index 980aa734..d7709856 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -40,7 +40,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { if webhookErrorUrl == "" { return api.WrapError( errors.New("empty webhook error URL"), - api.NewSentinelHTTPError(http.StatusBadRequest, "Invalid 'Gotenberg-Webhook-Error-Url' header: empty value or header not provided"), + api.NewSentinelHttpError(http.StatusBadRequest, "Invalid 'Gotenberg-Webhook-Error-Url' header: empty value or header not provided"), ) } @@ -50,7 +50,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { if !allowList.MatchString(URL) { return api.WrapError( fmt.Errorf("'%s' does not match the expression from the allowed list", URL), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusForbidden, fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), ), @@ -60,7 +60,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { if denyList.String() != "" && denyList.MatchString(URL) { return api.WrapError( fmt.Errorf("'%s' matches the expression from the denied list", URL), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusForbidden, fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), ), @@ -101,7 +101,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { return "", api.WrapError( fmt.Errorf("webhook method '%s' is not '%s', '%s' or '%s'", method, http.MethodPost, http.MethodPatch, http.MethodPut), - api.NewSentinelHTTPError( + api.NewSentinelHttpError( http.StatusBadRequest, fmt.Sprintf("Invalid '%s' header value: expected '%s', '%s' or '%s', but got '%s'", header, http.MethodPost, http.MethodPatch, http.MethodPut, method), ), @@ -127,7 +127,7 @@ func webhookMiddleware(w *Webhook) api.Middleware { if err != nil { return api.WrapError( fmt.Errorf("unmarshal webhook extra HTTP headers: %w", err), - api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Gotenberg-Webhook-Extra-Http-Headers' header value: %s", err.Error())), + api.NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Gotenberg-Webhook-Extra-Http-Headers' header value: %s", err.Error())), ) } } diff --git a/pkg/modules/webhook/middleware_test.go b/pkg/modules/webhook/middleware_test.go index 7a9af1f4..992273cd 100644 --- a/pkg/modules/webhook/middleware_test.go +++ b/pkg/modules/webhook/middleware_test.go @@ -274,19 +274,19 @@ func TestWebhookMiddlewareGuards(t *testing.T) { t.Fatalf("expected no error but got: %v", err) } - var httpErr api.HTTPError - isHTTPErr := errors.As(err, &httpErr) + var httpErr api.HttpError + isHttpError := errors.As(err, &httpErr) - if tc.expectHttpError && !isHTTPErr { + if tc.expectHttpError && !isHttpError { t.Errorf("expected an HTTP error but got: %v", err) } - if !tc.expectHttpError && isHTTPErr { + if !tc.expectHttpError && isHttpError { t.Errorf("expected no HTTP error but got one: %v", httpErr) } - if err != nil && tc.expectHttpError && isHTTPErr { - status, _ := httpErr.HTTPError() + 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) } @@ -365,7 +365,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) { mod: buildWebhookModule(), next: func() echo.HandlerFunc { return func(c echo.Context) error { - return api.NewSentinelHTTPError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) + return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) } }(), expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,