2026-03-11 15:12:02 +10:00
package httpserver
import (
2026-03-23 16:45:40 +10:00
"bytes"
2026-03-11 15:12:02 +10:00
"encoding/json"
2026-03-23 16:45:40 +10:00
"fmt"
2026-03-11 15:12:02 +10:00
"net/http"
"net/http/httptest"
"testing"
2026-03-11 15:30:49 +10:00
appsoc "github.com/syntrex/gomcp/internal/application/soc"
domsoc "github.com/syntrex/gomcp/internal/domain/soc"
"github.com/syntrex/gomcp/internal/infrastructure/sqlite"
2026-03-11 15:12:02 +10:00
)
// 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 )
2026-03-23 16:45:40 +10:00
mux . HandleFunc ( "GET /api/soc/incidents/{id}" , srv . handleIncidentDetail )
2026-03-11 15:12:02 +10:00
mux . HandleFunc ( "GET /api/soc/sensors" , srv . handleSensors )
2026-03-23 16:45:40 +10:00
mux . HandleFunc ( "GET /api/soc/clusters" , srv . handleClusters )
mux . HandleFunc ( "GET /api/soc/rules" , srv . handleRules )
2026-03-11 15:12:02 +10:00
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 )
2026-03-23 16:45:40 +10:00
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 )
2026-03-11 15:12:02 +10:00
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 )
2026-03-23 16:45:40 +10:00
// Ingest 10 events (unique descriptions to avoid dedup)
2026-03-11 15:12:02 +10:00
for i := 0 ; i < 10 ; i ++ {
socSvc . IngestEvent ( domsoc . SOCEvent {
2026-03-23 16:45:40 +10:00
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 ) ,
2026-03-11 15:12:02 +10:00
} )
}
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 {
2026-03-23 16:45:40 +10:00
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 ) ,
2026-03-11 15:12:02 +10:00
} )
}
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 {
2026-03-23 16:45:40 +10:00
SensorID : "test-sensor-001" ,
Source : domsoc . SourceSentinelCore ,
Category : "test" ,
Severity : domsoc . SeverityLow ,
Description : "test event for sensor registration" ,
2026-03-11 15:12:02 +10:00
} )
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 )
}
2026-03-23 16:45:40 +10:00
// TestHTTP_ThreatIntel_Returns200 verifies threat-intel returns IOCs and feeds.
func TestHTTP_ThreatIntel_Returns200 ( t * testing . T ) {
2026-03-11 15:12:02 +10:00
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 )
}
2026-03-23 16:45:40 +10:00
// ThreatIntelEngine is always initialized, should return enabled=true
if enabled , ok := result [ "enabled" ] . ( bool ) ; ! ok || ! enabled {
t . Error ( "expected enabled=true" )
2026-03-11 15:12:02 +10:00
}
}
// TestHTTP_Analytics_Returns200 verifies GET /api/soc/analytics returns a valid report.
func TestHTTP_Analytics_Returns200 ( t * testing . T ) {
ts , socSvc := newTestServer ( t )
2026-03-23 16:45:40 +10:00
// Ingest some events for analytics (unique descriptions to avoid dedup)
2026-03-11 15:12:02 +10:00
for i := 0 ; i < 5 ; i ++ {
socSvc . IngestEvent ( domsoc . SOCEvent {
2026-03-23 16:45:40 +10:00
SensorID : "analytics-sensor" ,
Source : domsoc . SourceShield ,
Category : "prompt_injection" ,
Severity : domsoc . SeverityHigh ,
Description : fmt . Sprintf ( "injection attempt for analytics test #%d" , i ) ,
2026-03-11 15:12:02 +10:00
} )
}
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 )
}
}
2026-03-23 16:45:40 +10:00
// --- 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 )
}
}