package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" ) // setupBenchmarkDB creates an in-memory SQLite database for benchmarks func setupBenchmarkDB(b *testing.B) *gorm.DB { b.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { b.Fatal(err) } if err := db.AutoMigrate( &models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.Setting{}, &models.ProxyHost{}, &models.AccessList{}, &models.User{}, ); err != nil { b.Fatal(err) } return db } // ============================================================================= // SECURITY HANDLER BENCHMARKS // ============================================================================= func BenchmarkSecurityHandler_GetStatus(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) // Seed settings settings := []models.Setting{ {Key: "feature.cerberus.enabled", Value: "true", Category: "feature"}, {Key: "security.waf.enabled", Value: "true", Category: "security"}, {Key: "security.rate_limit.enabled", Value: "true", Category: "security"}, {Key: "security.crowdsec.enabled", Value: "true", Category: "security"}, {Key: "security.acl.enabled", Value: "true", Category: "security"}, } for _, s := range settings { db.Create(&s) } cfg := config.SecurityConfig{CerberusEnabled: true} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/status", h.GetStatus) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_GetStatus_NoSettings(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) cfg := config.SecurityConfig{CerberusEnabled: true} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/status", h.GetStatus) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_ListDecisions(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) // Seed some decisions for i := 0; i < 100; i++ { db.Create(&models.SecurityDecision{ UUID: "test-uuid-" + string(rune(i)), Source: "test", Action: "block", IP: "192.168.1.1", }) } cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/decisions", h.ListDecisions) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/decisions?limit=50", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_ListRuleSets(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) // Seed some rulesets for i := 0; i < 10; i++ { db.Create(&models.SecurityRuleSet{ UUID: "ruleset-uuid-" + string(rune(i)), Name: "Ruleset " + string(rune('A'+i)), Content: "SecRule REQUEST_URI \"@contains /admin\" \"id:1000,phase:1,deny\"", Mode: "blocking", }) } cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/rulesets", h.ListRuleSets) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/rulesets", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_UpsertRuleSet(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.POST("/api/v1/security/rulesets", h.UpsertRuleSet) payload := map[string]any{ "name": "bench-ruleset", "content": "SecRule REQUEST_URI \"@contains /admin\" \"id:1000,phase:1,deny\"", "mode": "blocking", } body, _ := json.Marshal(payload) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("POST", "/api/v1/security/rulesets", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_CreateDecision(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.POST("/api/v1/security/decisions", h.CreateDecision) payload := map[string]any{ "ip": "192.168.1.100", "action": "block", "details": "benchmark test", } body, _ := json.Marshal(payload) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("POST", "/api/v1/security/decisions", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_GetConfig(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) // Seed a config db.Create(&models.SecurityConfig{ Name: "default", Enabled: true, AdminWhitelist: "192.168.1.0/24", WAFMode: "block", RateLimitEnable: true, RateLimitBurst: 10, }) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/config", h.GetConfig) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/config", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_UpdateConfig(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.PUT("/api/v1/security/config", h.UpdateConfig) payload := map[string]any{ "name": "default", "enabled": true, "rate_limit_enable": true, "rate_limit_burst": 10, "rate_limit_requests": 100, } body, _ := json.Marshal(payload) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("PUT", "/api/v1/security/config", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } // ============================================================================= // PARALLEL BENCHMARKS (Concurrency Testing) // ============================================================================= func BenchmarkSecurityHandler_GetStatus_Parallel(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) settings := []models.Setting{ {Key: "feature.cerberus.enabled", Value: "true", Category: "feature"}, {Key: "security.waf.enabled", Value: "true", Category: "security"}, } for _, s := range settings { db.Create(&s) } cfg := config.SecurityConfig{CerberusEnabled: true} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/status", h.GetStatus) b.ResetTimer() b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } }) } func BenchmarkSecurityHandler_ListDecisions_Parallel(b *testing.B) { gin.SetMode(gin.ReleaseMode) // Use file-based SQLite with WAL mode for parallel testing db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_journal_mode=WAL"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { b.Fatal(err) } if err := db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{}); err != nil { b.Fatal(err) } for i := 0; i < 100; i++ { db.Create(&models.SecurityDecision{ UUID: "test-uuid-" + string(rune(i)), Source: "test", Action: "block", IP: "192.168.1.1", }) } cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/decisions", h.ListDecisions) b.ResetTimer() b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { req := httptest.NewRequest("GET", "/api/v1/security/decisions?limit=50", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } }) } // ============================================================================= // MEMORY PRESSURE BENCHMARKS // ============================================================================= func BenchmarkSecurityHandler_LargeRuleSetContent(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.POST("/api/v1/security/rulesets", h.UpsertRuleSet) // 100KB ruleset content (under 2MB limit) largeContent := "" for i := 0; i < 1000; i++ { largeContent += "SecRule REQUEST_URI \"@contains /path" + string(rune(i)) + "\" \"id:" + string(rune(1000+i)) + ",phase:1,deny\"\n" } payload := map[string]any{ "name": "large-ruleset", "content": largeContent, "mode": "blocking", } body, _ := json.Marshal(payload) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("POST", "/api/v1/security/rulesets", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } } func BenchmarkSecurityHandler_ManySettingsLookups(b *testing.B) { gin.SetMode(gin.ReleaseMode) db := setupBenchmarkDB(b) // Seed many settings for i := 0; i < 100; i++ { db.Create(&models.Setting{ Key: "setting.key." + string(rune(i)), Value: "value", Category: "misc", }) } // Security settings settings := []models.Setting{ {Key: "feature.cerberus.enabled", Value: "true", Category: "feature"}, {Key: "security.waf.enabled", Value: "true", Category: "security"}, {Key: "security.rate_limit.enabled", Value: "true", Category: "security"}, {Key: "security.crowdsec.enabled", Value: "true", Category: "security"}, {Key: "security.crowdsec.mode", Value: "local", Category: "security"}, {Key: "security.crowdsec.api_url", Value: "http://localhost:8080", Category: "security"}, {Key: "security.acl.enabled", Value: "true", Category: "security"}, } for _, s := range settings { db.Create(&s) } cfg := config.SecurityConfig{} h := NewSecurityHandler(cfg, db, nil) router := gin.New() router.GET("/api/v1/security/status", h.GetStatus) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status: %d", w.Code) } } }