test: add comprehensive unit tests — coverage 20.2% → 91.8%
Add 12 test files covering all internal packages: - internal/util/util_test.go (97.2%) - internal/model/model_test.go (100.0%) - internal/config/config_test.go (88.1%) - internal/template/builtin_test.go (98.8%) - internal/middleware/middleware_test.go (98.7%) - internal/database/repo_extra_test.go (85.5%) - internal/rules/converter_test.go (99.1%) - internal/service/subscription_test.go (75.7%) - internal/handler/handler_test.go (90.6%) - internal/filter/filter_extra_test.go (91.9%) - internal/proxy/client_parser_test.go (96.1%) - internal/render/render_extra_test.go (99.2%) Overall: 91.8% (4033/4400 statements) — exceeds 85% acceptance threshold. All tests pass, go vet clean, go build clean.
This commit is contained in:
@@ -0,0 +1,828 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// newTestApp creates a fiber app, registers the given middleware via Use,
|
||||
// then a final GET handler that returns 200 "ok".
|
||||
// The last argument is the final route handler; preceding args are middleware.
|
||||
func newTestApp(t *testing.T, handlers ...fiber.Handler) *fiber.App {
|
||||
t.Helper()
|
||||
app := fiber.New()
|
||||
mws := handlers
|
||||
if len(mws) > 0 {
|
||||
middlewares := make([]any, len(mws))
|
||||
for i, h := range mws {
|
||||
middlewares[i] = h
|
||||
}
|
||||
app.Use(middlewares...)
|
||||
}
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
// newTestAppWithHandler creates a fiber app with middleware and a custom final route handler.
|
||||
// The last argument is the final route handler; preceding args are middleware.
|
||||
func newTestAppWithHandler(t *testing.T, handlers ...fiber.Handler) *fiber.App {
|
||||
t.Helper()
|
||||
app := fiber.New()
|
||||
if len(handlers) == 0 {
|
||||
app.Get("/test", func(c fiber.Ctx) error { return c.SendString("ok") })
|
||||
return app
|
||||
}
|
||||
mws := handlers[:len(handlers)-1]
|
||||
final := handlers[len(handlers)-1]
|
||||
if len(mws) > 0 {
|
||||
middlewares := make([]any, len(mws))
|
||||
for i, h := range mws {
|
||||
middlewares[i] = h
|
||||
}
|
||||
app.Use(middlewares...)
|
||||
}
|
||||
app.Get("/test", final)
|
||||
return app
|
||||
}
|
||||
|
||||
// handlersToAny converts []fiber.Handler to []any for variadic registration.
|
||||
func handlersToAny(hs []fiber.Handler) []any {
|
||||
out := make([]any, len(hs))
|
||||
for i, h := range hs {
|
||||
out[i] = h
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------- auth.go ----------
|
||||
|
||||
func TestExtractToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setReq func(*http.Request)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Bearer header",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Bearer my-token-123")
|
||||
},
|
||||
want: "my-token-123",
|
||||
},
|
||||
{
|
||||
name: "bearer lowercase",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "bearer lower-token")
|
||||
},
|
||||
want: "lower-token",
|
||||
},
|
||||
{
|
||||
name: "Bearer with extra spaces",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Bearer spaced-token ")
|
||||
},
|
||||
want: "spaced-token",
|
||||
},
|
||||
{
|
||||
name: "query param token",
|
||||
setReq: func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
q.Set("token", "query-token")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
},
|
||||
want: "query-token",
|
||||
},
|
||||
{
|
||||
name: "x-sub-store-token header",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("X-Sub-Store-Token", "header-token")
|
||||
},
|
||||
want: "header-token",
|
||||
},
|
||||
{
|
||||
name: "Bearer takes priority over query",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Bearer bearer-wins")
|
||||
q := r.URL.Query()
|
||||
q.Set("token", "query-loses")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
},
|
||||
want: "bearer-wins",
|
||||
},
|
||||
{
|
||||
name: "query takes priority over x-sub-store-token",
|
||||
setReq: func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
q.Set("token", "query-wins")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
r.Header.Set("X-Sub-Store-Token", "header-loses")
|
||||
},
|
||||
want: "query-wins",
|
||||
},
|
||||
{
|
||||
name: "no token anywhere",
|
||||
setReq: func(r *http.Request) {},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Authorization without Bearer prefix",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Basic abc123")
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty Bearer",
|
||||
setReq: func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Bearer ")
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
app := fiber.New()
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
got = ExtractToken(c)
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
tt.setReq(req)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("ExtractToken() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin(t *testing.T) {
|
||||
adminToken := "secret-admin-token"
|
||||
|
||||
t.Run("valid token passes", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, RequireAdmin(adminToken), func(c fiber.Ctx) error {
|
||||
return c.SendString("protected")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "protected" {
|
||||
t.Errorf("body = %q, want protected", string(body))
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("invalid token returns 401", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, RequireAdmin(adminToken), func(c fiber.Ctx) error {
|
||||
return c.SendString("protected")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer wrong-token")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Errorf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "invalid") && !strings.Contains(string(body), "failed") {
|
||||
t.Errorf("body should mention invalid, got: %s", string(body))
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("no token returns 401", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, RequireAdmin(adminToken), func(c fiber.Ctx) error {
|
||||
return c.SendString("protected")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 401 {
|
||||
t.Errorf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("token via query param", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, RequireAdmin(adminToken), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test?token="+adminToken, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("token via x-sub-store-token header", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, RequireAdmin(adminToken), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("X-Sub-Store-Token", adminToken)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- cors.go ----------
|
||||
|
||||
func TestCORS(t *testing.T) {
|
||||
t.Run("allowed origin wildcard", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, CORS("*"), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Errorf("ACAO = %q, want https://example.com", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got == "" {
|
||||
t.Error("Access-Control-Allow-Methods should be set")
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); !strings.Contains(got, "Authorization") {
|
||||
t.Errorf("Access-Control-Allow-Headers should contain Authorization, got %q", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("allowed origin specific", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, CORS("https://allowed.com, https://also.com"), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Origin", "https://allowed.com")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://allowed.com" {
|
||||
t.Errorf("ACAO = %q, want https://allowed.com", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("disallowed origin", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, CORS("https://allowed.com"), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Origin", "https://evil.com")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("ACAO = %q, want empty (disallowed)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("no origin header", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, CORS("*"), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("ACAO = %q, want empty (no origin)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("empty allowed origins", func(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, CORS(""), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("ACAO = %q, want empty (no origins configured)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleOptions(t *testing.T) {
|
||||
t.Run("OPTIONS preflight returns 204", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(HandleOptions())
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("OPTIONS", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 204 {
|
||||
t.Errorf("status = %d, want 204", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got == "" {
|
||||
t.Error("Access-Control-Allow-Methods should be set for OPTIONS")
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); !strings.Contains(got, "Authorization") {
|
||||
t.Errorf("Access-Control-Allow-Headers should contain Authorization, got %q", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("GET passes through", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(HandleOptions())
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseOrigins(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"single", "https://example.com", []string{"https://example.com"}},
|
||||
{"multiple", "https://a.com, https://b.com", []string{"https://a.com", "https://b.com"}},
|
||||
{"with spaces", " https://a.com , https://b.com ", []string{"https://a.com", "https://b.com"}},
|
||||
{"empty", "", []string{}},
|
||||
{"only commas", ",,,", []string{}},
|
||||
{"wildcard", "*", []string{"*"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseOrigins(tt.input)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("parseOrigins(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("parseOrigins(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsHelper(t *testing.T) {
|
||||
list := []string{"a", "b", "c"}
|
||||
if !contains(list, "a") {
|
||||
t.Error("contains should find 'a'")
|
||||
}
|
||||
if contains(list, "z") {
|
||||
t.Error("contains should not find 'z'")
|
||||
}
|
||||
if contains([]string{}, "a") {
|
||||
t.Error("contains on empty list should be false")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- security.go ----------
|
||||
|
||||
func TestSecurityHeaders(t *testing.T) {
|
||||
app := newTestAppWithHandler(t, SecurityHeaders(), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
csp := resp.Header.Get("Content-Security-Policy")
|
||||
if csp == "" {
|
||||
t.Fatal("Content-Security-Policy header should be set")
|
||||
}
|
||||
// Per review-resolution #27: CSP must NOT contain unsafe-eval
|
||||
if strings.Contains(csp, "unsafe-eval") {
|
||||
t.Errorf("CSP should not contain unsafe-eval: %s", csp)
|
||||
}
|
||||
// Should contain script-src 'self'
|
||||
if !strings.Contains(csp, "script-src 'self'") {
|
||||
t.Errorf("CSP should contain script-src 'self': %s", csp)
|
||||
}
|
||||
// Check other security headers
|
||||
if got := resp.Header.Get("Referrer-Policy"); got != "no-referrer" {
|
||||
t.Errorf("Referrer-Policy = %q, want no-referrer", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Content-Type-Options"); got != "nosniff" {
|
||||
t.Errorf("X-Content-Type-Options = %q, want nosniff", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Frame-Options"); got != "DENY" {
|
||||
t.Errorf("X-Frame-Options = %q, want DENY", got)
|
||||
}
|
||||
pp := resp.Header.Get("Permissions-Policy")
|
||||
if pp == "" {
|
||||
t.Error("Permissions-Policy should be set")
|
||||
}
|
||||
if !strings.Contains(pp, "camera=()") {
|
||||
t.Errorf("Permissions-Policy should contain camera=(): %s", pp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit(t *testing.T) {
|
||||
t.Run("within limit", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Post("/test", BodyLimit(100), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", strings.NewReader("small body"))
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("exceeds limit", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Post("/test", BodyLimit(10), func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", strings.NewReader(strings.Repeat("x", 100)))
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 413 {
|
||||
t.Errorf("status = %d, want 413", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "too large") {
|
||||
t.Errorf("body should mention too large: %s", string(body))
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloadHostIsolation(t *testing.T) {
|
||||
t.Run("no download hosts configured - all allowed", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(DownloadHostIsolation(nil))
|
||||
app.Get("/api/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.Host = "anyhost.com"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("download host serves /download/ path", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(DownloadHostIsolation([]string{"dl.example.com"}))
|
||||
app.Get("/download/sub", func(c fiber.Ctx) error {
|
||||
return c.SendString("download")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/download/sub", nil)
|
||||
req.Host = "dl.example.com"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("download host blocked on non-download path", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(DownloadHostIsolation([]string{"dl.example.com"}))
|
||||
app.Get("/api/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("api")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.Host = "dl.example.com"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 404 {
|
||||
t.Errorf("status = %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("non-download host allowed on any path", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(DownloadHostIsolation([]string{"dl.example.com"}))
|
||||
app.Get("/api/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("api")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.Host = "api.example.com"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("download host case-insensitive", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(DownloadHostIsolation([]string{"DL.Example.COM"}))
|
||||
app.Get("/api/test", func(c fiber.Ctx) error {
|
||||
return c.SendString("api")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.Host = "dl.example.com"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 404 {
|
||||
t.Errorf("status = %d, want 404 (case-insensitive match)", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetSafeResponseHeader(t *testing.T) {
|
||||
t.Run("valid value sets header", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
SetSafeResponseHeader(c, "X-Custom", "safe-value")
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("X-Custom"); got != "safe-value" {
|
||||
t.Errorf("X-Custom = %q, want safe-value", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("empty value does not set header", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
SetSafeResponseHeader(c, "X-Custom", "")
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("X-Custom"); got != "" {
|
||||
t.Errorf("X-Custom = %q, want empty (not set)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("CRLF injection blocked", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
SetSafeResponseHeader(c, "X-Custom", "safe\r\nX-Injected: evil")
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("X-Custom"); got != "" {
|
||||
t.Errorf("X-Custom = %q, want empty (CRLF blocked)", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Injected"); got != "" {
|
||||
t.Errorf("X-Injected = %q, want empty (injection blocked)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("LF only blocked", func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/test", func(c fiber.Ctx) error {
|
||||
SetSafeResponseHeader(c, "X-Custom", "safe\nX-Injected: evil")
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
if got := resp.Header.Get("X-Custom"); got != "" {
|
||||
t.Errorf("X-Custom = %q, want empty (LF blocked)", got)
|
||||
}
|
||||
resp.Body.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestSafeContentDisposition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple filename",
|
||||
input: `attachment; filename="test.txt"`,
|
||||
want: `attachment; filename="test.txt"`,
|
||||
},
|
||||
{
|
||||
name: "filename without quotes",
|
||||
input: `attachment; filename=test.txt`,
|
||||
want: `attachment; filename="test.txt"`,
|
||||
},
|
||||
{
|
||||
name: "UTF-8 encoded filename",
|
||||
input: `attachment; filename*=UTF-8''test%20file.txt`,
|
||||
want: `attachment; filename="test_20file.txt"`,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "CRLF injection attempt",
|
||||
input: "attachment; filename=\"test\r\nX-Injected: evil\"",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "no filename",
|
||||
input: "attachment",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "special chars sanitized",
|
||||
input: `attachment; filename="test@#$%.txt"`,
|
||||
want: `attachment; filename="test____.txt"`,
|
||||
},
|
||||
{
|
||||
name: "CJK preserved",
|
||||
input: `attachment; filename="节点.txt"`,
|
||||
want: `attachment; filename="节点.txt"`,
|
||||
},
|
||||
{
|
||||
name: "long filename truncated",
|
||||
input: `attachment; filename="` + strings.Repeat("a", 200) + `.txt"`,
|
||||
want: `attachment; filename="` + strings.Repeat("a", 120) + `"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := SafeContentDisposition(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("SafeContentDisposition(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeContentDispositionEmptyFilename(t *testing.T) {
|
||||
// filename with only special chars → sanitized to underscores (non-empty) → returns valid
|
||||
got := SafeContentDisposition(`attachment; filename="!!!"`)
|
||||
want := `attachment; filename="___"`
|
||||
if got != want {
|
||||
t.Errorf("SafeContentDisposition with only-special filename = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{`attachment; filename="test.txt"`, "test.txt"},
|
||||
{`attachment; filename=test.txt`, "test.txt"},
|
||||
{`attachment; filename*=UTF-8''test.txt`, "test.txt"},
|
||||
{`attachment`, ""},
|
||||
{``, ""},
|
||||
{`filename="only"`, "only"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := extractFilename(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractFilename(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"simple.txt", "simple.txt"},
|
||||
{"test@file.txt", "test_file.txt"},
|
||||
{"节点.txt", "节点.txt"}, // CJK preserved
|
||||
{"a b c.txt", "a b c.txt"}, // spaces preserved
|
||||
{"test(1).txt", "test(1).txt"}, // parens preserved
|
||||
{"test-1.txt", "test-1.txt"}, // hyphens preserved
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := sanitizeFilename(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// truncation
|
||||
long := strings.Repeat("a", 200)
|
||||
got := sanitizeFilename(long)
|
||||
if len(got) > 120 {
|
||||
t.Errorf("sanitizeFilename long input length = %d, want <= 120", len(got))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user