"""Shared pytest fixtures for the yunke-server test suite.""" from __future__ import annotations from collections.abc import AsyncIterator, Awaitable, Callable import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, create_async_engine, ) from sqlalchemy.pool import StaticPool from app.api import deps from app.api.main import app from app.config import get_settings from app.db.base import Base @pytest.fixture(autouse=True) def reset_settings_cache(): """Ensure each test starts with a fresh Settings cache. Tests that mutate env vars and call `get_settings()` must also clear the `lru_cache` themselves; this fixture is a safety net that runs after every test so a cached `Settings` from one test never leaks into the next. """ yield get_settings.cache_clear() @pytest.fixture(autouse=True) def _disable_request_throttle(monkeypatch): """Zero out the conservative request-pace settings for the test suite. Production defaults (``request_min_interval=1.0``, ``request_jitter=0.8``) would make every mocked request sleep ~1-1.8s and balloon the suite's runtime. In tests we want the *behaviour* (header stealth, backoff, cap) without the wall-clock cost, so we pin the throttle env vars to 0. The defaults themselves are still asserted directly via ``Settings`` field introspection in ``test_config.py``. The cache is cleared on either side so the zeroed env is picked up the first time a test reads settings. """ get_settings.cache_clear() monkeypatch.setenv("REQUEST_MIN_INTERVAL", "0") monkeypatch.setenv("REQUEST_JITTER", "0") yield get_settings.cache_clear() @pytest.fixture async def make_client() -> AsyncIterator[ Callable[[Callable[[AsyncSession], Awaitable[None]]], Awaitable[AsyncClient]] ]: """Factory yielding an ``AsyncClient`` over a seeded in-memory SQLite DB. Mirrors the override pattern established in ``tests/test_api_customer.py`` (``StaticPool`` + single in-memory connection + ``get_session`` dependency override) but lets each test supply its own seed coroutine so the read-API test files don't each re-~40-line the boilerplate. Usage:: async def test_x(make_client): async def seed(session): session.add(Department(dept_id="d1", ...)) await session.commit() client = await make_client(seed) r = await client.get("/api/departments") ... The factory owns the engine + client lifetime: both are torn down (``aclose`` / ``dispose``) and the dependency override cleared when the test exits. """ created: list[tuple[AsyncClient, object]] = [] async def _factory( seed: Callable[[AsyncSession], Awaitable[None]], ) -> AsyncClient: engine = create_async_engine( "sqlite+aiosqlite:///:memory:", poolclass=StaticPool, connect_args={"check_same_thread": False}, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) SessionLocal = async_sessionmaker(engine, expire_on_commit=False) async with SessionLocal() as session: await seed(session) async def _override_session() -> AsyncIterator[AsyncSession]: async with SessionLocal() as s: yield s app.dependency_overrides[deps.get_session] = _override_session client = AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) created.append((client, engine)) return client try: yield _factory finally: for client, engine in created: await client.aclose() await engine.dispose() app.dependency_overrides.clear()