From d71dd1bd0c273ea155f5fdf1a128a73f86031d04 Mon Sep 17 00:00:00 2001 From: Ali Zarinkolah Date: Sun, 16 Aug 2026 11:54:14 +0330 Subject: [PATCH] test: add unit test suite for config, health checks, and lifespan wiring --- tests/__init__.py | 0 tests/conftest.py | 37 ++++++++++++++++++++++++++ tests/e2e/__init__.py | 0 tests/fakes.py | 5 ++++ tests/integration/__init__.py | 0 tests/integration/minio/__init__.py | 0 tests/integration/postgres/__init__.py | 0 tests/integration/qdrant/__init__.py | 0 tests/support/__init__.py | 0 tests/support/assertions.py | 1 + tests/support/factories.py | 1 + tests/unit/__init__.py | 0 tests/unit/agent/__init__.py | 0 tests/unit/application/__init__.py | 0 tests/unit/bootstrap/__init__.py | 0 tests/unit/bootstrap/test_lifespan.py | 19 +++++++++++++ tests/unit/test_config.py | 27 +++++++++++++++++++ tests/unit/test_healthz.py | 11 ++++++++ tests/unit/test_readyz.py | 17 ++++++++++++ 19 files changed, 118 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/fakes.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/minio/__init__.py create mode 100644 tests/integration/postgres/__init__.py create mode 100644 tests/integration/qdrant/__init__.py create mode 100644 tests/support/__init__.py create mode 100644 tests/support/assertions.py create mode 100644 tests/support/factories.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/agent/__init__.py create mode 100644 tests/unit/application/__init__.py create mode 100644 tests/unit/bootstrap/__init__.py create mode 100644 tests/unit/bootstrap/test_lifespan.py create mode 100644 tests/unit/test_config.py create mode 100644 tests/unit/test_healthz.py create mode 100644 tests/unit/test_readyz.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..16c5aa6 --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fakes.py b/tests/fakes.py new file mode 100644 index 0000000..b9e09a6 --- /dev/null +++ b/tests/fakes.py @@ -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. +""" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/minio/__init__.py b/tests/integration/minio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/postgres/__init__.py b/tests/integration/postgres/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/qdrant/__init__.py b/tests/integration/qdrant/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/support/__init__.py b/tests/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/support/assertions.py b/tests/support/assertions.py new file mode 100644 index 0000000..2aa3b40 --- /dev/null +++ b/tests/support/assertions.py @@ -0,0 +1 @@ +"""Custom test assertions, populated as later phases need them.""" diff --git a/tests/support/factories.py b/tests/support/factories.py new file mode 100644 index 0000000..e2792ed --- /dev/null +++ b/tests/support/factories.py @@ -0,0 +1 @@ +"""Object builders for test fixtures, populated as later phases need them.""" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/agent/__init__.py b/tests/unit/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/application/__init__.py b/tests/unit/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/bootstrap/__init__.py b/tests/unit/bootstrap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/bootstrap/test_lifespan.py b/tests/unit/bootstrap/test_lifespan.py new file mode 100644 index 0000000..7d4fdb0 --- /dev/null +++ b/tests/unit/bootstrap/test_lifespan.py @@ -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 diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..ea462c3 --- /dev/null +++ b/tests/unit/test_config.py @@ -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 diff --git a/tests/unit/test_healthz.py b/tests/unit/test_healthz.py new file mode 100644 index 0000000..f348c87 --- /dev/null +++ b/tests/unit/test_healthz.py @@ -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"} diff --git a/tests/unit/test_readyz.py b/tests/unit/test_readyz.py new file mode 100644 index 0000000..b054a4f --- /dev/null +++ b/tests/unit/test_readyz.py @@ -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, + }