Files
gotenberg/pkg/modules/api/context_test.go

129 lines
3.1 KiB
Go

package api
import (
"bytes"
"context"
"log/slog"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
// Propagate HTTP request context cancellation to processing modules to save resources
// https://github.com/gotenberg/gotenberg/issues/1455
func TestNewContext_Cancellation(t *testing.T) {
e := echo.New()
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
err := writer.Close()
if err != nil {
t.Fatalf("failed to close multipart writer: %v", err)
}
// Create a request with a cancellable context.
reqCtx, cancelReq := context.WithCancel(context.Background())
req := httptest.NewRequest(http.MethodPost, "/", body).WithContext(reqCtx)
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
logger := slog.New(slog.DiscardHandler)
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
timeout := time.Duration(10) * time.Second
downloadFromCfg := downloadFromConfig{
disable: true,
}
ctx, cancel, err := newContext(c, logger, fs, timeout, 0, downloadFromCfg)
if err != nil {
t.Fatalf("expected no error from newContext, got: %v", err)
}
defer cancel()
// Verify initial state: context SHOULD NOT be done yet.
select {
case <-ctx.Done():
t.Fatal("context should not be done immediately")
default:
}
// Simulate Client Disconnect
cancelReq()
// Verify Propagation
select {
case <-ctx.Done():
// Success! The context was cancelled.
if ctx.Err() != context.Canceled {
t.Errorf("expected context error to be 'context.Canceled', got: %v", ctx.Err())
}
case <-time.After(100 * time.Millisecond):
t.Fatal("expected context to be cancelled after request context cancellation, but it timed out")
}
}
func TestSanitizeFilename(t *testing.T) {
for _, tc := range []struct {
scenario string
input string
expect string
}{
{
scenario: "plain filename is unchanged",
input: "report.pdf",
expect: "report.pdf",
},
{
scenario: "POSIX traversal is stripped",
input: "../../etc/passwd",
expect: "passwd",
},
{
scenario: "Windows traversal with backslashes is stripped",
input: `..\..\..\..\Windows\System32\evil.pdf`,
expect: "evil.pdf",
},
{
scenario: "mixed separators take the last segment",
input: `foo/bar\baz.pdf`,
expect: "baz.pdf",
},
{
scenario: "control characters are dropped",
input: "evil\x00\x07\x1f\x7f.pdf",
expect: "evil.pdf",
},
{
scenario: "NFC normalization collapses decomposed sequences",
// "e" + combining acute accent -> precomposed "é".
input: "café.pdf",
expect: "café.pdf",
},
{
scenario: "trailing backslash yields empty name",
input: `foo\`,
expect: "",
},
{
scenario: "empty input yields empty name",
input: "",
expect: "",
},
} {
t.Run(tc.scenario, func(t *testing.T) {
got := sanitizeFilename(tc.input)
if got != tc.expect {
t.Errorf("sanitizeFilename(%q) = %q, want %q", tc.input, got, tc.expect)
}
})
}
}