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>
547 lines
14 KiB
Go
547 lines
14 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
}
|
|
|
|
// Helper to create a valid JWT token for testing
|
|
func createTestToken(secret string, userID, email, role string, exp time.Time) string {
|
|
claims := UserClaims{
|
|
UserID: userID,
|
|
Email: email,
|
|
Role: role,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(exp),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, _ := token.SignedString([]byte(secret))
|
|
return tokenString
|
|
}
|
|
|
|
// TestCORS tests the CORS middleware
|
|
func TestCORS(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(CORS())
|
|
router.GET("/test", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
origin string
|
|
method string
|
|
expectedStatus int
|
|
expectAllowedOrigin bool
|
|
}{
|
|
{"localhost:3000", "http://localhost:3000", "GET", http.StatusOK, true},
|
|
{"localhost:8000", "http://localhost:8000", "GET", http.StatusOK, true},
|
|
{"production", "https://breakpilot.app", "GET", http.StatusOK, true},
|
|
{"unknown origin", "https://unknown.com", "GET", http.StatusOK, false},
|
|
{"preflight", "http://localhost:3000", "OPTIONS", http.StatusNoContent, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req, _ := http.NewRequest(tt.method, "/test", nil)
|
|
req.Header.Set("Origin", tt.origin)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
|
}
|
|
|
|
allowedOrigin := w.Header().Get("Access-Control-Allow-Origin")
|
|
if tt.expectAllowedOrigin && allowedOrigin != tt.origin {
|
|
t.Errorf("Expected Access-Control-Allow-Origin to be %s, got %s", tt.origin, allowedOrigin)
|
|
}
|
|
if !tt.expectAllowedOrigin && allowedOrigin != "" {
|
|
t.Errorf("Expected no Access-Control-Allow-Origin header, got %s", allowedOrigin)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCORSHeaders tests that CORS headers are set correctly
|
|
func TestCORSHeaders(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(CORS())
|
|
router.GET("/test", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "http://localhost:3000")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
expectedHeaders := map[string]string{
|
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Origin, Content-Type, Authorization, X-Requested-With",
|
|
"Access-Control-Allow-Credentials": "true",
|
|
"Access-Control-Max-Age": "86400",
|
|
}
|
|
|
|
for header, expected := range expectedHeaders {
|
|
actual := w.Header().Get(header)
|
|
if actual != expected {
|
|
t.Errorf("Expected %s to be %s, got %s", header, expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAuthMiddleware_ValidToken tests authentication with valid token
|
|
func TestAuthMiddleware_ValidToken(t *testing.T) {
|
|
secret := "test-secret-key"
|
|
userID := uuid.New().String()
|
|
email := "test@example.com"
|
|
role := "user"
|
|
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware(secret))
|
|
router.GET("/protected", func(c *gin.Context) {
|
|
uid, _ := c.Get("user_id")
|
|
em, _ := c.Get("email")
|
|
r, _ := c.Get("role")
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"user_id": uid,
|
|
"email": em,
|
|
"role": r,
|
|
})
|
|
})
|
|
|
|
token := createTestToken(secret, userID, email, role, time.Now().Add(time.Hour))
|
|
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAuthMiddleware_MissingHeader tests authentication without header
|
|
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware("test-secret"))
|
|
router.GET("/protected", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAuthMiddleware_InvalidFormat tests authentication with invalid header format
|
|
func TestAuthMiddleware_InvalidFormat(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware("test-secret"))
|
|
router.GET("/protected", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
header string
|
|
}{
|
|
{"no Bearer prefix", "some-token"},
|
|
{"Basic auth", "Basic dXNlcjpwYXNz"},
|
|
{"empty Bearer", "Bearer "},
|
|
{"multiple spaces", "Bearer token"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", tt.header)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAuthMiddleware_ExpiredToken tests authentication with expired token
|
|
func TestAuthMiddleware_ExpiredToken(t *testing.T) {
|
|
secret := "test-secret"
|
|
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware(secret))
|
|
router.GET("/protected", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
// Create expired token
|
|
token := createTestToken(secret, "user-123", "test@example.com", "user", time.Now().Add(-time.Hour))
|
|
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAuthMiddleware_WrongSecret tests authentication with wrong secret
|
|
func TestAuthMiddleware_WrongSecret(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware("correct-secret"))
|
|
router.GET("/protected", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
// Create token with different secret
|
|
token := createTestToken("wrong-secret", "user-123", "test@example.com", "user", time.Now().Add(time.Hour))
|
|
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAdminOnly tests the AdminOnly middleware
|
|
func TestAdminOnly(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
role string
|
|
expectedStatus int
|
|
}{
|
|
{"admin allowed", "admin", http.StatusOK},
|
|
{"super_admin allowed", "super_admin", http.StatusOK},
|
|
{"dpo allowed", "data_protection_officer", http.StatusOK},
|
|
{"user forbidden", "user", http.StatusForbidden},
|
|
{"empty role forbidden", "", http.StatusForbidden},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", tt.role)
|
|
c.Next()
|
|
})
|
|
router.Use(AdminOnly())
|
|
router.GET("/admin", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/admin", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAdminOnly_NoRole tests AdminOnly when role is not set
|
|
func TestAdminOnly_NoRole(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(AdminOnly())
|
|
router.GET("/admin", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/admin", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestDSBOnly tests the DSBOnly middleware
|
|
func TestDSBOnly(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
role string
|
|
expectedStatus int
|
|
}{
|
|
{"dpo allowed", "data_protection_officer", http.StatusOK},
|
|
{"super_admin allowed", "super_admin", http.StatusOK},
|
|
{"admin forbidden", "admin", http.StatusForbidden},
|
|
{"user forbidden", "user", http.StatusForbidden},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", tt.role)
|
|
c.Next()
|
|
})
|
|
router.Use(DSBOnly())
|
|
router.GET("/dsb", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/dsb", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsAdmin tests the IsAdmin helper function
|
|
func TestIsAdmin(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
role string
|
|
expected bool
|
|
}{
|
|
{"admin", "admin", true},
|
|
{"super_admin", "super_admin", true},
|
|
{"dpo", "data_protection_officer", true},
|
|
{"user", "user", false},
|
|
{"empty", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
if tt.role != "" {
|
|
c.Set("role", tt.role)
|
|
}
|
|
|
|
result := IsAdmin(c)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected IsAdmin to be %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsDSB tests the IsDSB helper function
|
|
func TestIsDSB(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
role string
|
|
expected bool
|
|
}{
|
|
{"dpo", "data_protection_officer", true},
|
|
{"super_admin", "super_admin", true},
|
|
{"admin", "admin", false},
|
|
{"user", "user", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("role", tt.role)
|
|
|
|
result := IsDSB(c)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected IsDSB to be %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetUserID tests the GetUserID helper function
|
|
func TestGetUserID(t *testing.T) {
|
|
validUUID := uuid.New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
userID string
|
|
setUserID bool
|
|
expectError bool
|
|
expectedID uuid.UUID
|
|
}{
|
|
{"valid UUID", validUUID.String(), true, false, validUUID},
|
|
{"invalid UUID", "not-a-uuid", true, true, uuid.Nil},
|
|
{"missing user_id", "", false, false, uuid.Nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
if tt.setUserID {
|
|
c.Set("user_id", tt.userID)
|
|
}
|
|
|
|
result, err := GetUserID(c)
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
|
|
if !tt.expectError && result != tt.expectedID {
|
|
t.Errorf("Expected %v, got %v", tt.expectedID, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetClientIP tests the GetClientIP helper function
|
|
func TestGetClientIP(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
xff string
|
|
xri string
|
|
clientIP string
|
|
expectedIP string
|
|
}{
|
|
{"X-Forwarded-For", "10.0.0.1", "", "192.168.1.1", "10.0.0.1"},
|
|
{"X-Forwarded-For multiple", "10.0.0.1, 10.0.0.2", "", "192.168.1.1", "10.0.0.1"},
|
|
{"X-Real-IP", "", "10.0.0.1", "192.168.1.1", "10.0.0.1"},
|
|
{"direct", "", "", "192.168.1.1", "192.168.1.1"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
c.Request, _ = http.NewRequest("GET", "/", nil)
|
|
if tt.xff != "" {
|
|
c.Request.Header.Set("X-Forwarded-For", tt.xff)
|
|
}
|
|
if tt.xri != "" {
|
|
c.Request.Header.Set("X-Real-IP", tt.xri)
|
|
}
|
|
c.Request.RemoteAddr = tt.clientIP + ":12345"
|
|
|
|
result := GetClientIP(c)
|
|
|
|
// Note: gin.ClientIP() might return different values
|
|
// depending on trusted proxies config
|
|
if result != tt.expectedIP && result != tt.clientIP {
|
|
t.Logf("Note: GetClientIP returned %s (expected %s or %s)", result, tt.expectedIP, tt.clientIP)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetUserAgent tests the GetUserAgent helper function
|
|
func TestGetUserAgent(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request, _ = http.NewRequest("GET", "/", nil)
|
|
|
|
expectedUA := "Mozilla/5.0 (Test)"
|
|
c.Request.Header.Set("User-Agent", expectedUA)
|
|
|
|
result := GetUserAgent(c)
|
|
if result != expectedUA {
|
|
t.Errorf("Expected %s, got %s", expectedUA, result)
|
|
}
|
|
}
|
|
|
|
// TestIsSuspended tests the IsSuspended helper function
|
|
func TestIsSuspended(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
suspended interface{}
|
|
setSuspended bool
|
|
expected bool
|
|
}{
|
|
{"suspended true", true, true, true},
|
|
{"suspended false", false, true, false},
|
|
{"not set", nil, false, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
if tt.setSuspended {
|
|
c.Set("account_suspended", tt.suspended)
|
|
}
|
|
|
|
result := IsSuspended(c)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkCORS benchmarks the CORS middleware
|
|
func BenchmarkCORS(b *testing.B) {
|
|
router := gin.New()
|
|
router.Use(CORS())
|
|
router.GET("/test", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "http://localhost:3000")
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkAuthMiddleware benchmarks the auth middleware
|
|
func BenchmarkAuthMiddleware(b *testing.B) {
|
|
secret := "test-secret-key"
|
|
token := createTestToken(secret, uuid.New().String(), "test@example.com", "user", time.Now().Add(time.Hour))
|
|
|
|
router := gin.New()
|
|
router.Use(AuthMiddleware(secret))
|
|
router.GET("/test", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
})
|
|
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
}
|