Files
chatbot_v3/tests/conftest.py
Ali Zarinkolah 2c72688440 refactor(test): share container fixtures across the integration and e2e suites
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>
2026-08-20 22:26:02 +03:30

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