dotb_ai-platform/tests/test_health.py
2026-09-14 09:57:12 +07:00

156 lines
5.1 KiB
Python

import asyncio
from importlib.metadata import version
from unittest.mock import MagicMock
import pytest
from httpx import ASGITransport, AsyncClient
from pydantic import ValidationError
from lead_scoring import __version__
from lead_scoring.config import Settings
from lead_scoring.db.session import check_db_health
from lead_scoring.main import create_app
from lead_scoring.schemas.health import HealthResponse
@pytest.mark.asyncio
async def test_health_endpoint_returns_ok_when_lifespan_healthy():
settings = Settings(database_url="postgresql+asyncpg://user:pass@localhost:5432/testdb")
app = create_app(settings=settings)
# Use lifespan context and mock engine probe
async with app.router.lifespan_context(app):
# Lifespan initializes app.state.engine and session_factory
assert hasattr(app.state, "engine")
mock_engine = MagicMock()
class HealthyAsyncCM:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def execute(self, statement):
return 1
mock_engine.connect.return_value = HealthyAsyncCM()
app.state.engine = mock_engine
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/health")
assert response.status_code == 200
data = response.json()
validated = HealthResponse.model_validate(data)
assert validated.status == "ok"
assert validated.database == "connected"
assert validated.version == __version__
@pytest.mark.asyncio
async def test_health_endpoint_returns_503_when_database_unhealthy():
settings = Settings(database_url="postgresql+asyncpg://user:pass@localhost:5432/testdb")
app = create_app(settings=settings)
mock_engine = MagicMock()
class FailingAsyncCM:
async def __aenter__(self):
raise ConnectionRefusedError("Database down")
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
mock_engine.connect.return_value = FailingAsyncCM()
app.state.engine = mock_engine
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/health")
assert response.status_code == 503
data = response.json()
validated = HealthResponse.model_validate(data)
assert validated.status == "unhealthy"
assert validated.database == "unreachable"
assert validated.version == __version__
@pytest.mark.asyncio
async def test_health_endpoint_returns_503_when_engine_unconfigured():
settings = Settings(database_url="postgresql+asyncpg://user:pass@localhost:5432/testdb")
app = create_app(settings=settings)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
app.state.engine = None
response = await client.get("/health")
assert response.status_code == 503
data = response.json()
validated = HealthResponse.model_validate(data)
assert validated.status == "unhealthy"
assert validated.database == "unconfigured"
@pytest.mark.asyncio
async def test_check_db_health_timeout():
mock_engine = MagicMock()
class HangingAsyncCM:
async def __aenter__(self):
await asyncio.sleep(5.0)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
mock_engine.connect.return_value = HangingAsyncCM()
is_healthy = await check_db_health(mock_engine, timeout=0.1)
assert is_healthy is False
def test_settings_url_normalization():
s1 = Settings(database_url="postgres://user:pass@host:5432/db")
assert s1.database_url == "postgresql+asyncpg://user:pass@host:5432/db"
s2 = Settings(database_url="postgresql://user:pass@host:5432/db")
assert s2.database_url == "postgresql+asyncpg://user:pass@host:5432/db"
s3 = Settings(database_url="postgresql+asyncpg://user:pass@host:5432/db")
assert s3.database_url == "postgresql+asyncpg://user:pass@host:5432/db"
def test_settings_timeout_must_be_positive():
with pytest.raises(ValidationError):
Settings(db_health_timeout_seconds=0)
with pytest.raises(ValidationError):
Settings(db_health_timeout_seconds=-1.0)
def test_version_matches_distribution_metadata():
installed_version = version("lead-scoring")
assert __version__ == installed_version
settings = Settings()
assert settings.app_version == installed_version
health = HealthResponse()
assert health.version == installed_version
def test_openapi_declares_503_health_response():
app = create_app()
openapi_schema = app.openapi()
health_responses = openapi_schema["paths"]["/health"]["get"]["responses"]
assert "200" in health_responses
assert "503" in health_responses
def test_engine_factory_enforces_hide_parameters():
from lead_scoring.db.session import create_engine_from_url
engine = create_engine_from_url(
"postgresql+asyncpg://user:pass@localhost:5432/testdb",
echo=True,
)
assert engine.sync_engine.hide_parameters is True