Files
ServiceMap-Public/backend/tests/test_health.py
T

100 lines
3.8 KiB
Python

from datetime import UTC, datetime
from pathlib import Path
import pytest
from app.health import dependency_status, effective_status, validate_dependencies
from app.incidents import incident_overview
from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind, ServiceUpdate
from app.store import Store
def _probe(service_id: str, status: HealthStatus) -> ProbeResult:
return ProbeResult(
id=f"probe-{service_id}-{status.value}",
service_id=service_id,
status=status,
latency_ms=1.0,
detail=status.value,
checked_at=datetime.now(UTC),
)
def test_down_dependency_degrades_healthy_service(tmp_path: Path) -> None:
store = Store(str(tmp_path / "servicemap.db"))
database = store.create_service(
ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432)
)
api = store.create_service(
ServiceCreate(
name="API",
kind=ServiceKind.HTTP,
url="https://example.test",
depends_on=[database.id],
)
)
store.save_probe(_probe(database.id, HealthStatus.DOWN))
store.save_probe(_probe(api.id, HealthStatus.HEALTHY))
assert dependency_status(api, store) == HealthStatus.DOWN
assert effective_status(api, store) == HealthStatus.DEGRADED
def test_service_without_dependencies_has_healthy_dependency_state(tmp_path: Path) -> None:
store = Store(str(tmp_path / "servicemap.db"))
service = store.create_service(
ServiceCreate(name="Standalone", kind=ServiceKind.HTTP, url="https://example.test")
)
assert dependency_status(service, store) == HealthStatus.HEALTHY
assert effective_status(service, store) == HealthStatus.UNKNOWN
def test_cycle_validation_rejects_recursive_dependencies(tmp_path: Path) -> None:
store = Store(str(tmp_path / "servicemap.db"))
database = store.create_service(
ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432)
)
api = store.create_service(
ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id])
)
with pytest.raises(ValueError, match="cycle"):
validate_dependencies([api.id], store, service_id=database.id)
validate_dependencies([database.id], store, service_id=api.id)
updated = store.update_service(
api.id,
ServiceUpdate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id]),
)
assert updated is not None
def test_incident_overview_finds_root_cause_and_multihop_impact(tmp_path: Path) -> None:
store = Store(str(tmp_path / "servicemap.db"))
database = store.create_service(
ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432)
)
api = store.create_service(
ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id])
)
web = store.create_service(
ServiceCreate(name="Web", kind=ServiceKind.HTTP, url="https://web.test", depends_on=[api.id])
)
store.save_probe(_probe(database.id, HealthStatus.DOWN))
store.save_probe(_probe(api.id, HealthStatus.HEALTHY))
store.save_probe(_probe(web.id, HealthStatus.HEALTHY))
assert effective_status(api, store) == HealthStatus.DEGRADED
assert dependency_status(web, store) == HealthStatus.DEGRADED
assert effective_status(web, store) == HealthStatus.DEGRADED
overview = incident_overview(store)
assert overview.active is True
assert overview.down_service_ids == [database.id]
assert set(overview.degraded_service_ids) == {api.id, web.id}
assert overview.candidates[0].service_id == database.id
assert api.id in overview.candidates[0].affected_service_ids
assert web.id in overview.candidates[0].affected_service_ids