Failing tests for access_control_allow_origin, upstream_fetch_timeout, upstream_max_response_size and downstream_timeout: their defaults, valid values from the file and the environment, invalid values aborting startup naming the key or variable and the value, the CORS middleware answering with the configured origin, and the server's write timeout coming from downstream_timeout. They do not compile until the settings exist. Model: opus-5-5
198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
package middleware
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/config"
|
|
)
|
|
|
|
// TestCORSAnswersWithConfiguredOrigin checks that the CORS middleware
|
|
// uses access_control_allow_origin: "*" lets any origin read responses,
|
|
// and a single origin lets that origin read them and no other.
|
|
func TestCORSAnswersWithConfiguredOrigin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const appOrigin = "https://app.example.com"
|
|
|
|
cases := []struct {
|
|
configured string
|
|
requestOrigin string
|
|
want string
|
|
}{
|
|
{"*", "https://any.example.com", "*"},
|
|
{appOrigin, appOrigin, appOrigin},
|
|
{appOrigin, "https://other.example.com", ""},
|
|
}
|
|
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
for _, tc := range cases {
|
|
mw := &Middleware{
|
|
log: slog.Default(),
|
|
config: &config.Config{AccessControlAllowOrigin: tc.configured},
|
|
}
|
|
|
|
handler := mw.CORS()(testHandler)
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, "/v1/image/example.com/a.jpg/1x1.png", nil)
|
|
req.Header.Set("Origin", tc.requestOrigin)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
got := rec.Header().Get("Access-Control-Allow-Origin")
|
|
if got != tc.want {
|
|
t.Errorf("configured %q, request from %q: "+
|
|
"Access-Control-Allow-Origin = %q, want %q",
|
|
tc.configured, tc.requestOrigin, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSecurityHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create middleware instance
|
|
cfg := &config.Config{}
|
|
mw := &Middleware{
|
|
log: slog.Default(),
|
|
config: cfg,
|
|
}
|
|
|
|
// Create a test handler
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Wrap with security headers middleware
|
|
handler := mw.SecurityHeaders()(testHandler)
|
|
|
|
// Make a test request
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Check security headers
|
|
tests := []struct {
|
|
header string
|
|
want string
|
|
}{
|
|
{"X-Content-Type-Options", "nosniff"},
|
|
{"X-Frame-Options", "DENY"},
|
|
{"Referrer-Policy", "strict-origin-when-cross-origin"},
|
|
{"X-XSS-Protection", "0"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.header, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := rec.Header().Get(tt.header)
|
|
if got != tt.want {
|
|
t.Errorf("%s = %q, want %q", tt.header, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecurityHeaders_PolicyHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := &config.Config{}
|
|
mw := &Middleware{
|
|
log: slog.Default(),
|
|
config: cfg,
|
|
}
|
|
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := mw.SecurityHeaders()(testHandler)
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
tests := []struct {
|
|
header string
|
|
want string
|
|
}{
|
|
{"Strict-Transport-Security", "max-age=31536000; includeSubDomains"},
|
|
{
|
|
"Content-Security-Policy",
|
|
"default-src 'self'; " +
|
|
"script-src 'self' 'unsafe-inline'; " +
|
|
"style-src 'self' 'unsafe-inline'; " +
|
|
"object-src 'none'; " +
|
|
"base-uri 'self'; " +
|
|
"form-action 'self'; " +
|
|
"frame-ancestors 'none'",
|
|
},
|
|
{
|
|
"Permissions-Policy",
|
|
"accelerometer=(), autoplay=(), camera=(), " +
|
|
"display-capture=(), geolocation=(), gyroscope=(), " +
|
|
"magnetometer=(), microphone=(), payment=(), usb=()",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.header, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := rec.Header().Get(tt.header)
|
|
if got != tt.want {
|
|
t.Errorf("%s = %q, want %q", tt.header, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := &config.Config{}
|
|
mw := &Middleware{
|
|
log: slog.Default(),
|
|
config: cfg,
|
|
}
|
|
|
|
// Handler that sets its own headers
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("X-Custom-Header", "custom-value")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := mw.SecurityHeaders()(testHandler)
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Security headers should be present
|
|
if rec.Header().Get("X-Content-Type-Options") != "nosniff" {
|
|
t.Error("X-Content-Type-Options not set")
|
|
}
|
|
|
|
// Custom headers should still be there
|
|
if rec.Header().Get("X-Custom-Header") != "custom-value" {
|
|
t.Error("Custom header was overwritten")
|
|
}
|
|
|
|
if rec.Header().Get("Content-Type") != "application/json" {
|
|
t.Error("Content-Type was overwritten")
|
|
}
|
|
}
|