diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index a4bc087..4c9de94 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -133,3 +133,13 @@ func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler { }, ) } + +// SecurityHeaders returns a middleware that adds security headers to responses. +func (s *Middleware) SecurityHeaders() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO: implement security headers + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go new file mode 100644 index 0000000..72337ac --- /dev/null +++ b/internal/middleware/middleware_test.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "sneak.berlin/go/pixa/internal/config" +) + +func TestSecurityHeaders(t *testing.T) { + // 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.NewRequest(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) { + 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) { + 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.NewRequest(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") + } +}