Cybersecurity-Projects/PROJECTS/advanced/ai-threat-detection/backend/tests/test_api.py

185 lines
5.3 KiB
Python

"""
©AngelaMos | 2026
test_api.py
Tests the FastAPI REST endpoints for health, threats, stats,
and model management using an in-memory database
Validates /health returns status, uptime, and pipeline flag.
Tests /threats CRUD: empty list returns zero total, random
UUID returns 404, seeded event is fetchable by ID with all
fields, and severity filter returns only matching items.
Tests /stats returns zeroed counts on empty window.
Tests /models/status returns detection_mode and
active_models list, and POST /models/retrain returns 202
with a 32-char job ID
Connects to:
api/health - liveness endpoint
api/threats - threat CRUD
api/stats - statistics endpoint
api/models_api - model status and retrain
"""
import uuid
from datetime import datetime, UTC
import pytest
from app.models.threat_event import ThreatEvent
@pytest.mark.asyncio
async def test_health_returns_200(db_client) -> None:
"""
Health endpoint returns 200 with status, uptime, and pipeline flag.
"""
response = await db_client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert isinstance(data["uptime_seconds"], int | float)
assert isinstance(data["pipeline_running"], bool)
@pytest.mark.asyncio
async def test_list_threats_empty(db_client) -> None:
"""
GET /threats on an empty database returns zero items.
"""
response = await db_client.get("/threats")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert data["limit"] == 50
assert data["offset"] == 0
assert data["items"] == []
@pytest.mark.asyncio
async def test_get_threat_not_found(db_client) -> None:
"""
GET /threats/{random_id} returns 404 when the event does not exist.
"""
fake_id = uuid.uuid4()
response = await db_client.get(f"/threats/{fake_id}")
assert response.status_code == 404
assert response.json()["detail"] == "Threat event not found"
@pytest.mark.asyncio
async def test_get_threat_by_id(db_session, db_client) -> None:
"""
Seed a threat event, then fetch it by ID.
"""
event_id = uuid.uuid4()
event = ThreatEvent(
id=event_id,
created_at=datetime.now(UTC),
source_ip="10.0.0.1",
request_method="GET",
request_path="/admin",
status_code=200,
response_size=512,
user_agent="TestBot/1.0",
threat_score=0.85,
severity="HIGH",
component_scores={"SQL_INJECTION": 0.85},
feature_vector=[0.0] * 35,
matched_rules=["SQL_INJECTION"],
)
db_session.add(event)
await db_session.commit()
response = await db_client.get(f"/threats/{event_id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(event_id)
assert data["source_ip"] == "10.0.0.1"
assert data["threat_score"] == 0.85
assert data["severity"] == "HIGH"
assert data["matched_rules"] == ["SQL_INJECTION"]
assert data["geo"]["country"] is None
@pytest.mark.asyncio
async def test_list_threats_severity_filter(db_session, db_client) -> None:
"""
Seed threats with different severities and filter by HIGH.
"""
now = datetime.now(UTC)
for severity, score in [("HIGH", 0.9), ("MEDIUM", 0.6), ("LOW", 0.3)]:
event = ThreatEvent(
id=uuid.uuid4(),
created_at=now,
source_ip="192.168.1.1",
request_method="GET",
request_path="/test",
status_code=200,
response_size=100,
user_agent="Mozilla/5.0",
threat_score=score,
severity=severity,
component_scores={},
feature_vector=[0.0] * 35,
)
db_session.add(event)
await db_session.commit()
response = await db_client.get("/threats", params={"severity": "HIGH"})
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert data["items"][0]["severity"] == "HIGH"
@pytest.mark.asyncio
async def test_stats_empty_window(db_client) -> None:
"""
GET /stats on an empty database returns zero counts.
"""
response = await db_client.get("/stats")
assert response.status_code == 200
data = response.json()
assert data["time_range"] == "24h"
assert data["threats_stored"] == 0
assert data["threats_detected"] == 0
assert data["severity_breakdown"]["high"] == 0
assert data["severity_breakdown"]["medium"] == 0
assert data["severity_breakdown"]["low"] == 0
assert data["top_source_ips"] == []
assert data["top_attacked_paths"] == []
@pytest.mark.asyncio
async def test_model_status(db_client) -> None:
"""
GET /models/status returns detection mode and active model list.
"""
response = await db_client.get("/models/status")
assert response.status_code == 200
data = response.json()
assert "detection_mode" in data
assert "active_models" in data
assert isinstance(data["active_models"], list)
@pytest.mark.asyncio
async def test_retrain_returns_202(db_client) -> None:
"""
POST /models/retrain returns 202 Accepted with a job ID.
"""
response = await db_client.post("/models/retrain")
assert response.status_code == 202
data = response.json()
assert data["status"] == "accepted"
assert len(data["job_id"]) == 32