test: add unit test suite for config, health checks, and lifespan wiring

This commit is contained in:
2026-08-16 11:54:14 +03:30
parent 3c660de093
commit d71dd1bd0c
19 changed files with 118 additions and 0 deletions

0
tests/__init__.py Normal file
View File

37
tests/conftest.py Normal file
View File

@@ -0,0 +1,37 @@
from collections.abc import AsyncIterator
import pytest
import pytest_asyncio
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from src.config import Settings
from src.main import create_app
@pytest.fixture
def settings() -> Settings:
return Settings(
postgres={"host": "127.0.0.1", "port": 1},
minio={"endpoint": "127.0.0.1:1"},
ingestion={"timeout_seconds": 1.0},
qdrant={"url": "http://127.0.0.1:1"},
app={"readiness_check_timeout_seconds": 0.5},
)
@pytest.fixture
def app(settings: Settings) -> FastAPI:
return create_app(settings)
@pytest_asyncio.fixture
async def client(app: FastAPI) -> AsyncIterator[AsyncClient]:
async with (
LifespanManager(app) as manager,
AsyncClient(
transport=ASGITransport(app=manager.app), base_url="http://test"
) as async_client,
):
yield async_client

0
tests/e2e/__init__.py Normal file
View File

5
tests/fakes.py Normal file
View File

@@ -0,0 +1,5 @@
"""Hand-written fakes for narrow application-owned ports.
No application ports exist yet (Phase 1 only wires infrastructure client
lifecycle). Fakes are added here as ports are introduced in later phases.
"""

View File

View File

View File

View File

View File

View File

@@ -0,0 +1 @@
"""Custom test assertions, populated as later phases need them."""

View File

@@ -0,0 +1 @@
"""Object builders for test fixtures, populated as later phases need them."""

0
tests/unit/__init__.py Normal file
View File

View File

View File

View File

View File

@@ -0,0 +1,19 @@
import pytest
from fastapi import FastAPI
from httpx import AsyncClient
from src.config import Settings
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
async def test_lifespan_starts_and_stops_without_docker(client: AsyncClient) -> None:
response = await client.get("/healthz")
assert response.status_code == 200
async def test_lifespan_binds_resources_to_app_state(app: FastAPI, client: AsyncClient) -> None:
await client.get("/healthz")
resources = app.state.resources
assert isinstance(resources.settings, Settings)
assert resources.minio_client is not None

27
tests/unit/test_config.py Normal file
View File

@@ -0,0 +1,27 @@
from pathlib import Path
import pytest
from src.config import Settings
pytestmark = pytest.mark.unit
def test_settings_loads_defaults_without_env_file() -> None:
settings = Settings(_env_file=None)
assert settings.postgres.port == 5433
assert settings.minio.bucket == "chatbot-source-files"
assert settings.ingestion.max_concurrency == 4
assert settings.qdrant.url == "http://127.0.0.1:6343"
def test_settings_parses_env_example() -> None:
env_example = Path(__file__).parent.parent.parent / ".env.example"
settings = Settings(_env_file=env_example)
assert settings.postgres.host == "127.0.0.1"
assert settings.minio.endpoint == "127.0.0.1:9100"
assert settings.ingestion.embed_batch_size == 128
assert settings.qdrant.api_key is None

View File

@@ -0,0 +1,11 @@
import pytest
from httpx import AsyncClient
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
async def test_healthz_returns_ok_status(client: AsyncClient) -> None:
response = await client.get("/healthz")
assert response.status_code == 200
assert response.json() == {"status": "ok"}

17
tests/unit/test_readyz.py Normal file
View File

@@ -0,0 +1,17 @@
import pytest
from httpx import AsyncClient
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
async def test_readyz_reports_all_dependencies_not_ready_without_docker(
client: AsyncClient,
) -> None:
response = await client.get("/readyz")
assert response.status_code == 503
assert response.json() == {
"postgres": False,
"minio": False,
"qdrant": False,
}