Why: - tests/e2e/ cannot reach fixtures defined in a per-boundary conftest, and `pytest_plugins` is only honoured in the root conftest. Changes: - move the Postgres/MinIO/Qdrant container fixtures into tests/support/containers.py and register it as a root plugin - fold MinIO bucket creation into `minio_settings`; an autouse fixture in a globally registered plugin would pull a container into unit runs - add a `postgres_settings` fixture so a component can be built from it directly Impact: - no behavior change; `pytest -m unit` still needs no Docker Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
from collections.abc import AsyncIterator, Iterator
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
import structlog
|
|
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
|
|
|
|
# Disposable Postgres/MinIO/Qdrant containers (ADR-0016). Registered here
|
|
# because `pytest_plugins` is only honoured in the root conftest, and both
|
|
# `tests/integration/*/` and `tests/e2e/` need the same containers. The
|
|
# fixtures are session-scoped but lazy, so unit runs still need no Docker.
|
|
pytest_plugins = ["tests.support.containers"]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_structlog_after_test() -> Iterator[None]:
|
|
"""Undo any real `configure_logging()` call before the next test runs.
|
|
|
|
Any test that exercises the app's lifespan (directly, or via the `client`/
|
|
`api_client` fixtures below and in `test_domains_api.py`) calls the real
|
|
`configure_logging()`, which mutates *global* structlog/stdlib state --
|
|
including `cache_logger_on_first_use=True`. Left in place, that setting
|
|
silently breaks `structlog.testing.capture_logs()` in unrelated tests
|
|
later in the same pytest process: a module-level
|
|
`logger = structlog.get_logger(__name__)` cached under the real config no
|
|
longer routes through `capture_logs()`'s temporary processor swap, so
|
|
assertions on captured events see nothing (ADR-0016: isolate per test --
|
|
this generalizes to global config mutations, not just data).
|
|
"""
|
|
yield
|
|
structlog.reset_defaults()
|
|
|
|
|
|
@pytest.fixture
|
|
def settings() -> Settings:
|
|
# Every external dependency points at a closed port so unit tests never
|
|
# reach real infrastructure. For the embedders this matters twice over:
|
|
# the real defaults are a colleague's Ollama box and OpenAI's paid API,
|
|
# and ADR-0016 forbids routine runs calling either. Connection-refused is
|
|
# immediate, so the lifespan's fail-soft warm-up costs nothing here --
|
|
# and these tests passing at all is what proves it is fail-soft.
|
|
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},
|
|
embedding={
|
|
"nomic": {"base_url": "http://127.0.0.1:1/v1", "timeout_seconds": 0.5},
|
|
"openai": {"base_url": "http://127.0.0.1:1/v1", "timeout_seconds": 0.5},
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def app(settings: Settings) -> FastAPI:
|
|
return create_app(settings)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_real_logging_configuration(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Prevent the app lifespan from calling the real `configure_logging()`.
|
|
|
|
It sets `cache_logger_on_first_use=True` (ADR-0011), which permanently
|
|
monkeypatches the `.bind` method on whichever module-level
|
|
`logger = structlog.get_logger(__name__)` instance is used first --
|
|
`structlog.reset_defaults()` only resets *global* config, not that
|
|
per-instance mutation, so real configuration leaking into one test would
|
|
silently break `structlog.testing.capture_logs()` in every test that runs
|
|
afterward in the same process (ADR-0016: isolate per test). Tests that
|
|
spin up the full app via `LifespanManager` (`client`, `api_client`) are
|
|
testing HTTP behavior, not logging output, so they don't need it for
|
|
real.
|
|
"""
|
|
monkeypatch.setattr("src.bootstrap.lifespan.configure_logging", lambda *a, **k: None)
|
|
|
|
|
|
@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
|