mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
feat: add metrics system, move webhook feature to dedicated module (#372)
This commit is contained in:
@@ -3,28 +3,21 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestHttpErrorHandler(t *testing.T) {
|
||||
func TestParseError(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
err error
|
||||
webhookClient *webhookClient
|
||||
expectStatus int
|
||||
expectMessage string
|
||||
}{
|
||||
@@ -46,30 +39,42 @@ func TestHttpErrorHandler(t *testing.T) {
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "foo",
|
||||
},
|
||||
} {
|
||||
actualStatus, actualMessage := ParseError(tc.err)
|
||||
|
||||
if actualStatus != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, actualStatus)
|
||||
}
|
||||
|
||||
if actualMessage != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, actualMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpErrorHandler(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
err error
|
||||
expectStatus int
|
||||
expectMessage string
|
||||
}{
|
||||
{
|
||||
err: echo.ErrInternalServerError,
|
||||
webhookClient: &webhookClient{
|
||||
errorURL: "http://localhost:%d/",
|
||||
errorMethod: http.MethodPost,
|
||||
client: retryablehttp.NewClient(),
|
||||
logger: zap.NewNop(),
|
||||
},
|
||||
err: echo.ErrInternalServerError,
|
||||
expectStatus: http.StatusInternalServerError,
|
||||
expectMessage: http.StatusText(http.StatusInternalServerError),
|
||||
},
|
||||
{
|
||||
err: echo.ErrInternalServerError,
|
||||
webhookClient: &webhookClient{
|
||||
errorURL: "non-existent",
|
||||
errorMethod: http.MethodPost,
|
||||
client: func() *retryablehttp.Client {
|
||||
client := retryablehttp.NewClient()
|
||||
client.RetryMax = 0
|
||||
|
||||
return client
|
||||
}(),
|
||||
logger: zap.NewNop(),
|
||||
},
|
||||
err: context.DeadlineExceeded,
|
||||
expectStatus: http.StatusServiceUnavailable,
|
||||
expectMessage: http.StatusText(http.StatusServiceUnavailable),
|
||||
},
|
||||
{
|
||||
err: WrapError(
|
||||
errors.New("foo"),
|
||||
NewSentinelHTTPError(http.StatusBadRequest, "foo"),
|
||||
),
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "foo",
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -81,105 +86,24 @@ func TestHttpErrorHandler(t *testing.T) {
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
c.Set("trace", "foo")
|
||||
|
||||
if tc.webhookClient != nil {
|
||||
c.Set("webhookClient", tc.webhookClient)
|
||||
handler := httpErrorHandler()
|
||||
handler(tc.err, c)
|
||||
|
||||
contentType := recorder.Header().Get(echo.HeaderContentType)
|
||||
if contentType != echo.MIMETextPlainCharsetUTF8 {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8, contentType)
|
||||
}
|
||||
|
||||
if tc.webhookClient == nil {
|
||||
handler := httpErrorHandler("Gotenberg-Trace")
|
||||
handler(tc.err, c)
|
||||
// Note: we cannot test the trace header in the response here, as it is set in the trace middleware.
|
||||
|
||||
contentType := recorder.Header().Get(echo.HeaderContentType)
|
||||
if contentType != echo.MIMETextPlainCharsetUTF8 {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8, contentType)
|
||||
}
|
||||
|
||||
// Note: we cannot test the trace header in the response here, as it is set in the trace middleware.
|
||||
|
||||
if recorder.Code != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
|
||||
}
|
||||
|
||||
if recorder.Body.String() != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, recorder.Body.String())
|
||||
}
|
||||
|
||||
continue
|
||||
if recorder.Code != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
|
||||
}
|
||||
|
||||
func() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
webhookPort := rand.Intn(65535-1025+1) + 1025
|
||||
|
||||
tc.webhookClient.errorURL = fmt.Sprintf(tc.webhookClient.errorURL, webhookPort)
|
||||
|
||||
c.Set("webhookClient", tc.webhookClient)
|
||||
|
||||
webhook := echo.New()
|
||||
webhook.HideBanner = true
|
||||
webhook.HidePort = true
|
||||
|
||||
webhook.POST(
|
||||
"/",
|
||||
func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
contentType := c.Request().Header.Get(echo.HeaderContentType)
|
||||
if contentType != echo.MIMEApplicationJSONCharsetUTF8 {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8, contentType)
|
||||
}
|
||||
|
||||
trace := c.Request().Header.Get("Gotenberg-Trace")
|
||||
if trace != "foo" {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, "Gotenberg-Trace", "foo", trace)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected not error but got: %v", i, err)
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected not error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if result.Status != tc.expectStatus {
|
||||
t.Errorf("test %d: expected status %d from JSON but got %d", i, tc.expectStatus, result.Status)
|
||||
}
|
||||
|
||||
if result.Message != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' from JSON but got '%s'", i, tc.expectMessage, result.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
)
|
||||
|
||||
go func(server *echo.Echo, port, i int) {
|
||||
err := webhook.Start(fmt.Sprintf(":%d", port))
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}(webhook, webhookPort, i)
|
||||
|
||||
defer func() {
|
||||
err := webhook.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}()
|
||||
|
||||
handler := httpErrorHandler("Gotenberg-Trace")
|
||||
handler(tc.err, c)
|
||||
}()
|
||||
if recorder.Body.String() != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,11 +222,52 @@ func TestTraceMiddleware(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutsMiddleware(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
|
||||
expectReadTimeout := time.Duration(1) * time.Second
|
||||
expectProcessTimeout := time.Duration(2) * time.Second
|
||||
expectWriteTimeout := time.Duration(3) * time.Second
|
||||
|
||||
err := timeoutsMiddleware(expectReadTimeout, expectProcessTimeout, expectWriteTimeout)(
|
||||
func(c echo.Context) error {
|
||||
return nil
|
||||
},
|
||||
)(c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
actualReadTimeout := c.Get("readTimeout").(time.Duration)
|
||||
actualProcessTimeout := c.Get("processTimeout").(time.Duration)
|
||||
actualWriteTimeout := c.Get("writeTimeout").(time.Duration)
|
||||
|
||||
if actualReadTimeout != expectReadTimeout {
|
||||
t.Errorf("expected '%s' but got '%s", expectReadTimeout, actualReadTimeout)
|
||||
}
|
||||
|
||||
if actualProcessTimeout != expectProcessTimeout {
|
||||
t.Errorf("expected '%s' but got '%s", expectProcessTimeout, actualProcessTimeout)
|
||||
}
|
||||
|
||||
if actualWriteTimeout != expectWriteTimeout {
|
||||
t.Errorf("expected '%s' but got '%s", actualWriteTimeout, expectWriteTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerMiddleware(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
request *http.Request
|
||||
next echo.HandlerFunc
|
||||
skipHealthRouteLogging bool
|
||||
request *http.Request
|
||||
next echo.HandlerFunc
|
||||
skipLogging bool
|
||||
}{
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/", nil),
|
||||
@@ -319,7 +284,7 @@ func TestLoggerMiddleware(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
skipHealthRouteLogging: true,
|
||||
skipLogging: true,
|
||||
},
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/health", nil),
|
||||
@@ -341,7 +306,12 @@ func TestLoggerMiddleware(t *testing.T) {
|
||||
c.Set("trace", "foo")
|
||||
c.Set("rootPath", "/")
|
||||
|
||||
err := loggerMiddleware(zap.NewNop(), tc.skipHealthRouteLogging)(tc.next)(c)
|
||||
var disableLoggingForPaths []string
|
||||
if tc.skipLogging {
|
||||
disableLoggingForPaths = append(disableLoggingForPaths, tc.request.RequestURI)
|
||||
}
|
||||
|
||||
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
@@ -349,7 +319,7 @@ func TestLoggerMiddleware(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
func TestContextMiddleware(t *testing.T) {
|
||||
buildMultipartFormDataRequest := func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
@@ -376,6 +346,7 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
request *http.Request
|
||||
next echo.HandlerFunc
|
||||
expectErr bool
|
||||
expectStatus int
|
||||
expectContentType string
|
||||
expectFilename string
|
||||
}{
|
||||
@@ -383,6 +354,15 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
request: httptest.NewRequest(http.MethodGet, "/", nil),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return ErrAsyncProcess
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
@@ -418,6 +398,7 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusOK,
|
||||
expectContentType: "application/pdf",
|
||||
expectFilename: "foo.pdf",
|
||||
},
|
||||
@@ -434,6 +415,7 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusOK,
|
||||
expectContentType: "application/zip",
|
||||
},
|
||||
} {
|
||||
@@ -448,16 +430,7 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
c.Set("trace", "foo")
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
cfg := contextMiddlewareConfig{
|
||||
timeout: struct {
|
||||
process time.Duration
|
||||
write time.Duration
|
||||
}{
|
||||
process: time.Duration(10) * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
err := contextMiddleware(cfg)(tc.next)(c)
|
||||
err := contextMiddleware(time.Duration(10) * time.Second)(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
@@ -471,8 +444,12 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, http.StatusOK, recorder.Code)
|
||||
if recorder.Code != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
|
||||
}
|
||||
|
||||
if tc.expectStatus == http.StatusNoContent {
|
||||
continue
|
||||
}
|
||||
|
||||
contentType := recorder.Header().Get(echo.HeaderContentType)
|
||||
@@ -487,470 +464,7 @@ func TestContextMiddlewareWithoutWebhook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMiddlewareWithWebhook(t *testing.T) {
|
||||
buildMultipartFormDataRequest := func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err := writer.WriteField("foo", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
buildContextMiddlewareConfig := func() contextMiddlewareConfig {
|
||||
return contextMiddlewareConfig{
|
||||
traceHeader: "Gotenberg-Trace",
|
||||
timeout: struct {
|
||||
process time.Duration
|
||||
write time.Duration
|
||||
}{
|
||||
process: time.Duration(10) * time.Second,
|
||||
write: time.Duration(10) * time.Second,
|
||||
},
|
||||
webhook: struct {
|
||||
allowList *regexp.Regexp
|
||||
denyList *regexp.Regexp
|
||||
errorAllowList *regexp.Regexp
|
||||
errorDenyList *regexp.Regexp
|
||||
maxRetry int
|
||||
retryMinWait time.Duration
|
||||
retryMaxWait time.Duration
|
||||
disable bool
|
||||
}{
|
||||
allowList: regexp.MustCompile(""),
|
||||
denyList: regexp.MustCompile(""),
|
||||
errorAllowList: regexp.MustCompile(""),
|
||||
errorDenyList: regexp.MustCompile(""),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for i, tc := range []struct {
|
||||
request *http.Request
|
||||
cfg contextMiddlewareConfig
|
||||
next echo.HandlerFunc
|
||||
autoWebhookURLs bool
|
||||
expectErr bool
|
||||
expectHTTPErr bool
|
||||
expectHTTPStatus int
|
||||
expectWebhookContentType string
|
||||
expectWebhookMethod string
|
||||
expectWebhookExtraHTTPHeaders map[string]string
|
||||
expectWebhookFilename string
|
||||
}{
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: func() contextMiddlewareConfig {
|
||||
cfg := buildContextMiddlewareConfig()
|
||||
cfg.webhook.disable = true
|
||||
|
||||
return cfg
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: func() contextMiddlewareConfig {
|
||||
cfg := buildContextMiddlewareConfig()
|
||||
cfg.webhook.allowList = regexp.MustCompile("bar")
|
||||
|
||||
return cfg
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: func() contextMiddlewareConfig {
|
||||
cfg := buildContextMiddlewareConfig()
|
||||
cfg.webhook.denyList = regexp.MustCompile("foo")
|
||||
|
||||
return cfg
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: func() contextMiddlewareConfig {
|
||||
cfg := buildContextMiddlewareConfig()
|
||||
cfg.webhook.errorAllowList = regexp.MustCompile("foo")
|
||||
|
||||
return cfg
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: func() contextMiddlewareConfig {
|
||||
cfg := buildContextMiddlewareConfig()
|
||||
cfg.webhook.errorDenyList = regexp.MustCompile("bar")
|
||||
|
||||
return cfg
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Method", http.MethodGet)
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet)
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Method", http.MethodPost)
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet)
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Method", http.MethodPatch)
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet)
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Method", http.MethodPut)
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet)
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||
req.Header.Set("Gotenberg-Webhook-Extra-Http-Headers", "foo")
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
}(),
|
||||
autoWebhookURLs: true,
|
||||
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||
expectWebhookMethod: http.MethodPost,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
autoWebhookURLs: true,
|
||||
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||
expectWebhookMethod: http.MethodPost,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Output-Filename", "foo")
|
||||
req.Header.Set("Gotenberg-Webhook-Extra-Http-Headers", `{ "foo": "bar" }`)
|
||||
|
||||
return req
|
||||
}(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*Context)
|
||||
ctx.outputPaths = []string{
|
||||
"/tests/test/testdata/api/sample2.pdf",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
autoWebhookURLs: true,
|
||||
expectWebhookContentType: "application/pdf",
|
||||
expectWebhookMethod: http.MethodPost,
|
||||
expectWebhookFilename: "foo",
|
||||
expectWebhookExtraHTTPHeaders: map[string]string{"foo": "bar"},
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
cfg: buildContextMiddlewareConfig(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*Context)
|
||||
ctx.outputPaths = []string{
|
||||
"/tests/test/testdata/api/sample1.txt",
|
||||
"/tests/test/testdata/api/sample2.pdf",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
autoWebhookURLs: true,
|
||||
expectWebhookContentType: "application/zip",
|
||||
expectWebhookMethod: http.MethodPost,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
srv.HTTPErrorHandler = httpErrorHandler(tc.cfg.traceHeader)
|
||||
|
||||
c := srv.NewContext(tc.request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
c.Set("trace", "foo")
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
webhook := echo.New()
|
||||
webhook.HideBanner = true
|
||||
webhook.HidePort = true
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
webhookPort := rand.Intn(65535-1025+1) + 1025
|
||||
|
||||
if tc.autoWebhookURLs {
|
||||
c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort))
|
||||
c.Request().Header.Set("Gotenberg-Webhook-Error-Url", fmt.Sprintf("http://localhost:%d/", webhookPort))
|
||||
}
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
webhook.POST(
|
||||
"/",
|
||||
func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
contentType := c.Request().Header.Get(echo.HeaderContentType)
|
||||
if contentType != tc.expectWebhookContentType {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, tc.expectWebhookContentType, contentType)
|
||||
}
|
||||
|
||||
trace := c.Request().Header.Get(tc.cfg.traceHeader)
|
||||
if trace != "foo" {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, "Gotenberg-Trace", "foo", trace)
|
||||
}
|
||||
|
||||
method := c.Request().Method
|
||||
if method != tc.expectWebhookMethod {
|
||||
t.Errorf("test %d: expected HTTP method '%s' but got '%s'", i, tc.expectWebhookMethod, method)
|
||||
}
|
||||
|
||||
for key, expect := range tc.expectWebhookExtraHTTPHeaders {
|
||||
actual := c.Request().Header.Get(key)
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, key, expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectWebhookContentType == echo.MIMEApplicationJSONCharsetUTF8 {
|
||||
errChan <- nil
|
||||
return nil
|
||||
}
|
||||
|
||||
contentLength := c.Request().Header.Get(echo.HeaderContentLength)
|
||||
if contentLength == "" {
|
||||
t.Errorf("test %d: expected non empty %s", i, echo.HeaderContentLength)
|
||||
}
|
||||
|
||||
contentDisposition := c.Request().Header.Get(echo.HeaderContentDisposition)
|
||||
if !strings.Contains(contentDisposition, tc.expectWebhookFilename) {
|
||||
t.Errorf("test %d: expected %s '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectWebhookFilename)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return nil
|
||||
}
|
||||
|
||||
if body == nil || len(body) == 0 {
|
||||
t.Errorf("test %d: expected non nil body", i)
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
)
|
||||
|
||||
go func(server *echo.Echo, port, i int) {
|
||||
err := server.Start(fmt.Sprintf(":%d", webhookPort))
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}(webhook, webhookPort, i)
|
||||
|
||||
defer func() {
|
||||
err := webhook.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}()
|
||||
|
||||
err := contextMiddleware(tc.cfg)(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
var 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 err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
|
||||
err = <-errChan
|
||||
if err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutMiddleware(t *testing.T) {
|
||||
func TestHardTimeoutMiddleware(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
next echo.HandlerFunc
|
||||
timeout time.Duration
|
||||
@@ -1007,7 +521,7 @@ func TestTimeoutMiddleware(t *testing.T) {
|
||||
c := srv.NewContext(request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
|
||||
err := timeoutMiddleware(tc.timeout)(tc.next)(c)
|
||||
err := hardTimeoutMiddleware(tc.timeout)(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
|
||||
Reference in New Issue
Block a user