package handlers import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "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/config" "github.com/Wikid82/charon/backend/internal/models" ) // TestSecurityHandler_GetStatus_RespectsSettingsTable verifies that GetStatus // reads WAF, Rate Limit, and CrowdSec enabled states from the settings table, // overriding the static config values. func TestSecurityHandler_GetStatus_RespectsSettingsTable(t *testing.T) { tests := []struct { name string cfg config.SecurityConfig settings []models.Setting expectedWAF bool expectedRate bool expectedCrowd bool }{ { name: "WAF enabled via settings overrides disabled config", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "disabled", RateLimitMode: "disabled", CrowdSecMode: "disabled", }, settings: []models.Setting{ {Key: "security.waf.enabled", Value: "true"}, }, expectedWAF: true, expectedRate: false, expectedCrowd: false, }, { name: "Rate Limit enabled via settings overrides disabled config", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "disabled", RateLimitMode: "disabled", CrowdSecMode: "disabled", }, settings: []models.Setting{ {Key: "security.rate_limit.enabled", Value: "true"}, }, expectedWAF: false, expectedRate: true, expectedCrowd: false, }, { name: "CrowdSec enabled via settings overrides disabled config", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "disabled", RateLimitMode: "disabled", CrowdSecMode: "disabled", }, settings: []models.Setting{ {Key: "security.crowdsec.enabled", Value: "true"}, }, expectedWAF: false, expectedRate: false, expectedCrowd: true, }, { name: "All modules enabled via settings", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "disabled", RateLimitMode: "disabled", CrowdSecMode: "disabled", }, settings: []models.Setting{ {Key: "security.waf.enabled", Value: "true"}, {Key: "security.rate_limit.enabled", Value: "true"}, {Key: "security.crowdsec.enabled", Value: "true"}, }, expectedWAF: true, expectedRate: true, expectedCrowd: true, }, { name: "WAF disabled via settings overrides enabled config", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local", }, settings: []models.Setting{ {Key: "security.waf.enabled", Value: "false"}, {Key: "security.rate_limit.enabled", Value: "false"}, {Key: "security.crowdsec.enabled", Value: "false"}, }, expectedWAF: false, expectedRate: false, expectedCrowd: false, }, { name: "No settings - falls back to config (enabled)", cfg: config.SecurityConfig{ CerberusEnabled: true, WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local", }, settings: []models.Setting{}, expectedWAF: true, expectedRate: true, expectedCrowd: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) // Insert settings for _, s := range tt.settings { db.Create(&s) } handler := NewSecurityHandler(tt.cfg, db, nil) router := gin.New() router.GET("/security/status", handler.GetStatus) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/status", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]any err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) // Check WAF enabled waf := response["waf"].(map[string]any) assert.Equal(t, tt.expectedWAF, waf["enabled"].(bool), "WAF enabled mismatch") // Check Rate Limit enabled rateLimit := response["rate_limit"].(map[string]any) assert.Equal(t, tt.expectedRate, rateLimit["enabled"].(bool), "Rate Limit enabled mismatch") // Check CrowdSec enabled crowdsec := response["crowdsec"].(map[string]any) assert.Equal(t, tt.expectedCrowd, crowdsec["enabled"].(bool), "CrowdSec enabled mismatch") }) } } // TestSecurityHandler_GetStatus_WAFModeFromSettings verifies that WAF mode // is properly reflected when enabled via settings. func TestSecurityHandler_GetStatus_WAFModeFromSettings(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) // WAF config is disabled, but settings says enabled cfg := config.SecurityConfig{ CerberusEnabled: true, WAFMode: "disabled", } db.Create(&models.Setting{Key: "security.waf.enabled", Value: "true"}) handler := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/security/status", handler.GetStatus) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/status", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]any err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) waf := response["waf"].(map[string]any) // When enabled via settings, mode should reflect "enabled" state assert.True(t, waf["enabled"].(bool)) } // TestSecurityHandler_GetStatus_RateLimitModeFromSettings verifies that Rate Limit mode // is properly reflected when enabled via settings. func TestSecurityHandler_GetStatus_RateLimitModeFromSettings(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) // Rate limit config is disabled, but settings says enabled cfg := config.SecurityConfig{ CerberusEnabled: true, RateLimitMode: "disabled", } db.Create(&models.Setting{Key: "security.rate_limit.enabled", Value: "true"}) handler := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/security/status", handler.GetStatus) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/status", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]any err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) rateLimit := response["rate_limit"].(map[string]any) assert.True(t, rateLimit["enabled"].(bool)) configApply := response["config_apply"].(map[string]any) assert.Equal(t, false, configApply["available"]) assert.Equal(t, "unknown", configApply["status"]) } func TestSecurityHandler_GetStatus_IncludesLatestConfigApplyState(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.CaddyConfig{})) require.NoError(t, db.Create(&models.CaddyConfig{Success: true, ErrorMsg: ""}).Error) handler := NewSecurityHandler(config.SecurityConfig{CerberusEnabled: true}, db, nil) router := gin.New() router.GET("/security/status", handler.GetStatus) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/status", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]any err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) configApply := response["config_apply"].(map[string]any) assert.Equal(t, true, configApply["available"]) assert.Equal(t, "applied", configApply["status"]) assert.Equal(t, true, configApply["success"]) } func TestSecurityHandler_PatchACL_RequiresAdminWhitelist(t *testing.T) { db := OpenTestDBWithMigrations(t) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "192.0.2.1/32"}).Error) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.PATCH("/security/acl", handler.PatchACL) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/security/acl", strings.NewReader(`{"enabled":true}`)) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "203.0.113.5:1234" router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestSecurityHandler_PatchACL_AllowsWhitelistedIP(t *testing.T) { db := OpenTestDBWithMigrations(t) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "203.0.113.0/24"}).Error) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.PATCH("/security/acl", handler.PatchACL) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/security/acl", strings.NewReader(`{"enabled":true}`)) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "203.0.113.5:1234" router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var setting models.Setting err := db.Where("key = ?", "feature.cerberus.enabled").First(&setting).Error require.NoError(t, err) assert.Equal(t, "true", setting.Value) var cfg models.SecurityConfig err = handler.db.Where("name = ?", "default").First(&cfg).Error require.NoError(t, err) assert.True(t, cfg.Enabled) } func TestSecurityHandler_PatchACL_SetsACLAndCerberusSettings(t *testing.T) { dsn := "file:TestSecurityHandler_PatchACL_SetsACLAndCerberusSettings?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) w := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(w) ctx.Set("role", "admin") ctx.Set("userID", uint(1)) ctx.Request, _ = http.NewRequest("PATCH", "/security/acl", strings.NewReader(`{"enabled":true}`)) ctx.Request.Header.Set("Content-Type", "application/json") ctx.Request.RemoteAddr = "203.0.113.5:1234" handler.toggleSecurityModule(ctx, "security.acl.enabled", true) assert.Equal(t, http.StatusOK, w.Code) var setting models.Setting err = db.Where("key = ?", "security.acl.enabled").First(&setting).Error require.NoError(t, err) assert.Equal(t, "true", setting.Value) var cerbSetting models.Setting err = db.Where("key = ?", "feature.cerberus.enabled").First(&cerbSetting).Error require.NoError(t, err) assert.Equal(t, "true", cerbSetting.Value) var legacySetting models.Setting err = db.Where("key = ?", "security.cerberus.enabled").First(&legacySetting).Error require.NoError(t, err) assert.Equal(t, "true", legacySetting.Value) } func TestSecurityHandler_EnsureSecurityConfigEnabled_CreatesWhenMissing(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) err := handler.ensureSecurityConfigEnabled() require.NoError(t, err) var cfg models.SecurityConfig err = handler.db.Where("name = ?", "default").First(&cfg).Error require.NoError(t, err) assert.True(t, cfg.Enabled) } func TestSecurityHandler_PatchACL_AllowsEmergencyBypass(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "192.0.2.1/32"}).Error) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Set("emergency_bypass", true) c.Next() }) router.PATCH("/security/acl", handler.PatchACL) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/security/acl", strings.NewReader(`{"enabled":true}`)) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "203.0.113.5:1234" router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }