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
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +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)
}
}
}