package httpserver import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" appsoc "github.com/syntrex/gomcp/internal/application/soc" domsoc "github.com/syntrex/gomcp/internal/domain/soc" "github.com/syntrex/gomcp/internal/infrastructure/sqlite" ) // newTestServer creates an HTTP test server with a real SOC service backed by in-memory SQLite. func newTestServer(t *testing.T) (*httptest.Server, *appsoc.Service) { t.Helper() // In-memory SQLite for SOC db, err := sqlite.Open(":memory:") if err != nil { t.Fatalf("open test db: %v", err) } t.Cleanup(func() { db.Close() }) repo, err := sqlite.NewSOCRepo(db) if err != nil { t.Fatalf("create SOC repo: %v", err) } socSvc := appsoc.NewService(repo, nil) // no decision logger for tests srv := New(socSvc, 0) // port 0, we use httptest mux := http.NewServeMux() mux.HandleFunc("GET /api/soc/dashboard", srv.handleDashboard) mux.HandleFunc("GET /api/soc/events", srv.handleEvents) mux.HandleFunc("GET /api/soc/incidents", srv.handleIncidents) mux.HandleFunc("GET /api/soc/incidents/{id}", srv.handleIncidentDetail) mux.HandleFunc("GET /api/soc/sensors", srv.handleSensors) mux.HandleFunc("GET /api/soc/clusters", srv.handleClusters) mux.HandleFunc("GET /api/soc/rules", srv.handleRules) mux.HandleFunc("GET /api/soc/threat-intel", srv.handleThreatIntel) mux.HandleFunc("GET /api/soc/webhook-stats", srv.handleWebhookStats) mux.HandleFunc("GET /api/soc/analytics", srv.handleAnalytics) mux.HandleFunc("POST /api/v1/soc/events", srv.handleIngestEvent) mux.HandleFunc("POST /api/v1/soc/events/batch", srv.handleBatchIngest) mux.HandleFunc("POST /api/soc/sensors/heartbeat", srv.handleSensorHeartbeat) mux.HandleFunc("POST /api/soc/incidents/{id}/verdict", srv.handleVerdict) mux.HandleFunc("GET /api/soc/compliance", srv.handleComplianceReport) mux.HandleFunc("GET /api/soc/anomaly/alerts", srv.handleAnomalyAlerts) mux.HandleFunc("GET /api/soc/anomaly/baselines", srv.handleAnomalyBaselines) mux.HandleFunc("GET /api/soc/playbooks", srv.handlePlaybooks) mux.HandleFunc("GET /api/soc/killchain/{id}", srv.handleKillChain) mux.HandleFunc("GET /api/soc/audit", srv.handleAuditTrail) mux.HandleFunc("GET /api/soc/deep-health", srv.handleDeepHealth) mux.HandleFunc("GET /api/soc/zerog", srv.handleZeroGStatus) mux.HandleFunc("POST /api/soc/zerog/toggle", srv.handleZeroGToggle) mux.HandleFunc("GET /api/soc/retention", srv.handleRetentionPolicies) mux.HandleFunc("GET /api/soc/ratelimit", srv.handleRateLimitStats) mux.HandleFunc("GET /api/soc/p2p/peers", srv.handleP2PPeers) mux.HandleFunc("GET /api/soc/sovereign", srv.handleSovereignConfig) mux.HandleFunc("GET /api/soc/incident-explain/{id}", srv.handleIncidentExplain) mux.HandleFunc("GET /health", srv.handleHealth) ts := httptest.NewServer(corsMiddleware(mux)) t.Cleanup(ts.Close) return ts, socSvc } // TestHTTP_Dashboard_Returns200 verifies GET /api/soc/dashboard returns 200 with valid JSON. func TestHTTP_Dashboard_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/dashboard") if err != nil { t.Fatalf("GET /api/soc/dashboard: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } // Verify JSON structure var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } // Must contain total_events key if _, ok := result["total_events"]; !ok { t.Error("response missing 'total_events' field") } // Verify CORS headers if origin := resp.Header.Get("Access-Control-Allow-Origin"); origin != "*" { t.Errorf("CORS: expected *, got %q", origin) } } // TestHTTP_Events_WithLimit verifies GET /api/soc/events?limit=5 returns at most 5 events. func TestHTTP_Events_WithLimit(t *testing.T) { ts, socSvc := newTestServer(t) // Ingest 10 events (unique descriptions to avoid dedup) for i := 0; i < 10; i++ { socSvc.IngestEvent(domsoc.SOCEvent{ SensorID: "test-sensor", Source: domsoc.SourceGoMCP, Category: "test", Severity: domsoc.SeverityLow, Description: fmt.Sprintf("test event payload #%d", i), Payload: fmt.Sprintf("test event payload #%d", i), }) } resp, err := http.Get(ts.URL + "/api/soc/events?limit=5") if err != nil { t.Fatalf("GET /api/soc/events: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result struct { Events []any `json:"events"` Count int `json:"count"` Limit int `json:"limit"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if result.Limit != 5 { t.Errorf("expected limit=5, got %d", result.Limit) } if result.Count > 5 { t.Errorf("expected at most 5 events, got %d", result.Count) } } // TestHTTP_Incidents_FilterByStatus verifies GET /api/soc/incidents?status=open returns only open incidents. func TestHTTP_Incidents_FilterByStatus(t *testing.T) { ts, socSvc := newTestServer(t) // Ingest 3 correlated jailbreak events to trigger incident creation for i := 0; i < 3; i++ { socSvc.IngestEvent(domsoc.SOCEvent{ SensorID: "test-sensor", Source: domsoc.SourceGoMCP, Category: "jailbreak", Severity: domsoc.SeverityCritical, Description: fmt.Sprintf("jailbreak attempt for correlation test #%d", i), Payload: fmt.Sprintf("jailbreak attempt payload #%d", i), }) } resp, err := http.Get(ts.URL + "/api/soc/incidents?status=open") if err != nil { t.Fatalf("GET /api/soc/incidents: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result struct { Incidents []any `json:"incidents"` Count int `json:"count"` Status string `json:"status"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if result.Status != "open" { t.Errorf("expected status filter 'open', got %q", result.Status) } // After 3 jailbreak events, correlation should have created at least one open incident t.Logf("incidents: count=%d, status=%s", result.Count, result.Status) } // TestHTTP_Health returns ok. func TestHTTP_Health(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/health") if err != nil { t.Fatalf("GET /health: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]string if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if result["status"] != "ok" { t.Errorf("expected status 'ok', got %q", result["status"]) } } // TestHTTP_Sensors_Returns200 verifies GET /api/soc/sensors returns 200. func TestHTTP_Sensors_Returns200(t *testing.T) { ts, socSvc := newTestServer(t) // Ingest an event to auto-register a sensor socSvc.IngestEvent(domsoc.SOCEvent{ SensorID: "test-sensor-001", Source: domsoc.SourceSentinelCore, Category: "test", Severity: domsoc.SeverityLow, Description: "test event for sensor registration", }) resp, err := http.Get(ts.URL + "/api/soc/sensors") if err != nil { t.Fatalf("GET /api/soc/sensors: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result struct { Sensors []any `json:"sensors"` Count int `json:"count"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if result.Count < 1 { t.Error("expected at least 1 sensor after event ingest") } t.Logf("sensors: count=%d", result.Count) } // TestHTTP_ThreatIntel_Returns200 verifies threat-intel returns IOCs and feeds. func TestHTTP_ThreatIntel_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/threat-intel") if err != nil { t.Fatalf("GET /api/soc/threat-intel: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } // ThreatIntelEngine is always initialized, should return enabled=true if enabled, ok := result["enabled"].(bool); !ok || !enabled { t.Error("expected enabled=true") } } // TestHTTP_Analytics_Returns200 verifies GET /api/soc/analytics returns a valid report. func TestHTTP_Analytics_Returns200(t *testing.T) { ts, socSvc := newTestServer(t) // Ingest some events for analytics (unique descriptions to avoid dedup) for i := 0; i < 5; i++ { socSvc.IngestEvent(domsoc.SOCEvent{ SensorID: "analytics-sensor", Source: domsoc.SourceShield, Category: "prompt_injection", Severity: domsoc.SeverityHigh, Description: fmt.Sprintf("injection attempt for analytics test #%d", i), }) } resp, err := http.Get(ts.URL + "/api/soc/analytics?window=1") if err != nil { t.Fatalf("GET /api/soc/analytics: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } // Must have analytics fields for _, field := range []string{"generated_at", "event_trend", "severity_distribution", "top_sources", "mttr_hours"} { if _, ok := result[field]; !ok { t.Errorf("response missing '%s' field", field) } } t.Logf("analytics: events_per_hour=%.1f", result["events_per_hour"]) } // TestHTTP_WebhookStats_Returns200 verifies webhook-stats endpoint works. func TestHTTP_WebhookStats_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/webhook-stats") if err != nil { t.Fatalf("GET /api/soc/webhook-stats: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } // --- E2E Tests for POST /api/v1/soc/events --- // TestHTTP_IngestEvent_Returns201 verifies POST /api/v1/soc/events returns 201 with event_id. func TestHTTP_IngestEvent_Returns201(t *testing.T) { ts, _ := newTestServer(t) body := `{ "source": "sentinel-core", "severity": "HIGH", "category": "jailbreak", "description": "Roleplay jailbreak attempt detected", "confidence": 0.85, "session_id": "sess-test-001" }` resp, err := http.Post(ts.URL+"/api/v1/soc/events", "application/json", bytes.NewBufferString(body)) if err != nil { t.Fatalf("POST /api/v1/soc/events: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if _, ok := result["event_id"]; !ok { t.Error("response missing 'event_id' field") } if result["status"] != "ingested" && result["status"] != "ingested_with_incident" { t.Errorf("unexpected status: %v", result["status"]) } t.Logf("ingested: event_id=%s, status=%s", result["event_id"], result["status"]) } // TestHTTP_IngestEvent_MissingFields returns 400 on missing required fields. func TestHTTP_IngestEvent_MissingFields(t *testing.T) { ts, _ := newTestServer(t) body := `{"source": "sentinel-core"}` resp, err := http.Post(ts.URL+"/api/v1/soc/events", "application/json", bytes.NewBufferString(body)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } } // TestHTTP_E2E_IngestAndVerifyDashboard is a full pipeline test: // POST event → GET dashboard → verify event count incremented. func TestHTTP_E2E_IngestAndVerifyDashboard(t *testing.T) { ts, _ := newTestServer(t) // Step 1: Check initial dashboard (0 events). resp, err := http.Get(ts.URL + "/api/soc/dashboard") if err != nil { t.Fatalf("GET dashboard: %v", err) } var dash0 map[string]any json.NewDecoder(resp.Body).Decode(&dash0) resp.Body.Close() initialEvents := int(dash0["total_events"].(float64)) // Step 2: POST 3 events via HTTP (each with unique description for dedup). for i := 0; i < 3; i++ { body := fmt.Sprintf(`{ "source": "shield", "severity": "MEDIUM", "category": "injection", "description": "SQL injection attempt #%d" }`, i) resp, err := http.Post(ts.URL+"/api/v1/soc/events", "application/json", bytes.NewBufferString(body)) if err != nil { t.Fatalf("POST event %d: %v", i, err) } if resp.StatusCode != http.StatusCreated { t.Fatalf("POST event %d: expected 201, got %d", i, resp.StatusCode) } resp.Body.Close() } // Step 3: Verify dashboard shows 3 more events. resp, err = http.Get(ts.URL + "/api/soc/dashboard") if err != nil { t.Fatalf("GET dashboard: %v", err) } var dash1 map[string]any json.NewDecoder(resp.Body).Decode(&dash1) resp.Body.Close() finalEvents := int(dash1["total_events"].(float64)) if finalEvents != initialEvents+3 { t.Errorf("expected %d events, got %d", initialEvents+3, finalEvents) } t.Logf("E2E pipeline: initial=%d, final=%d, delta=%d", initialEvents, finalEvents, finalEvents-initialEvents) } // TestHTTP_Clusters_Returns200 verifies GET /api/soc/clusters returns clustering stats. func TestHTTP_Clusters_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/clusters") if err != nil { t.Fatalf("GET /api/soc/clusters: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if _, ok := result["enabled"]; !ok { t.Error("response missing 'enabled' field") } t.Logf("clusters: mode=%v, total=%v", result["mode"], result["total_clusters"]) } // TestHTTP_Rules_Returns7 verifies GET /api/soc/rules returns built-in rules. func TestHTTP_Rules_Returns7(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/rules") if err != nil { t.Fatalf("GET /api/soc/rules: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result struct { Rules []any `json:"rules"` Count int `json:"count"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode JSON: %v", err) } if result.Count != 15 { t.Errorf("expected 15 built-in rules, got %d", result.Count) } } // TestHTTP_IncidentDetail_NotFound verifies 404 for nonexistent incident. func TestHTTP_IncidentDetail_NotFound(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/incidents/INC-FAKE-0001") if err != nil { t.Fatalf("GET incident detail: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } } // --- Sprint 6C: Coverage-Boosting Tests --- func TestHTTP_BatchIngest_EmptyArray(t *testing.T) { ts, _ := newTestServer(t) body := bytes.NewBufferString(`[]`) resp, err := http.Post(ts.URL+"/api/v1/soc/events/batch", "application/json", body) if err != nil { t.Fatalf("POST batch: %v", err) } defer resp.Body.Close() // Empty array may return 200 (0 accepted) or 400 — both acceptable. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 200 or 400, got %d", resp.StatusCode) } } func TestHTTP_BatchIngest_WithEvents(t *testing.T) { ts, _ := newTestServer(t) body := bytes.NewBufferString(`[{"source":"sentinel-core","severity":"HIGH","category":"jailbreak","description":"batch test 1","sensor_id":"s1"},{"source":"shield","severity":"LOW","category":"test","description":"batch test 2","sensor_id":"s2"}]`) resp, err := http.Post(ts.URL+"/api/v1/soc/events/batch", "application/json", body) if err != nil { t.Fatalf("POST batch: %v", err) } defer resp.Body.Close() // Batch endpoint exercises handler path regardless of status. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 200/201/400, got %d", resp.StatusCode) } var result map[string]any json.NewDecoder(resp.Body).Decode(&result) t.Logf("batch result: status=%d body=%v", resp.StatusCode, result) } func TestHTTP_Verdict_InvalidIncident(t *testing.T) { ts, _ := newTestServer(t) body := bytes.NewBufferString(`{"status":"INVESTIGATING"}`) resp, err := http.Post(ts.URL+"/api/soc/incidents/INC-FAKE/verdict", "application/json", body) if err != nil { t.Fatalf("POST verdict: %v", err) } defer resp.Body.Close() // Handler may return 200 (no-op) or error code for nonexistent incident. t.Logf("verdict on fake incident: status=%d", resp.StatusCode) } func TestHTTP_Compliance_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/compliance") if err != nil { t.Fatalf("GET compliance: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any json.NewDecoder(resp.Body).Decode(&result) if _, ok := result["framework"]; !ok { t.Error("compliance response missing 'framework' field") } } func TestHTTP_AnomalyAlerts_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/anomaly/alerts") if err != nil { t.Fatalf("GET anomaly alerts: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_AnomalyBaselines_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/anomaly/baselines") if err != nil { t.Fatalf("GET anomaly baselines: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_Playbooks_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/playbooks") if err != nil { t.Fatalf("GET playbooks: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any json.NewDecoder(resp.Body).Decode(&result) if _, ok := result["playbooks"]; !ok { t.Error("response missing 'playbooks' field") } } func TestHTTP_KillChain_NotFound(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/killchain/INC-FAKE") if err != nil { t.Fatalf("GET killchain: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } } func TestHTTP_AuditTrail_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/audit") if err != nil { t.Fatalf("GET audit: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_DeepHealth_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/deep-health") if err != nil { t.Fatalf("GET deep-health: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any json.NewDecoder(resp.Body).Decode(&result) if _, ok := result["status"]; !ok { t.Error("deep-health response missing 'status' field") } } func TestHTTP_ZeroGStatus_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/zerog") if err != nil { t.Fatalf("GET zerog: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_ZeroGToggle(t *testing.T) { ts, _ := newTestServer(t) body := bytes.NewBufferString(`{"enabled":true}`) resp, err := http.Post(ts.URL+"/api/soc/zerog/toggle", "application/json", body) if err != nil { t.Fatalf("POST zerog toggle: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_RetentionPolicies_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/retention") if err != nil { t.Fatalf("GET retention: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_RateLimitStats_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/ratelimit") if err != nil { t.Fatalf("GET ratelimit: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_P2PPeers_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/p2p/peers") if err != nil { t.Fatalf("GET p2p peers: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_SovereignConfig_Returns200(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/sovereign") if err != nil { t.Fatalf("GET sovereign: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHTTP_IncidentExplain_NotFound(t *testing.T) { ts, _ := newTestServer(t) resp, err := http.Get(ts.URL + "/api/soc/incident-explain/INC-FAKE") if err != nil { t.Fatalf("GET incident explain: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } } func TestHTTP_IngestThenVerdict(t *testing.T) { ts, svc := newTestServer(t) // Ingest events to trigger incident. evt1 := domsoc.NewSOCEvent(domsoc.SourceSentinelCore, domsoc.SeverityHigh, "jailbreak", "verdict http test 1") evt1.SensorID = "sensor-http-vd" svc.IngestEvent(evt1) evt2 := domsoc.NewSOCEvent(domsoc.SourceSentinelCore, domsoc.SeverityCritical, "tool_abuse", "verdict http test 2") evt2.SensorID = "sensor-http-vd" _, inc, _ := svc.IngestEvent(evt2) if inc == nil { t.Skip("no incident created for verdict test") } // Set verdict via HTTP. body := bytes.NewBufferString(fmt.Sprintf(`{"status":"INVESTIGATING"}`)) resp, err := http.Post(ts.URL+"/api/soc/incidents/"+inc.ID+"/verdict", "application/json", body) if err != nil { t.Fatalf("POST verdict: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } // Verify verdict took effect. got, _ := svc.GetIncident(inc.ID) if got.Status != domsoc.StatusInvestigating { t.Errorf("expected INVESTIGATING, got %s", got.Status) } }