Files
Charon/backend/internal/api/handlers/benchmark_test.go
GitHub Actions af8384046c chore: implement instruction compliance remediation
- Replace Go interface{} with any (Go 1.18+ standard)
- Add database indexes to frequently queried model fields
- Add JSDoc documentation to frontend API client methods
- Remove deprecated docker-compose version keys
- Add concurrency groups to all 25 GitHub Actions workflows
- Add YAML front matter and fix H1→H2 headings in docs

Coverage: Backend 85.5%, Frontend 87.73%
Security: No vulnerabilities detected

Refs: docs/plans/instruction_compliance_spec.md
2025-12-21 04:08:42 +00:00

464 lines
12 KiB
Go

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)
}
}
}