mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-08 19:12:37 +02:00
299 lines
8.1 KiB
Go
299 lines
8.1 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
appsoc "github.com/sentinel-community/gomcp/internal/application/soc"
|
|
domsoc "github.com/sentinel-community/gomcp/internal/domain/soc"
|
|
"github.com/sentinel-community/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/sensors", srv.handleSensors)
|
|
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("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
|
|
for i := 0; i < 10; i++ {
|
|
socSvc.IngestEvent(domsoc.SOCEvent{
|
|
SensorID: "test-sensor",
|
|
Category: "test",
|
|
Severity: domsoc.SeverityLow,
|
|
Payload: "test event payload",
|
|
})
|
|
}
|
|
|
|
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",
|
|
Category: "jailbreak",
|
|
Severity: domsoc.SeverityCritical,
|
|
Payload: "jailbreak attempt payload",
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
|
|
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_NotConfigured verifies threat-intel returns disabled when not configured.
|
|
func TestHTTP_ThreatIntel_NotConfigured(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)
|
|
}
|
|
|
|
// Without SetThreatIntel, should return enabled=false
|
|
if enabled, ok := result["enabled"].(bool); !ok || enabled {
|
|
t.Error("expected enabled=false when threat intel not configured")
|
|
}
|
|
}
|
|
|
|
// 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
|
|
for i := 0; i < 5; i++ {
|
|
socSvc.IngestEvent(domsoc.SOCEvent{
|
|
SensorID: "analytics-sensor",
|
|
Source: domsoc.SourceShield,
|
|
Category: "injection",
|
|
Severity: domsoc.SeverityHigh,
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|