"""Pytest fixtures for async DB testing with PostgreSQL.""" from typing import AsyncGenerator import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy import delete from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, create_async_engine, ) from app.database import Base, get_db from app.main import app from app.models.user import User, UserRole from app.services.auth_service import hash_password TEST_DATABASE_URL = ( "postgresql+asyncpg://erp_test_user:testpass@localhost:5432/erp_test" ) @pytest.fixture def event_loop(): """Create a fresh event loop per test for asyncpg compatibility.""" import asyncio loop = asyncio.new_event_loop() yield loop loop.close() @pytest_asyncio.fixture async def test_engine(): """Create a fresh async engine per test.""" engine = create_async_engine(TEST_DATABASE_URL, echo=False, pool_pre_ping=True) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture async def test_session_factory(test_engine): """Session factory bound to the test engine.""" return async_sessionmaker( test_engine, class_=AsyncSession, expire_on_commit=False, ) @pytest_asyncio.fixture async def db_session(test_session_factory) -> AsyncGenerator[AsyncSession, None]: """Provide an async DB session for tests.""" async with test_session_factory() as session: try: yield session finally: await session.close() @pytest_asyncio.fixture async def admin_user(db_session: AsyncSession) -> User: """Create a test admin user.""" user = User( email="admin@test.com", password_hash=hash_password("Admin12345!"), full_name="Test Admin", role=UserRole.admin, language="de", is_active=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest_asyncio.fixture async def verkaeufer_user(db_session: AsyncSession) -> User: """Create a test verkaeufer (non-admin) user.""" user = User( email="verkaeufer@test.com", password_hash=hash_password("Verk12345!"), full_name="Test Verkaeufer", role=UserRole.verkaeufer, language="en", is_active=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest_asyncio.fixture async def inactive_user(db_session: AsyncSession) -> User: """Create a deactivated test user.""" user = User( email="inactive@test.com", password_hash=hash_password("Inactive123!"), full_name="Test Inactive", role=UserRole.admin, language="de", is_active=False, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user def _get_test_token(user: User) -> str: """Generate an access token for a test user.""" from app.utils.jwt import create_access_token role_val = user.role.value if isinstance(user.role, UserRole) else str(user.role) return create_access_token( user_id=str(user.id), role=role_val, email=user.email, lang=user.language, ) @pytest_asyncio.fixture async def admin_token(admin_user: User) -> str: """Access token for admin user.""" return _get_test_token(admin_user) @pytest_asyncio.fixture async def verkaeufer_token(verkaeufer_user: User) -> str: """Access token for verkaeufer user.""" return _get_test_token(verkaeufer_user) @pytest_asyncio.fixture async def client(test_session_factory) -> AsyncGenerator[AsyncClient, None]: """Async HTTP test client with DB session override.""" async def _override_get_db(): async with test_session_factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() app.dependency_overrides[get_db] = _override_get_db transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac app.dependency_overrides.clear() @pytest_asyncio.fixture async def admin_client(client: AsyncClient, admin_token: str) -> AsyncClient: """HTTP client authenticated as admin.""" client.headers.update({"Authorization": f"Bearer {admin_token}"}) return client @pytest_asyncio.fixture async def verkaeufer_client(client: AsyncClient, verkaeufer_token: str) -> AsyncClient: """HTTP client authenticated as verkaeufer (non-admin).""" client.headers.update({"Authorization": f"Bearer {verkaeufer_token}"}) return client