package handlers import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "github.com/Wikid82/charon/backend/internal/caddy" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" ) func setupToggleTest(t *testing.T) (*SecurityHandler, *gorm.DB) { gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) // Create default SecurityConfig require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) // caddyManager nil to avoid reload logic return h, db } func TestSecurityToggles(t *testing.T) { h, db := setupToggleTest(t) tests := []struct { name string method string path string handler gin.HandlerFunc settingKey string expectVal string body string }{ // ACL {"EnableACL", "POST", "/api/v1/security/acl/enable", h.EnableACL, "security.acl.enabled", "true", ""}, {"DisableACL", "POST", "/api/v1/security/acl/disable", h.DisableACL, "security.acl.enabled", "false", ""}, // ACL Patch {"PatchACL_True", "PATCH", "/api/v1/security/acl", h.PatchACL, "security.acl.enabled", "true", `{"enabled": true}`}, {"PatchACL_False", "PATCH", "/api/v1/security/acl", h.PatchACL, "security.acl.enabled", "false", `{"enabled": false}`}, // WAF {"EnableWAF", "POST", "/api/v1/security/waf/enable", h.EnableWAF, "security.waf.enabled", "true", ""}, {"DisableWAF", "POST", "/api/v1/security/waf/disable", h.DisableWAF, "security.waf.enabled", "false", ""}, // WAF Patch {"PatchWAF_True", "PATCH", "/api/v1/security/waf", h.PatchWAF, "security.waf.enabled", "true", `{"enabled": true}`}, {"PatchWAF_False", "PATCH", "/api/v1/security/waf", h.PatchWAF, "security.waf.enabled", "false", `{"enabled": false}`}, // Cerberus {"EnableCerberus", "POST", "/api/v1/security/cerberus/enable", h.EnableCerberus, "feature.cerberus.enabled", "true", ""}, {"DisableCerberus", "POST", "/api/v1/security/cerberus/disable", h.DisableCerberus, "feature.cerberus.enabled", "false", ""}, // CrowdSec {"EnableCrowdSec", "POST", "/api/v1/security/crowdsec/enable", h.EnableCrowdSec, "security.crowdsec.enabled", "true", ""}, {"DisableCrowdSec", "POST", "/api/v1/security/crowdsec/disable", h.DisableCrowdSec, "security.crowdsec.enabled", "false", ""}, // CrowdSec Patch {"PatchCrowdSec_True", "PATCH", "/api/v1/security/crowdsec", h.PatchCrowdSec, "security.crowdsec.enabled", "true", `{"enabled": true}`}, {"PatchCrowdSec_False", "PATCH", "/api/v1/security/crowdsec", h.PatchCrowdSec, "security.crowdsec.enabled", "false", `{"enabled": false}`}, // RateLimit {"EnableRateLimit", "POST", "/api/v1/security/rate-limit/enable", h.EnableRateLimit, "security.rate_limit.enabled", "true", ""}, {"DisableRateLimit", "POST", "/api/v1/security/rate-limit/disable", h.DisableRateLimit, "security.rate_limit.enabled", "false", ""}, // RateLimit Patch {"PatchRateLimit_True", "PATCH", "/api/v1/security/rate-limit", h.PatchRateLimit, "security.rate_limit.enabled", "true", `{"enabled": true}`}, {"PatchRateLimit_False", "PATCH", "/api/v1/security/rate-limit", h.PatchRateLimit, "security.rate_limit.enabled", "false", `{"enabled": false}`}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { w := httptest.NewRecorder() var req *http.Request if tc.body != "" { req, _ = http.NewRequest(tc.method, tc.path, strings.NewReader(tc.body)) req.Header.Set("Content-Type", "application/json") } else { req, _ = http.NewRequest(tc.method, tc.path, nil) } c, _ := gin.CreateTestContext(w) c.Request = req // Mock Admin Role c.Set("role", "admin") tc.handler(c) require.Equal(t, http.StatusOK, w.Code) // Verify Setting var setting models.Setting err := db.Where("key = ?", tc.settingKey).First(&setting).Error assert.NoError(t, err) assert.Equal(t, tc.expectVal, setting.Value) if tc.expectVal == "true" && tc.settingKey != "feature.cerberus.enabled" { var cerberusSetting models.Setting err = db.Where("key = ?", "feature.cerberus.enabled").First(&cerberusSetting).Error assert.NoError(t, err) assert.Equal(t, "true", cerberusSetting.Value) } }) } } func TestSecurityToggles_Forbidden(t *testing.T) { h, _ := setupToggleTest(t) // Just test one endpoint to verify role check w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil) c, _ := gin.CreateTestContext(w) c.Request = req // No role set h.EnableACL(c) assert.Equal(t, http.StatusForbidden, w.Code) } func TestPatchACL_InvalidBody(t *testing.T) { h, _ := setupToggleTest(t) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/security/acl", strings.NewReader("invalid")) c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") h.PatchACL(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPatchWAF_InvalidBody(t *testing.T) { h, _ := setupToggleTest(t) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/security/waf", strings.NewReader("invalid")) c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") h.PatchWAF(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPatchRateLimit_InvalidBody(t *testing.T) { h, _ := setupToggleTest(t) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/security/rate-limit", strings.NewReader("invalid")) c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") h.PatchRateLimit(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPatchCrowdSec_InvalidBody(t *testing.T) { h, _ := setupToggleTest(t) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/security/crowdsec", strings.NewReader("invalid")) c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") h.PatchCrowdSec(c) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestACLForbiddenIfIPNotWhitelisted(t *testing.T) { h, db := setupToggleTest(t) // Update config to have whitelist err := db.Model(&models.SecurityConfig{}).Where("name = ?", "default").Update("admin_whitelist", "10.0.0.1").Error require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil) c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") c.Request.RemoteAddr = "192.168.1.5:1234" // Different IP h.EnableACL(c) assert.Equal(t, http.StatusForbidden, w.Code) } func TestACLEnabledIfIPWhitelisted(t *testing.T) { h, db := setupToggleTest(t) // Update config to have whitelist err := db.Model(&models.SecurityConfig{}).Where("name = ?", "default").Update("admin_whitelist", "1.2.3.4").Error require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil) req.Header.Set("X-Forwarded-For", "1.2.3.4") // Trusted proxy simulation needed or direct RemoteAddr c, _ := gin.CreateTestContext(w) c.Request = req c.Request.RemoteAddr = "1.2.3.4:1234" c.Set("role", "admin") h.EnableACL(c) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityToggles_RollbackSettingWhenApplyFails(t *testing.T) { gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error) require.NoError(t, db.Create(&models.Setting{Key: "security.waf.enabled", Value: "false", Category: "security", Type: "bool"}).Error) manager := caddy.NewManager( caddy.NewClient("http://127.0.0.1:65535"), db, t.TempDir(), t.TempDir(), false, config.SecurityConfig{}, ) h := NewSecurityHandler(config.SecurityConfig{}, db, manager) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/security/waf", strings.NewReader(`{"enabled":true}`)) req.Header.Set("Content-Type", "application/json") c, _ := gin.CreateTestContext(w) c.Request = req c.Set("role", "admin") h.PatchWAF(c) require.Equal(t, http.StatusInternalServerError, w.Code) var setting models.Setting require.NoError(t, db.Where("key = ?", "security.waf.enabled").First(&setting).Error) assert.Equal(t, "false", setting.Value) }