package handlers import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/services" ) func TestIsTransientSQLiteError(t *testing.T) { t.Parallel() tests := []struct { name string err error want bool }{ {name: "nil", err: nil, want: false}, {name: "locked", err: errors.New("database is locked"), want: true}, {name: "busy", err: errors.New("database is busy"), want: true}, {name: "table locked", err: errors.New("database table is locked"), want: true}, {name: "mixed case", err: errors.New("DataBase Is Locked"), want: true}, {name: "non transient", err: errors.New("constraint failed"), want: false}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { require.Equal(t, testCase.want, isTransientSQLiteError(testCase.err)) }) } } func TestUpsertSettingWithRetry_ReturnsErrorForClosedDB(t *testing.T) { db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) stdDB, err := db.DB() require.NoError(t, err) require.NoError(t, stdDB.Close()) setting := &models.Setting{ Key: "security.test.closed_db", Value: "false", Category: "security", Type: "bool", } err = handler.upsertSettingWithRetry(setting) require.Error(t, err) } func jsonReader(data interface{}) io.Reader { b, _ := json.Marshal(data) return bytes.NewReader(b) } func setupEmergencyTestDB(t *testing.T) *gorm.DB { dsn := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate( &models.Setting{}, &models.SecurityConfig{}, &models.SecurityAudit{}, &models.SecurityDecision{}, &models.EmergencyToken{}, ) require.NoError(t, err) return db } func setupEmergencyRouter(handler *EmergencyHandler) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() _ = router.SetTrustedProxies(nil) router.POST("/api/v1/emergency/security-reset", handler.SecurityReset) return router } type mockCaddyManager struct { calls int } func (m *mockCaddyManager) ApplyConfig(_ context.Context) error { m.calls++ return nil } type mockCacheInvalidator struct { calls int } func (m *mockCacheInvalidator) InvalidateCache() { m.calls++ } func TestEmergencySecurityReset_Success(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) // Configure valid token validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" _ = os.Setenv(EmergencyTokenEnvVar, validToken) defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }() // Create initial security config to verify it gets disabled secConfig := models.SecurityConfig{ Name: "default", Enabled: true, WAFMode: "enabled", RateLimitMode: "enabled", RateLimitEnable: true, CrowdSecMode: "local", } require.NoError(t, db.Create(&secConfig).Error) // Make request with valid token req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, validToken) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert response assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.True(t, response["success"].(bool)) assert.NotNil(t, response["disabled_modules"]) disabledModules := response["disabled_modules"].([]interface{}) assert.GreaterOrEqual(t, len(disabledModules), 5) // Verify settings were updated // Note: feature.cerberus.enabled is intentionally NOT disabled // The emergency reset only disables individual security modules (ACL, WAF, etc) // while keeping the Cerberus framework enabled for break glass testing // Verify ACL module is disabled var aclSetting models.Setting err = db.Where("key = ?", "security.acl.enabled").First(&aclSetting).Error require.NoError(t, err) assert.Equal(t, "false", aclSetting.Value) // Verify CrowdSec mode is disabled var crowdsecMode models.Setting err = db.Where("key = ?", "security.crowdsec.mode").First(&crowdsecMode).Error require.NoError(t, err) assert.Equal(t, "disabled", crowdsecMode.Value) // Verify admin whitelist is cleared var adminWhitelist models.Setting err = db.Where("key = ?", "security.admin_whitelist").First(&adminWhitelist).Error require.NoError(t, err) assert.Equal(t, "", adminWhitelist.Value) // Verify SecurityConfig was updated var updatedConfig models.SecurityConfig err = db.Where("name = ?", "default").First(&updatedConfig).Error require.NoError(t, err) assert.False(t, updatedConfig.Enabled) assert.Equal(t, "disabled", updatedConfig.WAFMode) assert.Equal(t, "", updatedConfig.AdminWhitelist) // Note: Audit logging is async via SecurityService channel, tested separately } func TestEmergencySecurityReset_InvalidToken(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) // Configure valid token validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" _ = os.Setenv(EmergencyTokenEnvVar, validToken) defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }() // Make request with invalid token req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, "wrong-token") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert response assert.Equal(t, http.StatusUnauthorized, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "unauthorized", response["error"]) // Note: Audit logging is async via SecurityService channel, tested separately } func TestEmergencySecurityReset_MissingToken(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) // Configure valid token validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" _ = os.Setenv(EmergencyTokenEnvVar, validToken) defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }() // Make request without token header req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert response assert.Equal(t, http.StatusUnauthorized, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "unauthorized", response["error"]) assert.Contains(t, response["message"], "required") // Note: Audit logging is async via SecurityService channel, tested separately } func TestEmergencySecurityReset_NotConfigured(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) // Ensure token is not configured _ = os.Unsetenv(EmergencyTokenEnvVar) // Make request req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, "any-token") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert response assert.Equal(t, http.StatusNotImplemented, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "not configured", response["error"]) assert.Contains(t, response["message"], "CHARON_EMERGENCY_TOKEN") // Note: Audit logging is async via SecurityService channel, tested separately } func TestEmergencySecurityReset_TokenTooShort(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) // Configure token that is too short shortToken := "too-short" require.NoError(t, os.Setenv(EmergencyTokenEnvVar, shortToken)) defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }() // Make request req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, shortToken) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert response assert.Equal(t, http.StatusNotImplemented, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "not configured", response["error"]) assert.Contains(t, response["message"], "minimum length") } func TestEmergencySecurityReset_NoRateLimit(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken)) defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }() wrongToken := "wrong-token-for-no-rate-limit-test-32chars" // Make rapid requests with invalid token; all should be unauthorized for i := 0; i < 10; i++ { req, _ := http.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, wrongToken) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "Request %d should be unauthorized", i+1) var response map[string]interface{} err := json.NewDecoder(w.Body).Decode(&response) require.NoError(t, err) assert.Equal(t, "unauthorized", response["error"]) } } func TestEmergencySecurityReset_TriggersReloadAndCacheInvalidate(t *testing.T) { // Setup db := setupEmergencyTestDB(t) mockCaddy := &mockCaddyManager{} mockCache := &mockCacheInvalidator{} handler := NewEmergencyHandlerWithDeps(db, mockCaddy, mockCache) router := setupEmergencyRouter(handler) validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken)) defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }() // Make request with valid token req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, validToken) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, 1, mockCaddy.calls) assert.Equal(t, 1, mockCache.calls) } func TestEmergencySecurityReset_ClearsBlockDecisions(t *testing.T) { db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) router := setupEmergencyRouter(handler) validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum" require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken)) defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }() require.NoError(t, db.Create(&models.SecurityDecision{UUID: "dec-1", Source: "manual", Action: "block", IP: "127.0.0.1", CreatedAt: time.Now()}).Error) require.NoError(t, db.Create(&models.SecurityDecision{UUID: "dec-2", Source: "manual", Action: "allow", IP: "127.0.0.2", CreatedAt: time.Now()}).Error) req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) req.Header.Set(EmergencyTokenHeader, validToken) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var remaining []models.SecurityDecision require.NoError(t, db.Find(&remaining).Error) require.Len(t, remaining, 1) assert.Equal(t, "allow", remaining[0].Action) } func TestEmergencySecurityReset_MiddlewarePrevalidatedBypass(t *testing.T) { db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) { c.Set("emergency_bypass", true) handler.SecurityReset(c) }) req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) } func TestEmergencySecurityReset_MiddlewareBypass_ResetFailure(t *testing.T) { db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) stdDB, err := db.DB() require.NoError(t, err) require.NoError(t, stdDB.Close()) gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) { c.Set("emergency_bypass", true) handler.SecurityReset(c) }) req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusInternalServerError, w.Code) } func TestLogEnhancedAudit(t *testing.T) { // Setup db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) defer handler.Close() // Flush async audit events // Test enhanced audit logging clientIP := "192.168.1.100" action := "emergency_reset_test" details := "Test audit log" duration := 150 * time.Millisecond handler.logEnhancedAudit(clientIP, action, details, true, duration) // Close to flush async events before querying DB handler.Close() // Verify audit log was created var audit models.SecurityAudit err := db.Where("actor = ?", clientIP).First(&audit).Error require.NoError(t, err, "Audit log should be created") assert.Equal(t, clientIP, audit.Actor) assert.Equal(t, action, audit.Action) assert.Contains(t, audit.Details, "result=success") assert.Contains(t, audit.Details, "duration=") assert.Contains(t, audit.Details, "timestamp=") } func TestNewEmergencyTokenHandler(t *testing.T) { db := setupEmergencyTestDB(t) // Create token service tokenService := services.NewEmergencyTokenService(db) // Create handler using the token handler constructor handler := NewEmergencyTokenHandler(tokenService) // Verify handler was created correctly require.NotNil(t, handler) require.NotNil(t, handler.db) require.NotNil(t, handler.tokenService) require.Nil(t, handler.securityService) // Token handler doesn't need security service // Cleanup handler.Close() } func TestGenerateToken_Success(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") c.Set("userID", uint(1)) handler.GenerateToken(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token", jsonReader(map[string]interface{}{"expiration_days": 30})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotEmpty(t, resp["token"]) assert.Equal(t, "30_days", resp["expiration_policy"]) } func TestGenerateToken_AdminRequired(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { // No role set - simulating non-admin user handler.GenerateToken(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token", jsonReader(map[string]interface{}{"expiration_days": 30})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestGenerateToken_InvalidExpirationDays(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") handler.GenerateToken(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token", jsonReader(map[string]interface{}{"expiration_days": 500})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "Expiration days must be between 0 and 365") } func TestGetTokenStatus_Success(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/v1/emergency/token/status", func(c *gin.Context) { c.Set("role", "admin") handler.GetTokenStatus(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/emergency/token/status", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) // Check key fields exist assert.True(t, resp["configured"].(bool)) assert.Equal(t, "30_days", resp["expiration_policy"]) } func TestGetTokenStatus_AdminRequired(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/v1/emergency/token/status", handler.GetTokenStatus) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/emergency/token/status", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestRevokeToken_Success(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) gin.SetMode(gin.TestMode) router := gin.New() router.DELETE("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") handler.RevokeToken(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, "/api/v1/emergency/token", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "Emergency token revoked") } func TestRevokeToken_AdminRequired(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.DELETE("/api/v1/emergency/token", handler.RevokeToken) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, "/api/v1/emergency/token", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestUpdateTokenExpiration_Success(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) { c.Set("role", "admin") handler.UpdateTokenExpiration(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration", jsonReader(map[string]interface{}{"expiration_days": 60})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "new_expires_at") } func TestUpdateTokenExpiration_AdminRequired(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", handler.UpdateTokenExpiration) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration", jsonReader(map[string]interface{}{"expiration_days": 60})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestUpdateTokenExpiration_InvalidDays(t *testing.T) { db := setupEmergencyTestDB(t) tokenService := services.NewEmergencyTokenService(db) handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) { c.Set("role", "admin") handler.UpdateTokenExpiration(c) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration", jsonReader(map[string]interface{}{"expiration_days": 400})) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "Expiration days must be between 0 and 365") }