""" Pytest configuration and fixtures for integration tests. Provides test database setup, client fixtures, and authentication helpers. """ import asyncio from typing import AsyncGenerator, Generator import pytest from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.pool import NullPool from app.main import app from app.core.database import Base, get_db from app.core.config import settings # Test database URL (separate from production) TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/patherly_test" @pytest.fixture(scope="session") def event_loop() -> Generator: """Create an instance of the default event loop for each test case.""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @pytest.fixture async def test_db() -> AsyncGenerator[AsyncSession, None]: """ Create a fresh database for each test function. This fixture: 1. Creates a test database engine 2. Creates all tables 3. Yields a session for the test 4. Drops all tables after the test """ # Create async engine for tests (with NullPool to avoid connection reuse issues) engine = create_async_engine( TEST_DATABASE_URL, poolclass=NullPool, echo=False ) # Create all tables async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Create async session maker async_session_maker = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) # Provide session to test async with async_session_maker() as session: yield session # Drop all tables after test async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest.fixture async def client(test_db: AsyncSession): """ Create an async HTTP client for testing API endpoints. Overrides the database dependency to use the test database. """ async def override_get_db(): yield test_db 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.fixture async def test_user(client): """ Create a test user and return their credentials. Returns: dict with email, password, and user_data """ user_data = { "email": "test@example.com", "password": "TestPassword123!", "name": "Test User", "role": "engineer" } response = await client.post("/api/v1/auth/register", json=user_data) assert response.status_code == 200 or response.status_code == 201 return { "email": user_data["email"], "password": user_data["password"], "user_data": response.json() } @pytest.fixture async def auth_headers(client, test_user): """ Get authentication headers for an authenticated test user. Returns: dict with Authorization header """ login_data = { "email": test_user["email"], "password": test_user["password"] } response = await client.post("/api/v1/auth/login/json", json=login_data) assert response.status_code == 200 token_data = response.json() return {"Authorization": f"Bearer {token_data['access_token']}"} @pytest.fixture async def test_tree(client, auth_headers): """ Create a test decision tree. Returns: dict with tree data """ tree_data = { "name": "Test Troubleshooting Tree", "description": "A test tree for integration tests", "category": "Testing", "tree_structure": { "id": "root", "type": "decision", "question": "Is this a test?", "options": [ {"id": "yes", "label": "Yes", "next_node_id": "solution1"}, {"id": "no", "label": "No", "next_node_id": "solution2"} ], "children": [ { "id": "solution1", "type": "solution", "title": "Test Confirmed", "description": "This is a test tree" }, { "id": "solution2", "type": "solution", "title": "Not a Test", "description": "This should not happen" } ] } } response = await client.post( "/api/v1/trees", json=tree_data, headers=auth_headers ) assert response.status_code == 201 return response.json() @pytest.fixture async def test_admin(client): """ Create a test admin user and return their credentials. Returns: dict with email, password, and user_data """ admin_data = { "email": "admin@example.com", "password": "AdminPassword123!", "name": "Test Admin", "role": "admin" } response = await client.post("/api/v1/auth/register", json=admin_data) assert response.status_code == 200 or response.status_code == 201 return { "email": admin_data["email"], "password": admin_data["password"], "user_data": response.json() } @pytest.fixture async def admin_auth_headers(client, test_admin): """ Get authentication headers for an authenticated admin user. Returns: dict with Authorization header """ login_data = { "email": test_admin["email"], "password": test_admin["password"] } response = await client.post("/api/v1/auth/login/json", json=login_data) assert response.status_code == 200 token_data = response.json() return {"Authorization": f"Bearer {token_data['access_token']}"}