100 lines
3.8 KiB
Python
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
|