check / check (push) Successful in 11s
The comment was not a readable sentence; it is now one plain sentence saying what the test checks. 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
|
|
// answers with access_control_allow_origin, where "*" lets any origin read
|
|
// responses and a single origin lets only that origin read them.
|
|
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")
|
|
}
|
|
}
|