package api import ( "bytes" "fmt" "net/http" "net/http/httptest" "testing" "github.com/ez-api/ez-api/internal/middleware" "github.com/ez-api/ez-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupAlertTestDB(t *testing.T) *gorm.DB { dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.Alert{}, &model.AlertThresholdConfig{}); err != nil { t.Fatalf("migrate: %v", err) } return db } func setupAlertRouter(db *gorm.DB) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(middleware.ResponseEnvelope()) handler := NewAlertHandler(db) r.GET("/admin/alerts/thresholds", handler.GetAlertThresholds) r.PUT("/admin/alerts/thresholds", handler.UpdateAlertThresholds) r.GET("/admin/alerts", handler.ListAlerts) r.POST("/admin/alerts", handler.CreateAlert) r.GET("/admin/alerts/stats", handler.GetAlertStats) return r } func TestGetAlertThresholdsDefault(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) req, _ := http.NewRequest("GET", "/admin/alerts/thresholds", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) } var resp AlertThresholdView decodeEnvelope(t, w, &resp) // Should return defaults if resp.GlobalQPS != 100 { t.Errorf("expected GlobalQPS=100, got %d", resp.GlobalQPS) } if resp.MasterRPM != 20 { t.Errorf("expected MasterRPM=20, got %d", resp.MasterRPM) } if resp.MasterRPD != 1000 { t.Errorf("expected MasterRPD=1000, got %d", resp.MasterRPD) } if resp.MasterTPM != 10_000_000 { t.Errorf("expected MasterTPM=10000000, got %d", resp.MasterTPM) } if resp.MasterTPD != 100_000_000 { t.Errorf("expected MasterTPD=100000000, got %d", resp.MasterTPD) } } func TestUpdateAlertThresholds(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) // Update some thresholds body := `{"global_qps": 500, "master_rpm": 100}` req, _ := http.NewRequest("PUT", "/admin/alerts/thresholds", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) } var resp AlertThresholdView decodeEnvelope(t, w, &resp) if resp.GlobalQPS != 500 { t.Errorf("expected GlobalQPS=500, got %d", resp.GlobalQPS) } if resp.MasterRPM != 100 { t.Errorf("expected MasterRPM=100, got %d", resp.MasterRPM) } // Other fields should remain default if resp.MasterRPD != 1000 { t.Errorf("expected MasterRPD=1000 (unchanged), got %d", resp.MasterRPD) } } func TestUpdateAlertThresholdsValidation(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) tests := []struct { name string body string expected int }{ {"negative global_qps", `{"global_qps": -1}`, http.StatusBadRequest}, {"zero global_qps", `{"global_qps": 0}`, http.StatusBadRequest}, {"negative master_rpm", `{"master_rpm": -5}`, http.StatusBadRequest}, {"valid update", `{"global_qps": 200}`, http.StatusOK}, {"negative min_rpm_requests_1m", `{"min_rpm_requests_1m": -1}`, http.StatusBadRequest}, {"zero min_rpm_requests_1m allowed", `{"min_rpm_requests_1m": 0}`, http.StatusOK}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req, _ := http.NewRequest("PUT", "/admin/alerts/thresholds", bytes.NewBufferString(tc.body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tc.expected { t.Errorf("expected status %d, got %d: %s", tc.expected, w.Code, w.Body.String()) } }) } } func TestCreateAlertWithTrafficSpikeType(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) body := `{ "type": "traffic_spike", "severity": "warning", "title": "RPM Exceeded", "message": "Master exceeded RPM threshold", "related_id": 1, "related_type": "master", "metadata": "{\"metric\":\"master_rpm\",\"value\":150,\"threshold\":20,\"window\":\"1m\"}" }` req, _ := http.NewRequest("POST", "/admin/alerts", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected status 201, got %d: %s", w.Code, w.Body.String()) } var resp AlertView decodeEnvelope(t, w, &resp) if resp.Type != "traffic_spike" { t.Errorf("expected type=traffic_spike, got %s", resp.Type) } if resp.Severity != "warning" { t.Errorf("expected severity=warning, got %s", resp.Severity) } if resp.Status != "active" { t.Errorf("expected status=active, got %s", resp.Status) } } func TestListAlertsWithTypeFilter(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) // Create some alerts alerts := []model.Alert{ {Type: model.AlertTypeRateLimit, Severity: model.AlertSeverityWarning, Status: model.AlertStatusActive, Title: "Rate Limit 1"}, {Type: model.AlertTypeTrafficSpike, Severity: model.AlertSeverityWarning, Status: model.AlertStatusActive, Title: "Traffic Spike 1"}, {Type: model.AlertTypeTrafficSpike, Severity: model.AlertSeverityCritical, Status: model.AlertStatusActive, Title: "Traffic Spike 2"}, } for _, a := range alerts { if err := db.Create(&a).Error; err != nil { t.Fatalf("create alert: %v", err) } } // Filter by type=traffic_spike req, _ := http.NewRequest("GET", "/admin/alerts?type=traffic_spike", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) } var resp ListAlertsResponse decodeEnvelope(t, w, &resp) if resp.Total != 2 { t.Errorf("expected 2 traffic_spike alerts, got %d", resp.Total) } for _, a := range resp.Items { if a.Type != "traffic_spike" { t.Errorf("expected type=traffic_spike, got %s", a.Type) } } } func TestAlertStatsIncludesAllAlerts(t *testing.T) { db := setupAlertTestDB(t) r := setupAlertRouter(db) // Create alerts of different types and statuses alerts := []model.Alert{ {Type: model.AlertTypeRateLimit, Severity: model.AlertSeverityWarning, Status: model.AlertStatusActive, Title: "A1"}, {Type: model.AlertTypeTrafficSpike, Severity: model.AlertSeverityCritical, Status: model.AlertStatusActive, Title: "A2"}, {Type: model.AlertTypeErrorSpike, Severity: model.AlertSeverityInfo, Status: model.AlertStatusResolved, Title: "A3"}, } for _, a := range alerts { if err := db.Create(&a).Error; err != nil { t.Fatalf("create alert: %v", err) } } req, _ := http.NewRequest("GET", "/admin/alerts/stats", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) } var resp AlertStats decodeEnvelope(t, w, &resp) if resp.Total != 3 { t.Errorf("expected total=3, got %d", resp.Total) } if resp.Active != 2 { t.Errorf("expected active=2, got %d", resp.Active) } if resp.Resolved != 1 { t.Errorf("expected resolved=1, got %d", resp.Resolved) } if resp.Critical != 1 { t.Errorf("expected critical=1, got %d", resp.Critical) } if resp.Warning != 1 { t.Errorf("expected warning=1, got %d", resp.Warning) } }