From 8f42156781527f5d202a84d3824ec936cfe9f422 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 2 Dec 2025 01:44:22 +0000 Subject: [PATCH] test: Add SecurityHeadersWithConfig tests Cover all CSP/X-Frame-Options embedding configurations (57.1% to 100%) --- internal/api/security_test.go | 134 ++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/internal/api/security_test.go b/internal/api/security_test.go index f11d657..376a427 100644 --- a/internal/api/security_test.go +++ b/internal/api/security_test.go @@ -1,7 +1,9 @@ package api import ( + "net/http" "net/http/httptest" + "strings" "sync" "testing" "time" @@ -902,3 +904,135 @@ func TestAuditEvent_Fields(t *testing.T) { t.Errorf("Details = %q, want 'successful login'", ae.Details) } } + +func TestSecurityHeadersWithConfig_EmbeddingDisabled(t *testing.T) { + handler := SecurityHeadersWithConfig( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + false, // allowEmbedding + "", // allowedOrigins + ) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + // Check X-Frame-Options is set to DENY when embedding is disabled + if got := rec.Header().Get("X-Frame-Options"); got != "DENY" { + t.Errorf("X-Frame-Options = %q, want DENY", got) + } + + // Check CSP has frame-ancestors 'none' + csp := rec.Header().Get("Content-Security-Policy") + if !strings.Contains(csp, "frame-ancestors 'none'") { + t.Errorf("CSP should contain frame-ancestors 'none', got: %s", csp) + } + + // Check other security headers are present + if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Errorf("X-Content-Type-Options = %q, want nosniff", got) + } + if got := rec.Header().Get("X-XSS-Protection"); got != "1; mode=block" { + t.Errorf("X-XSS-Protection = %q, want '1; mode=block'", got) + } + if got := rec.Header().Get("Referrer-Policy"); got != "strict-origin-when-cross-origin" { + t.Errorf("Referrer-Policy = %q, want strict-origin-when-cross-origin", got) + } +} + +func TestSecurityHeadersWithConfig_EmbeddingEnabledNoOrigins(t *testing.T) { + handler := SecurityHeadersWithConfig( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + true, // allowEmbedding + "", // allowedOrigins - empty means allow all + ) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + // X-Frame-Options should NOT be set when embedding is allowed + if got := rec.Header().Get("X-Frame-Options"); got != "" { + t.Errorf("X-Frame-Options = %q, want empty (not set)", got) + } + + // Check CSP has frame-ancestors * (allow any) + csp := rec.Header().Get("Content-Security-Policy") + if !strings.Contains(csp, "frame-ancestors *") { + t.Errorf("CSP should contain 'frame-ancestors *', got: %s", csp) + } +} + +func TestSecurityHeadersWithConfig_EmbeddingEnabledWithOrigins(t *testing.T) { + handler := SecurityHeadersWithConfig( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + true, // allowEmbedding + "https://example.com, https://other.com", // allowedOrigins + ) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + // X-Frame-Options should NOT be set when embedding is allowed + if got := rec.Header().Get("X-Frame-Options"); got != "" { + t.Errorf("X-Frame-Options = %q, want empty (not set)", got) + } + + // Check CSP has frame-ancestors with specific origins + csp := rec.Header().Get("Content-Security-Policy") + if !strings.Contains(csp, "frame-ancestors 'self' https://example.com https://other.com") { + t.Errorf("CSP should contain specific frame-ancestors, got: %s", csp) + } +} + +func TestSecurityHeadersWithConfig_EmbeddingWithEmptyOriginEntries(t *testing.T) { + // Test handling of origins with empty entries (e.g., trailing comma) + handler := SecurityHeadersWithConfig( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + true, // allowEmbedding + "https://example.com, , ,", // allowedOrigins with empty entries + ) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + // Check CSP has frame-ancestors with only non-empty origins + csp := rec.Header().Get("Content-Security-Policy") + if !strings.Contains(csp, "frame-ancestors 'self' https://example.com") { + t.Errorf("CSP should contain frame-ancestors with filtered origins, got: %s", csp) + } +} + +func TestSecurityHeadersWithConfig_NextHandlerCalled(t *testing.T) { + called := false + handler := SecurityHeadersWithConfig( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + }), + false, + "", + ) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if !called { + t.Error("next handler was not called") + } +}