"""Unit tests for health endpoint."""
from contextlib import asynccontextmanager
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import status
from httpx import AsyncClient
from vmk_data_collector.api.v1 import router_health
def _make_mock_engine(healthy: bool = True) -> Any:
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock()
@asynccontextmanager
async def connect_cm():
if not healthy:
raise RuntimeError("db down")
yield mock_conn
engine = MagicMock()
engine.connect = connect_cm
return engine
def _make_fake_httpx_client(healthy: bool = True) -> type:
class FakeClient:
def __init__(self, *a: Any, **k: Any) -> None:
pass
async def __aenter__(self):
return self
async def __aexit__(self, *a: Any) -> None:
pass
async def get(self, *a: Any, **k: Any) -> Any:
if not healthy:
raise RuntimeError("ollama down")
resp = MagicMock()
resp.raise_for_status = lambda: None
return resp
return FakeClient
class TestHealthCheck:
@pytest.mark.asyncio
async def test_all_healthy_returns_200(
self,
async_client: AsyncClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(router_health, "engine", _make_mock_engine(healthy=True))
monkeypatch.setattr(
router_health.httpx,
"AsyncClient",
_make_fake_httpx_client(healthy=True),
)
monkeypatch.setattr(
router_health.shutil,
"disk_usage",
lambda _p: MagicMock(free=500 * 1024 * 1024),
)
response = await async_client.get("/api/v1/health")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["status"] == "healthy"
for check in data["checks"].values():
assert check["status"] == "pass"
@pytest.mark.asyncio
async def test_db_fails_returns_503(
self,
async_client: AsyncClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(router_health, "engine", _make_mock_engine(healthy=False))
monkeypatch.setattr(
router_health.httpx,
"AsyncClient",
_make_fake_httpx_client(healthy=True),
)
monkeypatch.setattr(
router_health.shutil,
"disk_usage",
lambda _p: MagicMock(free=500 * 1024 * 1024),
)
response = await async_client.get("/api/v1/health")
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["status"] == "degraded"
assert data["checks"]["database"]["status"] == "fail"
assert data["checks"]["ollama"]["status"] == "pass"
@pytest.mark.asyncio
async def test_ollama_fails_returns_503(
self,
async_client: AsyncClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(router_health, "engine", _make_mock_engine(healthy=True))
monkeypatch.setattr(
router_health.httpx,
"AsyncClient",
_make_fake_httpx_client(healthy=False),
)
monkeypatch.setattr(
router_health.shutil,
"disk_usage",
lambda _p: MagicMock(free=500 * 1024 * 1024),
)
response = await async_client.get("/api/v1/health")
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["checks"]["ollama"]["status"] == "fail"
@pytest.mark.asyncio
async def test_low_disk_returns_503(
self,
async_client: AsyncClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(router_health, "engine", _make_mock_engine(healthy=True))
monkeypatch.setattr(
router_health.httpx,
"AsyncClient",
_make_fake_httpx_client(healthy=True),
)
monkeypatch.setattr(
router_health.shutil,
"disk_usage",
lambda _p: MagicMock(free=10), # way below 100 MB
)
response = await async_client.get("/api/v1/health")
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["checks"]["disk"]["status"] == "fail"