This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/middleware/security_headers_test.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

378 lines
9.7 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestSecurityHeaders_AddsBasicHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = true // Skip HSTS and cross-origin headers
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Check basic security headers
tests := []struct {
header string
expected string
}{
{"X-Content-Type-Options", "nosniff"},
{"X-Frame-Options", "DENY"},
{"X-XSS-Protection", "1; mode=block"},
{"Referrer-Policy", "strict-origin-when-cross-origin"},
}
for _, tt := range tests {
value := w.Header().Get(tt.header)
if value != tt.expected {
t.Errorf("Header %s: expected %q, got %q", tt.header, tt.expected, value)
}
}
}
func TestSecurityHeaders_HSTSNotAddedInDevelopment(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = true
config.HSTSEnabled = true
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
hstsHeader := w.Header().Get("Strict-Transport-Security")
if hstsHeader != "" {
t.Errorf("HSTS should not be set in development mode, got: %s", hstsHeader)
}
}
func TestSecurityHeaders_HSTSAddedInProduction(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = false
config.HSTSEnabled = true
config.HSTSMaxAge = 31536000
config.HSTSIncludeSubdomains = true
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
hstsHeader := w.Header().Get("Strict-Transport-Security")
if hstsHeader == "" {
t.Error("HSTS should be set in production mode")
}
// Check that it contains max-age
if hstsHeader != "max-age=31536000; includeSubDomains" {
t.Errorf("Unexpected HSTS value: %s", hstsHeader)
}
}
func TestSecurityHeaders_HSTSWithPreload(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = false
config.HSTSEnabled = true
config.HSTSMaxAge = 31536000
config.HSTSIncludeSubdomains = true
config.HSTSPreload = true
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
hstsHeader := w.Header().Get("Strict-Transport-Security")
expected := "max-age=31536000; includeSubDomains; preload"
if hstsHeader != expected {
t.Errorf("Expected HSTS %q, got %q", expected, hstsHeader)
}
}
func TestSecurityHeaders_CSPHeader(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.CSPEnabled = true
config.CSPPolicy = "default-src 'self'"
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
cspHeader := w.Header().Get("Content-Security-Policy")
if cspHeader != "default-src 'self'" {
t.Errorf("Expected CSP %q, got %q", "default-src 'self'", cspHeader)
}
}
func TestSecurityHeaders_NoCSPWhenDisabled(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.CSPEnabled = false
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
cspHeader := w.Header().Get("Content-Security-Policy")
if cspHeader != "" {
t.Errorf("CSP should not be set when disabled, got: %s", cspHeader)
}
}
func TestSecurityHeaders_ExcludedPaths(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.ExcludedPaths = []string{"/health", "/metrics"}
router.Use(SecurityHeadersWithConfig(config))
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
})
router.GET("/api", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// Test excluded path
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Header().Get("X-Content-Type-Options") != "" {
t.Error("Security headers should not be set for excluded paths")
}
// Test non-excluded path
req = httptest.NewRequest(http.MethodGet, "/api", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Error("Security headers should be set for non-excluded paths")
}
}
func TestSecurityHeaders_CrossOriginInProduction(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = false
config.CrossOriginOpenerPolicy = "same-origin"
config.CrossOriginResourcePolicy = "same-origin"
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
coopHeader := w.Header().Get("Cross-Origin-Opener-Policy")
if coopHeader != "same-origin" {
t.Errorf("Expected COOP %q, got %q", "same-origin", coopHeader)
}
corpHeader := w.Header().Get("Cross-Origin-Resource-Policy")
if corpHeader != "same-origin" {
t.Errorf("Expected CORP %q, got %q", "same-origin", corpHeader)
}
}
func TestSecurityHeaders_NoCrossOriginInDevelopment(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.DevelopmentMode = true
config.CrossOriginOpenerPolicy = "same-origin"
config.CrossOriginResourcePolicy = "same-origin"
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Header().Get("Cross-Origin-Opener-Policy") != "" {
t.Error("COOP should not be set in development mode")
}
if w.Header().Get("Cross-Origin-Resource-Policy") != "" {
t.Error("CORP should not be set in development mode")
}
}
func TestSecurityHeaders_PermissionsPolicy(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
config := DefaultSecurityHeadersConfig()
config.PermissionsPolicy = "geolocation=(), microphone=()"
router.Use(SecurityHeadersWithConfig(config))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
ppHeader := w.Header().Get("Permissions-Policy")
if ppHeader != "geolocation=(), microphone=()" {
t.Errorf("Expected Permissions-Policy %q, got %q", "geolocation=(), microphone=()", ppHeader)
}
}
func TestSecurityHeaders_DefaultMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
// Use the default middleware function
router.Use(SecurityHeaders())
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should at least have the basic headers
if w.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Error("Default middleware should set X-Content-Type-Options")
}
}
func TestBuildHSTSHeader(t *testing.T) {
tests := []struct {
name string
config SecurityHeadersConfig
expected string
}{
{
name: "basic HSTS",
config: SecurityHeadersConfig{
HSTSMaxAge: 31536000,
HSTSIncludeSubdomains: false,
HSTSPreload: false,
},
expected: "max-age=31536000",
},
{
name: "HSTS with subdomains",
config: SecurityHeadersConfig{
HSTSMaxAge: 31536000,
HSTSIncludeSubdomains: true,
HSTSPreload: false,
},
expected: "max-age=31536000; includeSubDomains",
},
{
name: "HSTS with preload",
config: SecurityHeadersConfig{
HSTSMaxAge: 31536000,
HSTSIncludeSubdomains: true,
HSTSPreload: true,
},
expected: "max-age=31536000; includeSubDomains; preload",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.buildHSTSHeader()
if result != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, result)
}
})
}
}
func TestIsExcludedPath(t *testing.T) {
config := SecurityHeadersConfig{
ExcludedPaths: []string{"/health", "/metrics", "/api/v1/health"},
}
tests := []struct {
path string
excluded bool
}{
{"/health", true},
{"/metrics", true},
{"/api/v1/health", true},
{"/api", false},
{"/health/check", false},
{"/", false},
}
for _, tt := range tests {
result := config.isExcludedPath(tt.path)
if result != tt.excluded {
t.Errorf("Path %s: expected excluded=%v, got %v", tt.path, tt.excluded, result)
}
}
}