package middleware import ( "crypto/tls" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestSecurityHeaders(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) wrapped := SecurityHeaders(handler) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() wrapped.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) // Verify all security headers are set assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options")) assert.Equal(t, "DENY", rec.Header().Get("X-Frame-Options")) assert.Equal(t, "1; mode=block", rec.Header().Get("X-XSS-Protection")) assert.Equal(t, "strict-origin-when-cross-origin", rec.Header().Get("Referrer-Policy")) assert.Equal(t, "geolocation=(), microphone=(), camera=()", rec.Header().Get("Permissions-Policy")) // CSP should NOT be set globally (would break proxied sites) assert.Empty(t, rec.Header().Get("Content-Security-Policy")) // HSTS should NOT be set on plain HTTP assert.Empty(t, rec.Header().Get("Strict-Transport-Security")) } func TestSecurityHeaders_HSTS_WithTLS(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) wrapped := SecurityHeaders(handler) req := httptest.NewRequest(http.MethodGet, "/", nil) req.TLS = &tls.ConnectionState{} // Simulate TLS connection rec := httptest.NewRecorder() wrapped.ServeHTTP(rec, req) assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security")) } func TestSecurityHeaders_HSTS_WithForwardedProto_NotTrusted(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) wrapped := SecurityHeaders(handler) // X-Forwarded-Proto is client-spoofable and should NOT trigger HSTS req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Forwarded-Proto", "https") rec := httptest.NewRecorder() wrapped.ServeHTTP(rec, req) assert.Empty(t, rec.Header().Get("Strict-Transport-Security"), "HSTS should not be set based on spoofable X-Forwarded-Proto") } func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Custom-Header", "custom-value") w.WriteHeader(http.StatusOK) }) wrapped := SecurityHeaders(handler) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() wrapped.ServeHTTP(rec, req) // Security headers should be set assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options")) // Handler's headers should also be present assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) assert.Equal(t, "custom-value", rec.Header().Get("X-Custom-Header")) } func TestSecurityHeaders_CallsNextHandler(t *testing.T) { called := false handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusOK) }) wrapped := SecurityHeaders(handler) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() wrapped.ServeHTTP(rec, req) assert.True(t, called, "next handler should have been called") }