import asyncio from datetime import UTC, datetime from typing import Self import pytest from app import probes from app.models import HealthStatus, ProbeResult, Service, ServiceKind def _service() -> Service: now = datetime.now(UTC) return Service( id="service-1", name="API", kind=ServiceKind.HTTP, url="https://api.local", created_at=now, updated_at=now, ) def _tcp_service() -> Service: now = datetime.now(UTC) return Service( id="tcp-service-1", name="TCP service", kind=ServiceKind.TCP, host="tcp.local", port=5432, created_at=now, updated_at=now, ) @pytest.mark.parametrize( ("status_code", "expected"), [ (200, HealthStatus.HEALTHY), (302, HealthStatus.HEALTHY), (404, HealthStatus.DEGRADED), (503, HealthStatus.DOWN), ], ) def test_http_status_classification( monkeypatch: pytest.MonkeyPatch, status_code: int, expected: HealthStatus, ) -> None: class FakeResponse: def __init__(self, code: int) -> None: self.status_code = code class FakeClient: async def __aenter__(self) -> Self: return self async def __aexit__(self, *args: object) -> None: return None async def get(self, url: str) -> FakeResponse: assert url == "https://api.local" return FakeResponse(status_code) monkeypatch.setattr(probes.httpx, "AsyncClient", lambda **kwargs: FakeClient()) status, detail = asyncio.run(probes._probe_once(_service(), 1.0)) assert status == expected assert detail == f"HTTP {status_code}" def test_retry_latency_reports_final_attempt_not_backoff( monkeypatch: pytest.MonkeyPatch, ) -> None: calls = 0 async def fake_probe_once(service: Service, timeout_seconds: float) -> tuple[HealthStatus, str]: nonlocal calls calls += 1 if calls == 1: raise OSError("temporary failure") return HealthStatus.HEALTHY, "HTTP 200" timings = iter([10.0, 10.05, 20.0, 20.02]) monkeypatch.setattr(probes, "_probe_once", fake_probe_once) monkeypatch.setattr(probes, "perf_counter", lambda: next(timings)) result = asyncio.run(probes.probe_service(_service(), timeout_seconds=1.0, retries=1)) assert calls == 2 assert result.status == HealthStatus.HEALTHY assert result.latency_ms == pytest.approx(20.0) assert result.detail == "HTTP 200 after 2 attempts" def test_tcp_probe_success_and_failure(monkeypatch: pytest.MonkeyPatch) -> None: class FakeWriter: def close(self) -> None: return None async def wait_closed(self) -> None: return None async def fake_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]: assert (host, port) == ("tcp.local", 5432) return object(), FakeWriter() monkeypatch.setattr(probes.asyncio, "open_connection", fake_open_connection) status, detail = asyncio.run(probes._probe_once(_tcp_service(), 1.0)) assert status == HealthStatus.HEALTHY assert detail == "TCP tcp.local:5432 reachable" async def failing_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]: raise ConnectionRefusedError("refused") monkeypatch.setattr(probes.asyncio, "open_connection", failing_open_connection) result = asyncio.run(probes.probe_service(_tcp_service(), timeout_seconds=1.0, retries=0)) assert result.status == HealthStatus.DOWN assert "ConnectionRefusedError" in result.detail def test_probe_many_respects_concurrency_boundary(monkeypatch: pytest.MonkeyPatch) -> None: services = [ _service().model_copy(update={"id": f"service-{index}", "name": f"API-{index}"}) for index in range(5) ] active = 0 maximum = 0 async def fake_probe_service( service: Service, *, timeout_seconds: float, retries: int, ) -> ProbeResult: nonlocal active, maximum active += 1 maximum = max(maximum, active) await asyncio.sleep(0) active -= 1 return ProbeResult( id=f"probe-{service.id}", service_id=service.id, status=HealthStatus.HEALTHY, latency_ms=1.0, detail="ok", checked_at=datetime.now(UTC), ) monkeypatch.setattr(probes, "probe_service", fake_probe_service) results = asyncio.run(probes.probe_many(services, concurrency=2)) assert len(results) == len(services) assert maximum <= 2