package handlers import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" ) // Tests for UpdateConfig handler to improve coverage (currently 46%) func TestSecurityHandler_UpdateConfig_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/config", handler.UpdateConfig) payload := map[string]any{ "name": "default", "admin_whitelist": "192.168.1.0/24", "waf_mode": "monitor", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotNil(t, resp["config"]) } func TestSecurityHandler_UpdateConfig_DefaultName(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/config", handler.UpdateConfig) // Payload without name - should default to "default" payload := map[string]any{ "admin_whitelist": "10.0.0.0/8", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_UpdateConfig_InvalidPayload(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/config", handler.UpdateConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", strings.NewReader("invalid json")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for GetConfig handler func TestSecurityHandler_GetConfig_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create a config cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "127.0.0.1"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/config", handler.GetConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/config", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotNil(t, resp["config"]) } func TestSecurityHandler_GetConfig_NotFound(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/config", handler.GetConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/config", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Nil(t, resp["config"]) } // Tests for ListDecisions handler func TestSecurityHandler_ListDecisions_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) // Create some decisions with UUIDs db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: "1.2.3.4", Action: "block", Source: "waf"}) db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: "5.6.7.8", Action: "allow", Source: "acl"}) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/decisions", handler.ListDecisions) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/decisions", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) decisions := resp["decisions"].([]any) assert.Len(t, decisions, 2) } func TestSecurityHandler_ListDecisions_WithLimit(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) // Create 5 decisions with unique UUIDs for i := 0; i < 5; i++ { db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: fmt.Sprintf("1.2.3.%d", i), Action: "block", Source: "waf"}) } handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/decisions", handler.ListDecisions) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/decisions?limit=2", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) decisions := resp["decisions"].([]any) assert.Len(t, decisions, 2) } // Tests for CreateDecision handler func TestSecurityHandler_CreateDecision_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "ip": "10.0.0.1", "action": "block", "reason": "manual block", "details": "Test manual override", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_CreateDecision_MissingIP(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "action": "block", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_CreateDecision_MissingAction(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "ip": "10.0.0.1", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_CreateDecision_InvalidPayload(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/decisions", handler.CreateDecision) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", strings.NewReader("invalid")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for ListRuleSets handler func TestSecurityHandler_ListRuleSets_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) // Create some rulesets with UUIDs db.Create(&models.SecurityRuleSet{UUID: uuid.New().String(), Name: "owasp-crs", Mode: "blocking", Content: "# OWASP rules"}) db.Create(&models.SecurityRuleSet{UUID: uuid.New().String(), Name: "custom", Mode: "detection", Content: "# Custom rules"}) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/rulesets", handler.ListRuleSets) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/rulesets", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) rulesets := resp["rulesets"].([]any) assert.Len(t, rulesets, 2) } // Tests for UpsertRuleSet handler func TestSecurityHandler_UpsertRuleSet_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/rulesets", handler.UpsertRuleSet) payload := map[string]any{ "name": "test-ruleset", "mode": "blocking", "content": "# Test rules", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_UpsertRuleSet_MissingName(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/rulesets", handler.UpsertRuleSet) payload := map[string]any{ "mode": "blocking", "content": "# Test rules", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_UpsertRuleSet_InvalidPayload(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/rulesets", handler.UpsertRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", strings.NewReader("invalid")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for DeleteRuleSet handler (currently 52%) func TestSecurityHandler_DeleteRuleSet_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) // Create a ruleset to delete ruleset := models.SecurityRuleSet{Name: "delete-me", Mode: "blocking"} db.Create(&ruleset) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/1", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["deleted"].(bool)) } func TestSecurityHandler_DeleteRuleSet_NotFound(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/999", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestSecurityHandler_DeleteRuleSet_InvalidID(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/invalid", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_DeleteRuleSet_EmptyID(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() // Note: This route pattern won't match empty ID, but testing the handler directly router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) // This should hit the "id is required" check if we bypass routing w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/", http.NoBody) router.ServeHTTP(w, req) // Router won't match this path, so 404 assert.Equal(t, http.StatusNotFound, w.Code) } // Tests for Enable handler func TestSecurityHandler_Enable_NoConfigNoWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) // Should succeed when no config exists - creates new config assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_WithWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with whitelist containing 127.0.0.1 cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "127.0.0.1"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "127.0.0.1:12345" // Use RemoteAddr for ClientIP router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_IPNotInWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with whitelist that doesn't include test IP cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "10.0.0.0/8"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.1:12345" // Not in 10.0.0.0/8 router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestSecurityHandler_Enable_WithValidBreakGlassToken(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) router.POST("/security/enable", handler.Enable) // First, create a config with no whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: ""} db.Create(&cfg) // Generate a break-glass token w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var tokenResp map[string]string _ = json.Unmarshal(w.Body.Bytes(), &tokenResp) token := tokenResp["token"] // Now try to enable with the token payload := map[string]string{"break_glass_token": token} body, _ := json.Marshal(payload) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/security/enable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_WithInvalidBreakGlassToken(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with no whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: ""} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) payload := map[string]string{"break_glass_token": "invalid-token"} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // Tests for Disable handler (currently 44%) func TestSecurityHandler_Disable_FromLocalhost(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { // Simulate localhost request c.Request.RemoteAddr = "127.0.0.1:12345" handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.False(t, resp["enabled"].(bool)) } func TestSecurityHandler_Disable_FromRemoteWithToken(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) // Generate token w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) var tokenResp map[string]string _ = json.Unmarshal(w.Body.Bytes(), &tokenResp) token := tokenResp["token"] // Disable with token payload := map[string]string{"break_glass_token": token} body, _ := json.Marshal(payload) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/security/disable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Disable_FromRemoteNoToken(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestSecurityHandler_Disable_FromRemoteInvalidToken(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) payload := map[string]string{"break_glass_token": "invalid-token"} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // Tests for GenerateBreakGlass handler func TestSecurityHandler_GenerateBreakGlass_NoConfig(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) // Should succeed and create a new config with the token assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotEmpty(t, resp["token"]) } // Test Enable with IPv6 localhost func TestSecurityHandler_Disable_FromIPv6Localhost(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "[::1]:12345" // IPv6 localhost handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // Test Enable with CIDR whitelist matching func TestSecurityHandler_Enable_WithCIDRWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with CIDR whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "192.168.0.0/16, 10.0.0.0/8"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.50:12345" // In 192.168.0.0/16 router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // Test Enable with exact IP in whitelist func TestSecurityHandler_Enable_WithExactIPWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with exact IP whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "192.168.1.100"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.100:12345" router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }