chore: minor refactor of api module

This commit is contained in:
Julien Neuhart
2023-11-22 11:54:34 +01:00
parent 71eeca96ee
commit 3a28f4a0cb
20 changed files with 1228 additions and 904 deletions

View File

@@ -22,12 +22,12 @@ import (
) )
func init() { 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. // middlewares or health checks.
type API struct { type Api struct {
port int port int
readTimeout time.Duration readTimeout time.Duration
writeTimeout time.Duration writeTimeout time.Duration
@@ -44,7 +44,7 @@ type API struct {
srv *echo.Echo 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 { type Router interface {
Routes() ([]Route, error) Routes() ([]Route, error)
} }
@@ -72,7 +72,7 @@ type Route struct {
Handler echo.HandlerFunc 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 { type MiddlewareProvider interface {
Middlewares() ([]Middleware, error) Middlewares() ([]Middleware, error)
} }
@@ -99,7 +99,7 @@ const (
VeryHighPriority 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. // chain.
// //
// middleware := Middleware{ // middleware := Middleware{
@@ -149,8 +149,8 @@ type HealthChecker interface {
Checks() ([]health.CheckerOption, error) Checks() ([]health.CheckerOption, error)
} }
// Descriptor returns an [API]'s module descriptor. // Descriptor returns an [Api]'s module descriptor.
func (API) Descriptor() gotenberg.ModuleDescriptor { func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ return gotenberg.ModuleDescriptor{
ID: "api", ID: "api",
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
@@ -176,12 +176,12 @@ func (API) Descriptor() gotenberg.ModuleDescriptor {
return fs return fs
}(), }(),
New: func() gotenberg.Module { return new(API) }, New: func() gotenberg.Module { return new(Api) },
} }
} }
// Provision sets the module properties. // Provision sets the module properties.
func (a *API) Provision(ctx *gotenberg.Context) error { func (a *Api) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags() flags := ctx.ParsedFlags()
a.port = flags.MustInt("api-port") a.port = flags.MustInt("api-port")
a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout") 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. // Validate validates the module properties.
func (a API) Validate() error { func (a *Api) Validate() error {
var err error var err error
if a.port < 1 || a.port > 65535 { if a.port < 1 || a.port > 65535 {
@@ -369,7 +369,7 @@ func (a API) Validate() error {
} }
// Start starts the HTTP server. // Start starts the HTTP server.
func (a *API) Start() error { func (a *Api) Start() error {
a.srv = echo.New() a.srv = echo.New()
a.srv.HideBanner = true a.srv.HideBanner = true
a.srv.HidePort = true a.srv.HidePort = true
@@ -465,19 +465,19 @@ func (a *API) Start() error {
} }
// StartupMessage returns a custom startup message. // 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) return fmt.Sprintf("server listening on port %d", a.port)
} }
// Stop stops the HTTP server. // 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) return a.srv.Shutdown(ctx)
} }
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*API)(nil) _ gotenberg.Module = (*Api)(nil)
_ gotenberg.Provisioner = (*API)(nil) _ gotenberg.Provisioner = (*Api)(nil)
_ gotenberg.Validator = (*API)(nil) _ gotenberg.Validator = (*Api)(nil)
_ gotenberg.App = (*API)(nil) _ gotenberg.App = (*Api)(nil)
) )

View File

