187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app import main as main_module
|
|
from app.demo import DEMO_ROOT_CAUSE_NAME, build_demo_records
|
|
from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind
|
|
from app.store import Store
|
|
|
|
|
|
def _client(tmp_path: Path) -> tuple[Store, TestClient]:
|
|
store = Store(str(tmp_path / "servicemap.db"))
|
|
main_module.store = store
|
|
return store, TestClient(main_module.app)
|
|
|
|
|
|
def _assert_acyclic(graph: dict[str, set[str]]) -> None:
|
|
visiting: set[str] = set()
|
|
visited: set[str] = set()
|
|
|
|
def visit(service_id: str) -> None:
|
|
assert service_id not in visiting
|
|
if service_id in visited:
|
|
return
|
|
visiting.add(service_id)
|
|
for dependency_id in graph[service_id]:
|
|
visit(dependency_id)
|
|
visiting.remove(service_id)
|
|
visited.add(service_id)
|
|
|
|
for service_id in graph:
|
|
visit(service_id)
|
|
|
|
|
|
def test_demo_seeds_topology_health_history_availability_and_incident(tmp_path: Path) -> None:
|
|
store, client = _client(tmp_path)
|
|
|
|
response = client.post("/api/v1/demo")
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
assert result["active"] is True
|
|
assert result["synthetic"] is True
|
|
assert result["scenario_name"] == "Northstar Platform"
|
|
assert result["total"] == 9
|
|
|
|
snapshots = client.get("/api/v1/services").json()
|
|
assert len(snapshots) == 9
|
|
by_name = {item["service"]["name"]: item for item in snapshots}
|
|
assert set(by_name) == {
|
|
"Public Endpoint",
|
|
"Reverse Proxy",
|
|
"Web Frontend",
|
|
"Auth Service",
|
|
"API Gateway",
|
|
"PostgreSQL",
|
|
"Redis",
|
|
"Background Worker",
|
|
"Object Storage",
|
|
}
|
|
ids = {item["service"]["id"] for item in snapshots}
|
|
assert all(set(item["service"]["depends_on"]).issubset(ids) for item in snapshots)
|
|
assert by_name[DEMO_ROOT_CAUSE_NAME]["latest_probe"]["status"] == HealthStatus.DOWN
|
|
assert by_name[DEMO_ROOT_CAUSE_NAME]["effective_status"] == HealthStatus.DOWN
|
|
assert all(item["effective_status"] != HealthStatus.UNKNOWN for item in snapshots)
|
|
|
|
semantic = client.get("/api/v1/topology/semantic").json()
|
|
assert {item["name"] for item in semantic["domains"]} == {"Edge", "Application", "Data"}
|
|
assert {item["name"] for item in semantic["applications"]} == {
|
|
"Public Gateway",
|
|
"Northstar App",
|
|
"Data Platform",
|
|
"Worker Platform",
|
|
}
|
|
|
|
incidents = client.get("/api/v1/incidents").json()
|
|
assert incidents["active"] is True
|
|
assert incidents["candidates"][0]["service_name"] == DEMO_ROOT_CAUSE_NAME
|
|
assert set(incidents["candidates"][0]["affected_service_names"]) == {
|
|
"API Gateway",
|
|
"Web Frontend",
|
|
"Reverse Proxy",
|
|
"Public Endpoint",
|
|
"Background Worker",
|
|
}
|
|
|
|
postgres_id = by_name[DEMO_ROOT_CAUSE_NAME]["service"]["id"]
|
|
history = client.get(f"/api/v1/services/{postgres_id}/history?limit=50").json()
|
|
assert len(history) == 24
|
|
availability = client.get(f"/api/v1/services/{postgres_id}/availability").json()
|
|
assert availability["total_checks"] == 24
|
|
assert availability["uptime_percent"] < 100
|
|
assert availability["down_checks"] == 2
|
|
assert store.demo_active() is True
|
|
|
|
|
|
def test_demo_reset_is_deterministic_and_exit_restores_topology_and_probes(tmp_path: Path) -> None:
|
|
store, client = _client(tmp_path)
|
|
original = store.create_service(
|
|
ServiceCreate(name="Existing API", kind=ServiceKind.HTTP, url="https://existing.test")
|
|
)
|
|
original_probe = ProbeResult(
|
|
id="existing-probe",
|
|
service_id=original.id,
|
|
status=HealthStatus.HEALTHY,
|
|
latency_ms=12.5,
|
|
detail="existing data",
|
|
checked_at=datetime.now(UTC),
|
|
)
|
|
store.save_probe(original_probe)
|
|
original_service_json = client.get(f"/api/v1/services/{original.id}").json()
|
|
|
|
first = client.post("/api/v1/demo").json()
|
|
first_services = client.get("/api/v1/services").json()
|
|
first_statuses = {
|
|
item["service"]["name"]: (item["latest_probe"]["status"], item["effective_status"])
|
|
for item in first_services
|
|
}
|
|
assert first["restored"] is False
|
|
assert client.get(f"/api/v1/services/{original.id}").status_code == 404
|
|
|
|
reset = client.post("/api/v1/demo/reset").json()
|
|
reset_services = client.get("/api/v1/services").json()
|
|
reset_statuses = {
|
|
item["service"]["name"]: (item["latest_probe"]["status"], item["effective_status"])
|
|
for item in reset_services
|
|
}
|
|
assert reset["active"] is True
|
|
assert reset["root_cause_service_name"] == DEMO_ROOT_CAUSE_NAME
|
|
assert reset_statuses == first_statuses
|
|
assert len(reset_services) == 9
|
|
|
|
exited = client.post("/api/v1/demo/exit").json()
|
|
assert exited["active"] is False
|
|
assert exited["restored"] is True
|
|
assert client.get("/api/v1/demo").json() == {
|
|
"active": False,
|
|
"synthetic": False,
|
|
"scenario_name": None,
|
|
"service_count": 0,
|
|
"root_cause_service_id": None,
|
|
"root_cause_service_name": None,
|
|
"affected_service_ids": [],
|
|
"affected_service_names": [],
|
|
"affected_count": 0,
|
|
}
|
|
assert client.get(f"/api/v1/services/{original.id}").json() == original_service_json
|
|
assert store.probe_history(original.id)[0].id == original_probe.id
|
|
|
|
with store._connect() as db: # regression assertion for FK-clean restoration
|
|
orphan_count = db.execute(
|
|
"SELECT COUNT(*) FROM probe_results p LEFT JOIN services s ON s.id = p.service_id WHERE s.id IS NULL"
|
|
).fetchone()[0]
|
|
assert orphan_count == 0
|
|
orphan_evidence_count = db.execute(
|
|
"SELECT COUNT(*) FROM health_evidence e LEFT JOIN services s ON s.id = e.service_id "
|
|
"WHERE e.service_id IS NOT NULL AND s.id IS NULL"
|
|
).fetchone()[0]
|
|
orphan_link_count = db.execute(
|
|
"SELECT COUNT(*) FROM health_evidence_service_links link LEFT JOIN health_evidence e "
|
|
"ON e.id = link.evidence_id WHERE e.id IS NULL"
|
|
).fetchone()[0]
|
|
assert orphan_evidence_count == 0
|
|
assert orphan_link_count == 0
|
|
|
|
|
|
def test_demo_probe_endpoints_remain_external_service_free(tmp_path: Path) -> None:
|
|
_, client = _client(tmp_path)
|
|
client.post("/api/v1/demo")
|
|
snapshots = client.get("/api/v1/services").json()
|
|
postgres_id = next(item["service"]["id"] for item in snapshots if item["service"]["name"] == DEMO_ROOT_CAUSE_NAME)
|
|
|
|
assert client.post(f"/api/v1/services/{postgres_id}/probe").status_code == 409
|
|
assert client.post("/api/v1/probe-all").json() == {"requested": 0, "completed": 0, "results": []}
|
|
|
|
|
|
def test_demo_records_have_valid_graph_and_stable_ids() -> None:
|
|
fixed_now = datetime(2026, 9, 12, 10, 0, tzinfo=UTC)
|
|
services_a, probes_a = build_demo_records(fixed_now)
|
|
services_b, probes_b = build_demo_records(fixed_now)
|
|
|
|
assert [service.model_dump(mode="json") for service in services_a] == [service.model_dump(mode="json") for service in services_b]
|
|
assert [probe.model_dump(mode="json") for probe in probes_a] == [probe.model_dump(mode="json") for probe in probes_b]
|
|
assert len(probes_a) == 9 * 24
|
|
graph = {service.id: set(service.depends_on) for service in services_a}
|
|
_assert_acyclic(graph)
|