"""Test fixtures: in-memory SQLite DB, async test client, auth helpers. Design: - One AsyncEngine + session_factory per test (function-scoped) so each test has a fresh, isolated DB. - The FastAPI app's get_db dependency is overridden to use the same engine. - register/headers/login helpers talk to the HTTP API (integration tests). """ from __future__ import annotations from collections.abc import AsyncGenerator from typing import Any import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from app.core.db import Base, get_db from app.main import create_app @pytest_asyncio.fixture async def engine() -> AsyncGenerator[AsyncEngine, None]: """Per-test in-memory SQLite engine with schema created from metadata.""" eng = create_async_engine( "sqlite+aiosqlite:///:memory:", echo=False, future=True, ) async with eng.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield eng await eng.dispose() @pytest_asyncio.fixture async def session_factory( engine: AsyncEngine, ) -> async_sessionmaker[AsyncSession]: """Session factory bound to the test engine.""" return async_sessionmaker( bind=engine, expire_on_commit=False, class_=AsyncSession ) @pytest_asyncio.fixture async def client( session_factory: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[AsyncClient, None]: """httpx.AsyncClient wired to a fresh FastAPI app with the test DB injected.""" app = create_app() async def _override_get_db() -> AsyncGenerator[AsyncSession, None]: async with session_factory() as session: try: yield session except Exception: await session.rollback() raise app.dependency_overrides[get_db] = _override_get_db transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield c # === Convenience: register via API (covers bootstrap, user, token, headers) === @pytest_asyncio.fixture async def registered_user( client: AsyncClient, ) -> dict[str, Any]: """Register a bootstrap user via the API. Returns a dict with user, token, headers, email, password, name. Use this fixture (or the dependent `auth_headers`) for all auth-required tests. """ payload = { "email": "admin@test.com", "password": "TestPass123!", "name": "Test Admin", } resp = await client.post("/api/v1/auth/register", json=payload) assert resp.status_code == 201, f"register failed: {resp.status_code} {resp.text}" data = resp.json() token = data["access_token"] return { "user": data["user"], "token": token, "headers": {"Authorization": f"Bearer {token}"}, "email": payload["email"], "password": payload["password"], "name": payload["name"], } @pytest_asyncio.fixture async def auth_headers(registered_user: dict[str, Any]) -> dict[str, str]: """Authorization headers for the bootstrap user.""" return registered_user["headers"] @pytest_asyncio.fixture async def second_user( client: AsyncClient, registered_user: dict[str, Any], ) -> dict[str, Any]: """Register a second user (admin-only flow) and return its auth info.""" # Use admin headers to create another user admin_headers = registered_user["headers"] payload = { "email": "rep@test.com", "password": "RepPass123!", "name": "Test Rep", "role": "sales_rep", } resp = await client.post("/api/v1/users/", json=payload, headers=admin_headers) assert resp.status_code == 201, f"create user failed: {resp.status_code} {resp.text}" new_user = resp.json() # Log in as the new user to get a token login_resp = await client.post( "/api/v1/auth/login", data={"username": payload["email"], "password": payload["password"]}, ) assert login_resp.status_code == 200, f"login failed: {login_resp.status_code} {login_resp.text}" token = login_resp.json()["access_token"] return { "user": new_user, "token": token, "headers": {"Authorization": f"Bearer {token}"}, "email": payload["email"], "password": payload["password"], "name": payload["name"], }