@@ -18,81 +18,30 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
type ProtoModule struct { func TestApi_Descriptor(t *testing.T) {
descriptor func() gotenberg.ModuleDescriptor descriptor := new(Api).Descriptor()
}
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()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(API)) expect := reflect.TypeOf(new(Api))
if actual != expect { if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual) t.Errorf("expected '%s' but got '%s'", expect, actual)
} }
} }
func TestAPI_Provision(t *testing.T) { func TestApi_Provision(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
ctx *gotenberg.Context ctx *gotenberg.Context
setEnv func(i int) setEnv func()
expectPort int expectPort int
expectMiddlewares []Middleware expectMiddlewares []Middleware
expectErr bool expectError bool
}{ }{
{ {
scenario: "port from env: non-existing environment variable",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=FOO"}) err := fs.Parse([]string{"--api-port-from-env=FOO"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
@@ -105,11 +54,12 @@ func TestAPI_Provision(t *testing.T) {
nil, nil,
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "port from env: empty environment variable",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"}) err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
@@ -122,17 +72,18 @@ func TestAPI_Provision(t *testing.T) {
nil, nil,
) )
}(), }(),
setEnv: func(i int) { setEnv: func() {
err := os.Setenv("PORT", "") err := os.Setenv("PORT", "")
if err != nil { 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 { ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet fs := new(Api).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"}) err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
@@ -145,235 +96,228 @@ func TestAPI_Provision(t *testing.T) {
nil, nil,
) )
}(), }(),
setEnv: func(i int) { setEnv: func() {
err := os.Setenv("PORT", "foo") 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 { if err != nil {
t.Fatalf("expected no error but got: %v", err) 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, expectError: true,
expectErr: true,
}, },
{ {
scenario: "no valid routers",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoRouter }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.ValidatorMock
RouterMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
} }
mod.validate = func() error { mod.ValidateMock = func() error {
return errors.New("foo") return errors.New("foo")
} }
mod.routes = func() ([]Route, error) { mod.RoutesMock = func() ([]Route, error) {
return nil, nil return nil, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "cannot retrieve routes from router",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoMiddlewareProvider }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
RouterMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} 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") return errors.New("foo")
} }
mod.middlewares = func() ([]Middleware, error) { mod.MiddlewaresMock = func() ([]Middleware, error) {
return nil, nil return nil, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "cannot retrieve middlewares from middleware provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoMiddlewareProvider }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
MiddlewareProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
} }
mod.validate = func() error { mod.MiddlewaresMock = func() ([]Middleware, error) {
return nil
}
mod.middlewares = func() ([]Middleware, error) {
return nil, errors.New("foo") return nil, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no valid health checkers",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoRouter }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.ValidatorMock
HealthCheckerMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
} }
mod.validate = func() error { mod.ValidateMock = 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 {
return errors.New("foo") return errors.New("foo")
} }
mod.checks = func() ([]health.CheckerOption, error) { mod.ChecksMock = func() ([]health.CheckerOption, error) {
return nil, nil return nil, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "cannot retrieve health checks from health checker",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoHealthChecker }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
HealthCheckerMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
} }
mod.validate = func() error { mod.ChecksMock = func() ([]health.CheckerOption, error) {
return nil
}
mod.checks = func() ([]health.CheckerOption, error) {
return nil, errors.New("foo") return nil, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{}, []gotenberg.ModuleDescriptor{},
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoLoggerProvider }{} mod := &struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} 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 nil, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: new(Api).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), mod.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "success",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod1 := struct{ ProtoRouter }{} mod1 := &struct {
mod1.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
RouterMock
}{}
mod1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
} }
mod1.validate = func() error { mod1.RoutesMock = func() ([]Route, error) {
return nil
}
mod1.routes = func() ([]Route, error) {
return []Route{{}}, nil return []Route{{}}, nil
} }
mod2 := struct{ ProtoMiddlewareProvider }{} mod2 := &struct {
mod2.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
MiddlewareProviderMock
}{}
mod2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }} return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
} }
mod2.validate = func() error { mod2.MiddlewaresMock = func() ([]Middleware, error) {
return nil
}
mod2.middlewares = func() ([]Middleware, error) {
return []Middleware{ return []Middleware{
{ {
Priority: VeryLowPriority, Priority: VeryLowPriority,
@@ -393,28 +337,37 @@ func TestAPI_Provision(t *testing.T) {
}, nil }, nil
} }
mod3 := struct{ ProtoHealthChecker }{} mod3 := &struct {
mod3.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
HealthCheckerMock
}{}
mod3.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }} return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
} }
mod3.validate = func() error { mod3.ChecksMock = func() ([]health.CheckerOption, error) {
return nil
}
mod3.checks = func() ([]health.CheckerOption, error) {
return []health.CheckerOption{health.WithDisabledAutostart()}, nil return []health.CheckerOption{health.WithDisabledAutostart()}, nil
} }
mod4 := struct{ ProtoLoggerProvider }{} mod4 := &struct {
mod4.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod4.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "qux", New: func() gotenberg.Module { return mod4 }} 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 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( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet, FlagSet: fs,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod1.Descriptor(), 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{ expectMiddlewares: []Middleware{
{ {
Priority: VeryHighPriority, Priority: VeryHighPriority,
@@ -441,52 +401,93 @@ func TestAPI_Provision(t *testing.T) {
Priority: VeryLowPriority, Priority: VeryLowPriority,
}, },
}, },
expectError: false,
}, },
} { } {
if tc.setEnv != nil { t.Run(tc.scenario, func(t *testing.T) {
tc.setEnv(i) if tc.setEnv != nil {
} tc.setEnv()
}
mod := new(API) mod := new(Api)
err := mod.Provision(tc.ctx) err := mod.Provision(tc.ctx)
if tc.expectPort != 0 && mod.port != tc.expectPort { if !tc.expectError && err != nil {
t.Errorf("test %d: expected port %d but got %d", i, tc.expectPort, mod.port) t.Fatalf("expected no error but got: %v", err)
} }
if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) { if tc.expectError && err == nil {
t.Errorf("test %d: expected %+v, but got: %+v", i, tc.expectMiddlewares, mod.externalMiddlewares) t.Fatal("expected error but got none")
} }
if tc.expectErr && err == nil { if tc.expectPort != 0 && mod.port != tc.expectPort {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("expected port %d but got %d", tc.expectPort, mod.port)
} }
if !tc.expectErr && err != nil { if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("expected %+v, but got: %+v", tc.expectMiddlewares, mod.externalMiddlewares)
} }
})
} }
} }
func TestAPI_Validate(t *testing.T) { func TestApi_Validate(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
port int port int
rootPath string rootPath string
traceHeader string traceHeader string
routes []Route routes []Route
middlewares []Middleware middlewares []Middleware
expectErr bool expectError bool
}{ }{
{ {
port: 0, scenario: "invalid port (< 1)",
expectErr: true, port: 0,
rootPath: "/foo/",
traceHeader: "foo",
routes: nil,
middlewares: nil,
expectError: true,
}, },
{ {
port: 65536, scenario: "invalid port (> 65535)",
rootPath: "foo", port: 65536,
expectErr: true, 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, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
@@ -495,9 +496,11 @@ func TestAPI_Validate(t *testing.T) {
Path: "", Path: "",
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid route: missing / prefix in path",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
@@ -506,9 +509,11 @@ func TestAPI_Validate(t *testing.T) {
Path: "foo", Path: "foo",
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid multipart route: no /forms prefix in path",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
@@ -518,34 +523,40 @@ func TestAPI_Validate(t *testing.T) {
IsMultipart: true, IsMultipart: true,
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid route: no method",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
routes: []Route{ routes: []Route{
{ {
Path: "/forms/foo", Path: "/foo",
IsMultipart: true, Method: "",
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid route: nil handler",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
routes: []Route{ routes: []Route{
{ {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/foo", Path: "/foo",
IsMultipart: true, Handler: nil,
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid route: path already existing",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
@@ -561,20 +572,25 @@ func TestAPI_Validate(t *testing.T) {
Handler: func(_ echo.Context) error { return nil }, Handler: func(_ echo.Context) error { return nil },
}, },
}, },
expectErr: true, middlewares: nil,
expectError: true,
}, },
{ {
scenario: "invalid middleware: nil handler",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
routes: nil,
middlewares: []Middleware{ middlewares: []Middleware{
{ {
Priority: HighPriority, Priority: HighPriority,
Handler: nil,
}, },
}, },
expectErr: true, expectError: true,
}, },
{ {
scenario: "success",
port: 10, port: 10,
rootPath: "/foo/", rootPath: "/foo/",
traceHeader: "foo", traceHeader: "foo",
@@ -584,6 +600,12 @@ func TestAPI_Validate(t *testing.T) {
Path: "/foo", Path: "/foo",
Handler: func(_ echo.Context) error { return nil }, Handler: func(_ echo.Context) error { return nil },
}, },
{
Method: http.MethodGet,
Path: "/forms/foo",
Handler: func(_ echo.Context) error { return nil },
IsMultipart: true,
},
}, },
middlewares: []Middleware{ middlewares: []Middleware{
{ {
@@ -599,28 +621,29 @@ func TestAPI_Validate(t *testing.T) {
}, },
}, },
} { } {
mod := API{ t.Run(tc.scenario, func(t *testing.T) {
port: tc.port, mod := Api{
rootPath: tc.rootPath, port: tc.port,
traceHeader: tc.traceHeader, rootPath: tc.rootPath,
routes: tc.routes, traceHeader: tc.traceHeader,
externalMiddlewares: tc.middlewares, 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 { if tc.expectError && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Fatal("expected error but got none")
} }
})
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
} }
} }
func TestAPI_Start(t *testing.T) { func TestApi_Start(t *testing.T) {
mod := new(API) mod := new(Api)
mod.port = 3000 mod.port = 3000
mod.rootPath = "/" mod.rootPath = "/"
mod.disableHealthCheckLogging = true mod.disableHealthCheckLogging = true
@@ -705,7 +728,7 @@ func TestAPI_Start(t *testing.T) {
} }
// "multipart/form-data" request. // "multipart/form-data" request.
multipartRequest := func(URL string) *http.Request { multipartRequest := func(url string) *http.Request {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@@ -732,7 +755,7 @@ func TestAPI_Start(t *testing.T) {
t.Fatalf("expected no error but got: %v", err) 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()) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req return req
@@ -758,8 +781,8 @@ func TestAPI_Start(t *testing.T) {
} }
} }
func TestAPI_StartupMessage(t *testing.T) { func TestApi_StartupMessage(t *testing.T) {
mod := API{ mod := Api{
port: 3000, port: 3000,
} }
@@ -771,8 +794,8 @@ func TestAPI_StartupMessage(t *testing.T) {
} }
} }
func TestAPI_Stop(t *testing.T) { func TestApi_Stop(t *testing.T) {
mod := API{ mod := &Api{
port: 3000, port: 3000,
routes: []Route{ routes: []Route{
{ {
@@ -794,21 +817,3 @@ func TestAPI_Stop(t *testing.T) {
t.Errorf("expected no error but got: %v", err) 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)
)

View File

@@ -93,21 +93,21 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
if errors.Is(err, http.ErrNotMultipart) { if errors.Is(err, http.ErrNotMultipart) {
return nil, cancel, WrapError( return nil, cancel, WrapError(
fmt.Errorf("get multipart form: %w", err), 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) { if errors.Is(err, http.ErrMissingBoundary) {
return nil, cancel, WrapError( return nil, cancel, WrapError(
fmt.Errorf("get multipart form: %w", err), 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()) { if strings.Contains(err.Error(), io.EOF.Error()) {
return nil, cancel, WrapError( return nil, cancel, WrapError(
fmt.Errorf("get multipart form: %w", err), 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 fields: %+v", ctx.values))
ctx.Log().Debug(fmt.Sprintf("form data files: %+v", ctx.files)) ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files))
return ctx, cancel, err return ctx, cancel, err
} }
// Request returns the [http.Request]. // Request returns the [http.Request].
func (ctx Context) Request() *http.Request { func (ctx *Context) Request() *http.Request {
return ctx.echoCtx.Request() return ctx.echoCtx.Request()
} }
// FormData return a [FormData]. // FormData return a [FormData].
func (ctx Context) FormData() *FormData { func (ctx *Context) FormData() *FormData {
return &FormData{ return &FormData{
values: ctx.values, values: ctx.values,
files: ctx.files, files: ctx.files,
@@ -201,7 +201,7 @@ func (ctx Context) FormData() *FormData {
// GeneratePath generates a path within the context's working directory. It // GeneratePath generates a path within the context's working directory. It
// does not create a file. // 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) 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]. // Log returns the context [zap.Logger].
func (ctx Context) Log() *zap.Logger { func (ctx *Context) Log() *zap.Logger {
return ctx.logger return ctx.logger
} }
// BuildOutputFile builds the output file according to the output paths // BuildOutputFile builds the output file according to the output paths
// registered in the context. If many output paths, an archive is created. // 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 { if ctx.cancelled {
return "", ErrContextAlreadyClosed return "", ErrContextAlreadyClosed
} }
@@ -268,7 +268,7 @@ func (ctx Context) BuildOutputFile() (string, error) {
// OutputFilename returns the filename based on the given output path or the // OutputFilename returns the filename based on the given output path or the
// "Gotenberg-Output-Filename" header's value. // "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") filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
if filename == "" { if filename == "" {

View File

@@ -19,138 +19,134 @@ import (
) )
func TestNewContext(t *testing.T) { func TestNewContext(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
request *http.Request request *http.Request
expectErr bool expectError bool
expectHTTPErr bool expectHttpError bool
expectHTTPStatus int expectHttpStatus int
}{ }{
{ {
scenario: "http.ErrNotMultipart",
request: httptest.NewRequest(http.MethodPost, "/", nil), request: httptest.NewRequest(http.MethodPost, "/", nil),
expectErr: true, expectError: true,
expectHTTPErr: true, expectHttpError: true,
expectHTTPStatus: http.StatusUnsupportedMediaType, expectHttpStatus: http.StatusUnsupportedMediaType,
}, },
{ {
scenario: "http.ErrMissingBoundary",
request: func() *http.Request { request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", nil) req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm) req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)
return req return req
}(), }(),
expectErr: true, expectError: true,
expectHTTPErr: true, expectHttpError: true,
expectHTTPStatus: http.StatusUnsupportedMediaType, expectHttpStatus: http.StatusUnsupportedMediaType,
}, },
{ {
scenario: "malformed body",
request: func() *http.Request { request: func() *http.Request {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
defer func() { defer func() {
err := writer.Close() err := writer.Close()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
}() }()
err := writer.WriteField("foo", "foo") err := writer.WriteField("foo", "foo")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
req := httptest.NewRequest(http.MethodPost, "/", nil) req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req return req
}(), }(),
expectErr: true, expectError: true,
expectHTTPErr: true, expectHttpError: true,
expectHTTPStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
}, },
{ {
scenario: "success",
request: func() *http.Request { request: func() *http.Request {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
defer func() { defer func() {
err := writer.Close() err := writer.Close()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
}() }()
err := writer.WriteField("foo", "foo") err := writer.WriteField("foo", "foo")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
part, err := writer.CreateFormFile("foo.txt", "foo.txt") part, err := writer.CreateFormFile("foo.txt", "foo.txt")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
_, err = part.Write([]byte("foo")) _, err = part.Write([]byte("foo"))
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
req := httptest.NewRequest(http.MethodPost, "/", body) req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
return req return req
}(), }(),
expectError: false,
expectHttpError: false,
}, },
} { } {
handler := func(c echo.Context) error { t.Run(tc.scenario, func(t *testing.T) {
_, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second) handler := func(c echo.Context) error {
defer cancel() _, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second)
// Context already cancelled. defer cancel()
defer cancel() // Context already cancelled.
defer cancel()
if err != nil { if err != nil {
return err return err
}
return nil
} }
return nil recorder := httptest.NewRecorder()
}
recorder := httptest.NewRecorder() srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
srv := echo.New() c := srv.NewContext(tc.request, recorder)
srv.HideBanner = true err := handler(c)
srv.HidePort = true
c := srv.NewContext(tc.request, recorder) if tc.expectError && err == nil {
err := handler(c) t.Fatal("expected error but got none", 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)
}
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.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() recorder := httptest.NewRecorder()
c := echo.New().NewContext(request, recorder) c := echo.New().NewContext(request, recorder)
ctx := Context{ ctx := &Context{
echoCtx: c, echoCtx: c,
} }
@@ -169,7 +165,7 @@ func TestContext_Request(t *testing.T) {
} }
func TestContext_FormData(t *testing.T) { func TestContext_FormData(t *testing.T) {
ctx := Context{ ctx := &Context{
values: map[string][]string{ values: map[string][]string{
"foo": {"foo"}, "foo": {"foo"},
}, },
@@ -190,7 +186,7 @@ func TestContext_FormData(t *testing.T) {
} }
func TestContext_GeneratePath(t *testing.T) { func TestContext_GeneratePath(t *testing.T) {
ctx := Context{ ctx := &Context{
dirPath: "/foo", dirPath: "/foo",
} }
@@ -202,40 +198,49 @@ func TestContext_GeneratePath(t *testing.T) {
} }
func TestContext_AddOutputPaths(t *testing.T) { func TestContext_AddOutputPaths(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
ctx *Context ctx *Context
path string path string
expectCount int expectCount int
expectErr bool expectError bool
}{ }{
{ {
ctx: &Context{cancelled: true}, scenario: "ErrContextAlreadyClosed",
expectErr: true, ctx: &Context{cancelled: true},
expectCount: 0,
expectError: true,
}, },
{ {
ctx: &Context{dirPath: "/foo"}, scenario: "ErrOutOfBoundsOutputPath",
path: "/bar/foo.txt", ctx: &Context{dirPath: "/foo"},
expectErr: true, path: "/bar/foo.txt",
expectCount: 0,
expectError: true,
}, },
{ {
scenario: "success",
ctx: &Context{dirPath: "/foo"}, ctx: &Context{dirPath: "/foo"},
path: "/foo/foo.txt", path: "/foo/foo.txt",
expectCount: 1, 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 { if tc.expectError && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Fatal("expected error but got none", err)
} }
if !tc.expectErr && err != nil { if !tc.expectError && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Fatalf("expected no error but got: %v", err)
} }
if len(tc.ctx.outputPaths) != tc.expectCount { 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)) 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) { func TestContext_BuildOutputFile(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
ctx *Context scenario string
expectErr bool ctx *Context
expectError bool
}{ }{
{ {
ctx: &Context{cancelled: true}, scenario: "ErrContextAlreadyClosed",
expectErr: true, ctx: &Context{cancelled: true},
expectError: true,
}, },
{ {
ctx: &Context{}, scenario: "no output path",
expectErr: true, 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"}}, scenario: "cannot archive: invalid output paths",
expectErr: true, ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
expectError: true,
}, },
{ {
scenario: "success: many output paths",
ctx: &Context{ ctx: &Context{
outputPaths: []string{ outputPaths: []string{
"/tests/test/testdata/api/sample1.txt", "/tests/test/testdata/api/sample1.txt",
"/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() fs := gotenberg.NewFileSystem()
dirPath, err := fs.MkdirAll() dirPath, err := fs.MkdirAll()
if err != nil { 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() { defer func() {
err := os.RemoveAll(fs.WorkingDirPath()) err := os.RemoveAll(fs.WorkingDirPath())
if err != nil { 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() _, err = tc.ctx.BuildOutputFile()
if tc.expectErr && err == nil { if tc.expectError && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Fatal("expected error but got none", err)
} }
if !tc.expectErr && err != nil { if !tc.expectError && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Fatalf("expected no error but got: %v", err)
} }
}() })
} }
} }
func TestContext_OutputFilename(t *testing.T) { func TestContext_OutputFilename(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
ctx *Context ctx *Context
outputPath string outputPath string
expectOutputFilename string expectOutputFilename string
}{ }{
{ {
scenario: "with Gotenberg-Output-Filename header",
ctx: func() *Context { ctx: func() *Context {
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil) c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
c.Request().Header.Set("Gotenberg-Output-Filename", "foo") c.Request().Header.Set("Gotenberg-Output-Filename", "foo")
return &Context{echoCtx: c} return &Context{echoCtx: c}
}(), }(),
outputPath: "/foo/bar.txt", outputPath: "/foo/bar.txt",
expectOutputFilename: "foo.txt", expectOutputFilename: "foo.txt",
}, },
{ {
scenario: "without custom filename",
ctx: func() *Context { ctx: func() *Context {
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil) c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
return &Context{echoCtx: c} return &Context{echoCtx: c}
@@ -333,10 +348,12 @@ func TestContext_OutputFilename(t *testing.T) {
expectOutputFilename: "foo.txt", 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 { if actual != tc.expectOutputFilename {
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectOutputFilename, actual) t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual)
} }
})
} }
} }

View File

@@ -2,66 +2,66 @@ package api
// Credits: https://www.joeshaw.org/error-handling-in-go-http-applications. // Credits: https://www.joeshaw.org/error-handling-in-go-http-applications.
// HTTPError is an interface allowing to retrieve the HTTP details of an error. // HttpError is an interface allowing to retrieve the HTTP details of an error.
type HTTPError interface { type HttpError interface {
HTTPError() (int, string) HttpError() (int, string)
} }
// SentinelHTTPError is the HTTP sidekick of an error. // SentinelHttpError is the HTTP sidekick of an error.
type SentinelHTTPError struct { type SentinelHttpError struct {
status int status int
message string 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 // as the response's body if returned from a handler, so make sure to not leak
// sensible information. // sensible information.
func NewSentinelHTTPError(status int, message string) SentinelHTTPError { func NewSentinelHttpError(status int, message string) SentinelHttpError {
return SentinelHTTPError{ return SentinelHttpError{
status: status, status: status,
message: message, message: message,
} }
} }
// Error returns the message. // Error returns the message.
func (err SentinelHTTPError) Error() string { func (err SentinelHttpError) Error() string {
return err.message return err.message
} }
// HTTPError returns the status and message. // HttpError returns the status and message.
func (err SentinelHTTPError) HTTPError() (int, string) { func (err SentinelHttpError) HttpError() (int, string) {
return err.status, err.message return err.status, err.message
} }
// sentinelWrappedError contains both the error which will logged and the // sentinelWrappedError contains both the error which will logged and the
// sidekick [SentinelHTTPError]. // sidekick [SentinelHttpError].
type sentinelWrappedError struct { type sentinelWrappedError struct {
error error
sentinel SentinelHTTPError sentinel SentinelHttpError
} }
func (w sentinelWrappedError) Is(err error) bool { func (w sentinelWrappedError) Is(err error) bool {
return w.sentinel == err return w.sentinel == err
} }
func (w sentinelWrappedError) HTTPError() (int, string) { func (w sentinelWrappedError) HttpError() (int, string) {
return w.sentinel.HTTPError() return w.sentinel.HttpError()
} }
// WrapError wraps the given error with a [SentinelHTTPError]. The wrapped // WrapError wraps the given error with a [SentinelHttpError]. The wrapped
// error will be displayed in a log, while the [SentinelHTTPError] will be sent // error will be displayed in a log, while the [SentinelHttpError] will be sent
// in the response. // in the response.
// //
// return api.WrapError( // return api.WrapError(
// // This first error will be logged. // // This first error will be logged.
// fmt.Errorf("my action: %w", err), // fmt.Errorf("my action: %w", err),
// // The HTTP error will be sent as a response. // // The HTTP error will be sent as a response.
// api.NewSentinelHTTPError( // api.NewSentinelHttpError(
// http.StatusForbidden, // http.StatusForbidden,
// "Hey, you did something wrong!" // "Hey, you did something wrong!"
// ), // ),
// ) // )
func WrapError(err error, sentinel SentinelHTTPError) error { func WrapError(err error, sentinel SentinelHttpError) error {
return sentinelWrappedError{ return sentinelWrappedError{
error: err, error: err,
sentinel: sentinel, sentinel: sentinel,
@@ -70,8 +70,8 @@ func WrapError(err error, sentinel SentinelHTTPError) error {
// Interface guards. // Interface guards.
var ( var (
_ error = (*SentinelHTTPError)(nil) _ error = (*SentinelHttpError)(nil)
_ HTTPError = (*SentinelHTTPError)(nil) _ HttpError = (*SentinelHttpError)(nil)
_ error = (*sentinelWrappedError)(nil) _ error = (*sentinelWrappedError)(nil)
_ HTTPError = (*sentinelWrappedError)(nil) _ HttpError = (*sentinelWrappedError)(nil)
) )

View File

@@ -7,9 +7,9 @@ import (
"testing" "testing"
) )
func TestNewSentinelHTTPError(t *testing.T) { func TestNewSentinelHttpError(t *testing.T) {
actual := NewSentinelHTTPError(http.StatusInternalServerError, "foo") actual := NewSentinelHttpError(http.StatusInternalServerError, "foo")
expect := SentinelHTTPError{ expect := SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
} }
@@ -19,8 +19,8 @@ func TestNewSentinelHTTPError(t *testing.T) {
} }
} }
func TestSentinelHTTPError_Error(t *testing.T) { func TestSentinelHttpError_Error(t *testing.T) {
err := SentinelHTTPError{ err := SentinelHttpError{
message: "foo", message: "foo",
} }
@@ -32,11 +32,11 @@ func TestSentinelHTTPError_Error(t *testing.T) {
} }
} }
func TestSentinelHTTPError_HTTPError(t *testing.T) { func TestSentinelHttpError_HttpError(t *testing.T) {
actualStatus, actualMessage := SentinelHTTPError{ actualStatus, actualMessage := SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
}.HTTPError() }.HttpError()
expectStatus := http.StatusInternalServerError expectStatus := http.StatusInternalServerError
expectMessage := "foo" expectMessage := "foo"
@@ -51,7 +51,7 @@ func TestSentinelHTTPError_HTTPError(t *testing.T) {
} }
func TestSentinelWrappedError_Is(t *testing.T) { func TestSentinelWrappedError_Is(t *testing.T) {
errSentinel := SentinelHTTPError{} errSentinel := SentinelHttpError{}
err := sentinelWrappedError{ err := sentinelWrappedError{
error: errors.New("foo"), error: errors.New("foo"),
@@ -63,19 +63,19 @@ func TestSentinelWrappedError_Is(t *testing.T) {
} }
} }
func TestSentinelWrappedError_HTTPError(t *testing.T) { func TestSentinelWrappedError_HttpError(t *testing.T) {
expectStatus, expectMessage := SentinelHTTPError{ expectStatus, expectMessage := SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
}.HTTPError() }.HttpError()
actualStatus, actualMessage := sentinelWrappedError{ actualStatus, actualMessage := sentinelWrappedError{
error: errors.New("foo"), error: errors.New("foo"),
sentinel: SentinelHTTPError{ sentinel: SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
}, },
}.HTTPError() }.HttpError()
if actualStatus != expectStatus { if actualStatus != expectStatus {
t.Errorf("expected %d but got %d", expectStatus, actualStatus) t.Errorf("expected %d but got %d", expectStatus, actualStatus)
@@ -91,13 +91,13 @@ func TestWrapError(t *testing.T) {
expect := sentinelWrappedError{ expect := sentinelWrappedError{
error: errFoo, error: errFoo,
sentinel: SentinelHTTPError{ sentinel: SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
}, },
} }
actual := WrapError(errFoo, SentinelHTTPError{ actual := WrapError(errFoo, SentinelHttpError{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
message: "foo", message: "foo",
}) })

View File

@@ -24,7 +24,7 @@ type FormData struct {
} }
// Validate returns nil or an error related to the [FormData] values, with a // 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. // inside.
// //
// var foo string // var foo string
@@ -32,14 +32,14 @@ type FormData struct {
// err := ctx.FormData(). // err := ctx.FormData().
// MandatoryString("foo", &foo, "bar"). // MandatoryString("foo", &foo, "bar").
// Validate() // Validate()
func (form FormData) Validate() error { func (form *FormData) Validate() error {
if form.errors == nil { if form.errors == nil {
return nil return nil
} }
return WrapError( return WrapError(
form.errors, form.errors,
NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)),
) )
} }

File diff suppressed because it is too large Load Diff

View File

@@ -32,9 +32,9 @@ func ParseError(err error) (int, string) {
return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable) return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable)
} }
var httpErr HTTPError var httpErr HttpError
if errors.As(err, &httpErr) { if errors.As(err, &httpErr) {
return httpErr.HTTPError() return httpErr.HttpError()
} }
// Default 500 status code. // Default 500 status code.

View File

@@ -36,7 +36,7 @@ func TestParseError(t *testing.T) {
{ {
err: WrapError( err: WrapError(
errors.New("foo"), errors.New("foo"),
NewSentinelHTTPError(http.StatusBadRequest, "foo"), NewSentinelHttpError(http.StatusBadRequest, "foo"),
), ),
expectStatus: http.StatusBadRequest, expectStatus: http.StatusBadRequest,
expectMessage: "foo", expectMessage: "foo",
@@ -73,7 +73,7 @@ func TestHttpErrorHandler(t *testing.T) {
{ {
err: WrapError( err: WrapError(
errors.New("foo"), errors.New("foo"),
NewSentinelHTTPError(http.StatusBadRequest, "foo"), NewSentinelHttpError(http.StatusBadRequest, "foo"),
), ),
expectStatus: http.StatusBadRequest, expectStatus: http.StatusBadRequest,
expectMessage: "foo", expectMessage: "foo",

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"github.com/alexliesenfeld/health"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -63,7 +64,7 @@ func (ctx *ContextMock) SetCancelled(cancelled bool) {
// //
// ctx := &api.ContextMock{Context: &api.Context{}} // ctx := &api.ContextMock{Context: &api.Context{}}
// outputPaths := ctx.OutputPaths() // outputPaths := ctx.OutputPaths()
func (ctx ContextMock) OutputPaths() []string { func (ctx *ContextMock) OutputPaths() []string {
return ctx.outputPaths return ctx.outputPaths
} }
@@ -82,3 +83,37 @@ func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
func (ctx *ContextMock) SetEchoContext(c echo.Context) { func (ctx *ContextMock) SetEchoContext(c echo.Context) {
ctx.Context.echoCtx = c 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)
)

View File

@@ -4,6 +4,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/alexliesenfeld/health"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -115,3 +116,42 @@ func TestContextMock_SetEchoContext(t *testing.T) {
t.Errorf("expected %v but got %v", expect, actual) 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)
}
}

View File

@@ -304,7 +304,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
if markdownFilesNotFoundErr != nil { if markdownFilesNotFoundErr != nil {
return api.WrapError( return api.WrapError(
fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr), fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr), 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) { if errors.Is(err, ErrUrlNotAuthorized) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusForbidden, http.StatusForbidden,
fmt.Sprintf("'%s' does not match the authorized URLs", url), 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) { if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
"omitBackground requires printBackground set to true", "omitBackground requires printBackground set to true",
), ),
@@ -367,7 +367,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression), 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) { if errors.Is(err, ErrInvalidPrinterSettings) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
"Chromium does not handle the provided settings; please check for aberrant form values", "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) { if errors.Is(err, ErrPageRangesSyntaxError) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("Chromium does not handle the page ranges '%s' (nativePageRanges)", options.PageRanges), 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) { if errors.Is(err, ErrConsoleExceptions) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusConflict, http.StatusConflict,
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")), 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) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, 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), fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
), ),

View File

@@ -259,19 +259,19 @@ func TestConvertUrlRoute(t *testing.T) {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) 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) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) 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) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) 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) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
} }

View File

@@ -110,7 +110,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) { if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), 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) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, 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), 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) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, 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), fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
), ),

View File

@@ -525,19 +525,19 @@ func TestConvertRoute(t *testing.T) {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
} }

View File

@@ -78,7 +78,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, 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), 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 { if pdfFormats == zeroValued {
return api.WrapError( return api.WrapError(
errors.New("no PDF formats"), errors.New("no PDF formats"),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
"Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided", "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) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, 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), fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
), ),

View File

@@ -193,19 +193,19 @@ func TestMergeHandler(t *testing.T) {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) 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) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
} }

View File

@@ -40,7 +40,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
if webhookErrorUrl == "" { if webhookErrorUrl == "" {
return api.WrapError( return api.WrapError(
errors.New("empty webhook error URL"), 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) { if !allowList.MatchString(URL) {
return api.WrapError( return api.WrapError(
fmt.Errorf("'%s' does not match the expression from the allowed list", URL), fmt.Errorf("'%s' does not match the expression from the allowed list", URL),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusForbidden, http.StatusForbidden,
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), 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) { if denyList.String() != "" && denyList.MatchString(URL) {
return api.WrapError( return api.WrapError(
fmt.Errorf("'%s' matches the expression from the denied list", URL), fmt.Errorf("'%s' matches the expression from the denied list", URL),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusForbidden, http.StatusForbidden,
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL), 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( return "", api.WrapError(
fmt.Errorf("webhook method '%s' is not '%s', '%s' or '%s'", method, http.MethodPost, http.MethodPatch, http.MethodPut), fmt.Errorf("webhook method '%s' is not '%s', '%s' or '%s'", method, http.MethodPost, http.MethodPatch, http.MethodPut),
api.NewSentinelHTTPError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("Invalid '%s' header value: expected '%s', '%s' or '%s', but got '%s'", header, http.MethodPost, http.MethodPatch, http.MethodPut, method), 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 { if err != nil {
return api.WrapError( return api.WrapError(
fmt.Errorf("unmarshal webhook extra HTTP headers: %w", err), 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())),
) )
} }
} }

View File

@@ -274,19 +274,19 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
var httpErr api.HTTPError var httpErr api.HttpError
isHTTPErr := errors.As(err, &httpErr) isHttpError := errors.As(err, &httpErr)
if tc.expectHttpError && !isHTTPErr { if tc.expectHttpError && !isHttpError {
t.Errorf("expected an HTTP error but got: %v", err) 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) t.Errorf("expected no HTTP error but got one: %v", httpErr)
} }
if err != nil && tc.expectHttpError && isHTTPErr { if err != nil && tc.expectHttpError && isHttpError {
status, _ := httpErr.HTTPError() status, _ := httpErr.HttpError()
if status != tc.expectHttpStatus { if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) 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(), mod: buildWebhookModule(),
next: func() echo.HandlerFunc { next: func() echo.HandlerFunc {
return func(c echo.Context) error { 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, expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,