"""Shared test fixtures and environment defaults. Ensures the SPEC ยง14 env vars exist even when tests run outside the conda activate hook (e.g. CI), so ``app.config`` imports cleanly. """ from __future__ import annotations import base64 import os os.environ.setdefault("APP_DOMAIN", "test.webmail.local") os.environ.setdefault( "DATABASE_URL", "postgresql+asyncpg://webmail:webmail_dev@127.0.0.1:5433/webmail_dev" ) # Force a dedicated Redis DB for tests (index 15) so the suite is isolated from the # dev app's Redis (DB 0) and can be flushed between tests without touching real data. os.environ["REDIS_URL"] = "redis://127.0.0.1:6380/15" os.environ.setdefault("MASTER_KEY", base64.b64encode(b"\x00" * 32).decode()) os.environ.setdefault("BLOB_ROOT", "./var/blobs-test") from collections.abc import AsyncIterator # noqa: E402 import pytest_asyncio # noqa: E402 from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine # noqa: E402 from sqlalchemy.pool import NullPool # noqa: E402 from app.config import get_settings # noqa: E402 @pytest_asyncio.fixture(autouse=True) async def _reset_global_engine() -> AsyncIterator[None]: """Dispose process-wide async resources after each test. Each test runs on its own event loop; disposing here ensures the next test (and any code using the global engine/redis, e.g. the search backend) rebinds to its own loop rather than reusing a closed one. """ from app.redis_client import get_redis # Flush the dedicated test Redis DB so rate-limit / session / token state does # not leak between tests (each test starts from a clean slate). await get_redis().flushdb() yield from app.db import dispose_engine from app.redis_client import close_redis await dispose_engine() await close_redis() @pytest_asyncio.fixture async def db_session() -> AsyncIterator[AsyncSession]: """Rollback-per-test session (nothing persists to the dev database).""" engine = create_async_engine(get_settings().database_url, poolclass=NullPool) conn = await engine.connect() trans = await conn.begin() session = AsyncSession(bind=conn, expire_on_commit=False) try: yield session finally: await session.close() await trans.rollback() await conn.close() await engine.dispose()