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