""" Integration test fixtures — no mocks, real services. All services (PostgreSQL, Redis, Hatchet, Garage, server, workers) are expected to be running via docker-compose.integration.yml. """ import asyncio import os from pathlib import Path import boto3 import httpx import pytest import pytest_asyncio from sqlalchemy.ext.asyncio import create_async_engine SERVER_URL = os.environ.get("SERVER_URL", "http://server:1250") GARAGE_ENDPOINT = os.environ.get("GARAGE_ENDPOINT", "http://garage:3900") DATABASE_URL = os.environ.get( "DATABASE_URL_ASYNC", os.environ.get( "DATABASE_URL", "postgresql+asyncpg://reflector:reflector@postgres:5432/reflector", ), ) GARAGE_KEY_ID = os.environ.get("TRANSCRIPT_STORAGE_AWS_ACCESS_KEY_ID", "") GARAGE_KEY_SECRET = os.environ.get("TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY", "") BUCKET_NAME = "reflector-media" @pytest_asyncio.fixture async def api_client(): """HTTP client pointed at the running server.""" async with httpx.AsyncClient( base_url=f"{SERVER_URL}/v1", timeout=httpx.Timeout(30.0), ) as client: yield client @pytest.fixture(scope="session") def s3_client(): """Boto3 S3 client pointed at Garage.""" return boto3.client( "s3", endpoint_url=GARAGE_ENDPOINT, aws_access_key_id=GARAGE_KEY_ID, aws_secret_access_key=GARAGE_KEY_SECRET, region_name="garage", ) @pytest_asyncio.fixture async def db_engine(): """SQLAlchemy async engine for direct DB operations.""" engine = create_async_engine(DATABASE_URL) yield engine await engine.dispose() @pytest.fixture(scope="session") def test_records_dir(): """Path to the test audio files directory.""" return Path(__file__).parent.parent / "records" @pytest.fixture(scope="session") def bucket_name(): """S3 bucket name used for integration tests.""" return BUCKET_NAME async def _poll_transcript_status( client: httpx.AsyncClient, transcript_id: str, target: str | tuple[str, ...], error: str = "error", max_wait: int = 300, interval: int = 3, ) -> dict: """ Poll GET /transcripts/{id} until status matches target or error. target can be a single status string or a tuple of acceptable statuses. Returns the transcript dict on success, raises on timeout or error status. """ targets = (target,) if isinstance(target, str) else target elapsed = 0 status = None while elapsed < max_wait: resp = await client.get(f"/transcripts/{transcript_id}") resp.raise_for_status() data = resp.json() status = data.get("status") if status in targets: return data if status == error: raise AssertionError( f"Transcript {transcript_id} reached error status: {data}" ) await asyncio.sleep(interval) elapsed += interval raise TimeoutError( f"Transcript {transcript_id} did not reach status '{target}' " f"within {max_wait}s (last status: {status})" ) @pytest_asyncio.fixture def poll_transcript_status(): """Returns the poll_transcript_status async helper function.""" return _poll_transcript_status