Files
Charon/backend/internal/api/handlers/perf_assert_test.go
GitHub Actions 67f2f27cf8 feat: Add Import Success Modal and Certificate Status Card features
- Implemented ImportSuccessModal to replace alert with a modal displaying import results and guidance.
- Updated ImportCaddy to show the new modal with import summary and navigation options.
- Created CertificateStatusCard to display certificate provisioning status on the dashboard.
- Enhanced API types and hooks to support new features.
- Added unit tests for ImportSuccessModal and CertificateStatusCard components.
- Updated QA report to reflect the status of the new features and tests.
2025-12-12 00:42:27 +00:00

184 lines
5.4 KiB
Go

package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"sort"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"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"
)
// quick helper to form float ms from duration
func ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000.0 }
// setupPerfDB - uses a file-backed sqlite to avoid concurrency panics in parallel tests
func setupPerfDB(t *testing.T) *gorm.DB {
t.Helper()
path := ":memory:?cache=shared&_journal_mode=WAL"
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityDecision{}, &models.SecurityRuleSet{}, &models.SecurityConfig{}))
return db
}
// thresholdFromEnv loads threshold from environment var as milliseconds
// thresholdFromEnv removed — tests use inline environment parsing for clarity.
// gatherStats runs the request counts times and returns durations ms
func gatherStats(t *testing.T, req *http.Request, router http.Handler, counts int) []float64 {
t.Helper()
res := make([]float64, 0, counts)
for i := 0; i < counts; i++ {
w := httptest.NewRecorder()
s := time.Now()
router.ServeHTTP(w, req)
d := time.Since(s)
res = append(res, ms(d))
if w.Code >= 500 {
t.Fatalf("unexpected status: %d", w.Code)
}
}
return res
}
// computePercentiles returns avg, p50, p95, p99, max
func computePercentiles(samples []float64) (avg, p50, p95, p99, maxVal float64) {
sort.Float64s(samples)
var sum float64
for _, s := range samples {
sum += s
}
avg = sum / float64(len(samples))
p := func(pct float64) float64 {
idx := int(float64(len(samples)) * pct)
if idx < 0 {
idx = 0
}
if idx >= len(samples) {
idx = len(samples) - 1
}
return samples[idx]
}
p50 = p(0.50)
p95 = p(0.95)
p99 = p(0.99)
maxVal = samples[len(samples)-1]
return
}
// perfLogStats removed — tests log stats inline where helpful.
func TestPerf_GetStatus_AssertThreshold(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
db := setupPerfDB(t)
// seed settings to emulate production path
_ = db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "true", Category: "feature"})
_ = db.Create(&models.Setting{Key: "security.waf.enabled", Value: "true", Category: "security"})
cfg := config.SecurityConfig{CerberusEnabled: true}
h := NewSecurityHandler(cfg, db, nil)
router := gin.New()
router.GET("/api/v1/security/status", h.GetStatus)
counts := 500
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
samples := gatherStats(t, req, router, counts)
avg, _, p95, _, maxVal := computePercentiles(samples)
// default thresholds ms
thresholdP95 := 2.0 // 2ms per request
if env := os.Getenv("PERF_MAX_MS_GETSTATUS_P95"); env != "" {
if parsed, err := time.ParseDuration(env); err == nil {
thresholdP95 = ms(parsed)
}
}
// fail if p95 exceeds threshold
t.Logf("GetStatus avg=%.3fms p95=%.3fms max=%.3fms", avg, p95, maxVal)
if p95 > thresholdP95 {
t.Fatalf("GetStatus P95 (%.3fms) exceeds threshold %.3fms", p95, thresholdP95)
}
}
func TestPerf_GetStatus_Parallel_AssertThreshold(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
db := setupPerfDB(t)
cfg := config.SecurityConfig{CerberusEnabled: true}
h := NewSecurityHandler(cfg, db, nil)
router := gin.New()
router.GET("/api/v1/security/status", h.GetStatus)
n := 200
samples := make(chan float64, n)
var worker = func() {
for i := 0; i < n; i++ {
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
w := httptest.NewRecorder()
s := time.Now()
router.ServeHTTP(w, req)
d := time.Since(s)
samples <- ms(d)
}
}
// run 4 concurrent workers
for k := 0; k < 4; k++ {
go worker()
}
collected := make([]float64, 0, n*4)
for i := 0; i < n*4; i++ {
collected = append(collected, <-samples)
}
avg, _, p95, _, maxVal := computePercentiles(collected)
thresholdP95 := 5.0 // 5ms default
if env := os.Getenv("PERF_MAX_MS_GETSTATUS_P95_PARALLEL"); env != "" {
if parsed, err := time.ParseDuration(env); err == nil {
thresholdP95 = ms(parsed)
}
}
t.Logf("GetStatus Parallel avg=%.3fms p95=%.3fms max=%.3fms", avg, p95, maxVal)
if p95 > thresholdP95 {
t.Fatalf("GetStatus Parallel P95 (%.3fms) exceeds threshold %.3fms", p95, thresholdP95)
}
}
func TestPerf_ListDecisions_AssertThreshold(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
db := setupPerfDB(t)
// seed decisions
for i := 0; i < 1000; i++ {
db.Create(&models.SecurityDecision{UUID: fmt.Sprintf("d-%d", 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)
counts := 200
req := httptest.NewRequest("GET", "/api/v1/security/decisions?limit=50", http.NoBody)
samples := gatherStats(t, req, router, counts)
avg, _, p95, _, maxVal := computePercentiles(samples)
thresholdP95 := 30.0 // 30ms default
if env := os.Getenv("PERF_MAX_MS_LISTDECISIONS_P95"); env != "" {
if parsed, err := time.ParseDuration(env); err == nil {
thresholdP95 = ms(parsed)
}
}
t.Logf("ListDecisions avg=%.3fms p95=%.3fms max=%.3fms", avg, p95, maxVal)
if p95 > thresholdP95 {
t.Fatalf("ListDecisions P95 (%.3fms) exceeds threshold %.3fms", p95, thresholdP95)
}
}