"""Shared test fixtures for all test categories.""" import asyncio import os from typing import AsyncGenerator, Generator from unittest.mock import patch import pytest import pytest_asyncio from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine # Set test environment BEFORE importing app modules os.environ["APP_ENV"] = "testing" os.environ["SECRET_KEY"] = "test-secret-key-for-testing-only-do-not-use-in-production" os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:" from src.config import Settings from src.models.base import Base from src.main import app from src.auth.dependencies import get_db_session @pytest.fixture def test_client() -> Generator[TestClient, None, None]: """Provide a FastAPI test client with SQLite database.""" # Create a single engine for this test engine = create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, ) # Create tables async def init_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) asyncio.run(init_db()) async def override_get_db_session() -> AsyncGenerator[AsyncSession, None]: async with async_sessionmaker(engine, expire_on_commit=False)() as session: yield session # Override the dependency app.dependency_overrides[get_db_session] = override_get_db_session # Patch startup events to prevent PostgreSQL connection attempts with patch("src.main.init_database") as mock_init: mock_init.return_value = True try: with TestClient(app) as client: yield client finally: # Clean up overrides app.dependency_overrides.pop(get_db_session, None) asyncio.run(engine.dispose()) @pytest_asyncio.fixture async def db_session(test_client) -> AsyncGenerator[AsyncSession, None]: """Provide an async database session for unit tests.""" # Get the override function from the test_client fixture override_fn = app.dependency_overrides.get(get_db_session) if override_fn: gen = override_fn() session = await gen.asend(None) try: yield session finally: await gen.aclose() else: # Fallback: create a new engine and session engine = create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async with async_sessionmaker(engine, expire_on_commit=False)() as session: yield session await engine.dispose() @pytest.fixture def authenticated_client(test_client) -> Generator[TestClient, None, None]: """Provide an authenticated test client with a test user.""" import uuid from src.auth.session import create_session_cookie from src.models.user import User user_id = str(uuid.uuid4()) settings = Settings() # Create user in database using the same engine as test_client # We need to access the engine from the test_client fixture # Since we can't easily do that, we'll create the user via API call # But we need the user to exist before any API calls # So we need to create the user using the overridden dependency async def create_test_user(): # Get the override function override_fn = app.dependency_overrides.get(get_db_session) if override_fn: gen = override_fn() session = await gen.asend(None) try: user = User( id=uuid.UUID(user_id), email="test@headquarter.local", name="Test User", authentik_id=f"authentik-{user_id}", avatar_url=None, ) session.add(user) await session.commit() finally: await gen.aclose() asyncio.run(create_test_user()) # Create session cookie session_cookie = create_session_cookie( settings=settings, user_id=user_id, ) # Set cookie on client test_client.cookies.set("session", session_cookie) yield test_client @pytest.fixture def test_project_and_repo(authenticated_client) -> tuple[str, str]: """Create a project and repository directly in the database.""" import uuid from src.models.project import Project from src.models.git_repository import GitRepository project_id = uuid.uuid4() repo_id = uuid.uuid4() user_id = None # Get user ID from session async def get_user_id(): nonlocal user_id from src.auth.session import decode_session_cookie settings = Settings() session_cookie = authenticated_client.cookies.get("session") if session_cookie: session = decode_session_cookie(settings=settings, cookie_value=session_cookie) if session: user_id = uuid.UUID(session["user_id"]) asyncio.run(get_user_id()) if not user_id: raise RuntimeError("Could not get user ID from authenticated client") async def create_project_and_repo(): override_fn = app.dependency_overrides.get(get_db_session) if override_fn: gen = override_fn() session = await gen.asend(None) try: project = Project( id=project_id, name="test-project", description="Test project", owner_id=user_id, ) session.add(project) repo = GitRepository( id=repo_id, name="test-repo", path="/tmp/test-repo", project_id=project_id, owner_id=user_id, remote_url="https://github.com/test/repo.git", ) session.add(repo) await session.commit() finally: await gen.aclose() asyncio.run(create_project_and_repo()) return str(project_id), str(repo_id) @pytest.fixture def admin_client(test_client) -> Generator[TestClient, None, None]: """Provide an authenticated test client with an admin user.""" import uuid from src.auth.session import create_session_cookie from src.models.user import User user_id = str(uuid.uuid4()) settings = Settings() async def create_admin_user(): override_fn = app.dependency_overrides.get(get_db_session) if override_fn: gen = override_fn() session = await gen.asend(None) try: user = User( id=uuid.UUID(user_id), email="admin@headquarter.local", name="Admin User", authentik_id=f"authentik-admin-{user_id}", avatar_url=None, is_admin=True, ) session.add(user) await session.commit() finally: await gen.aclose() asyncio.run(create_admin_user()) # Create session cookie session_cookie = create_session_cookie( settings=settings, user_id=user_id, ) # Set cookie on client test_client.cookies.set("session", session_cookie) yield test_client