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>
This commit is contained in:
377
consent-service/internal/middleware/security_headers_test.go
Normal file
377
consent-service/internal/middleware/security_headers_test.go
Normal file
@@ -0,0 +1,377 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user