"""Integration-test fixtures using the running Docker Compose infrastructure. Run `docker compose up -d` before executing integration tests. Migrations run once per session; a service token is seeded so adapter-style endpoints can authenticate. """ from __future__ import annotations import os import subprocess import sys from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING import httpx import pytest import pytest_asyncio from asgi_lifespan import LifespanManager from sqlalchemy import text from contract_check.api.app import create_app from contract_check.core.config import get_settings from contract_check.core.db.session import create_session_factory from contract_check.core.tokens import hash_token if TYPE_CHECKING: pass pytestmark = pytest.mark.integration _BOT_SERVICE_TOKEN = "it-test-bot-token" _DB_URL = "postgresql+asyncpg://contract_check:contract_check@localhost:15432/contract_check" _AMQP_URL = "amqp://contract_check:contract_check@localhost:5672//" _S3_URL = "http://localhost:9000" def _set_env() -> None: env_vars = { "DATABASE_URL": _DB_URL, "RABBITMQ_URL": _AMQP_URL, "S3_ENDPOINT_URL": _S3_URL, "S3_ACCESS_KEY": "contract_check", "S3_SECRET_KEY": "contract_check", "S3_BUCKET": "contract-check-docs", "REDIS_URL": "redis://localhost:17379/0", "OLLAMA_HOST": "http://localhost", "OLLAMA_API_KEY": "test", "JWT_SECRET": "it-test-jwt-secret-not-for-production", "JWT_ALGORITHM": "HS256", "JWT_ACCESS_TTL_MINUTES": "1440", "TELEGRAM_BOT_TOKEN": "it-test-bot-token:it-test-secret", # httpx ASGITransport buffers the whole response — infinite SSE streams # can only complete via the max-stream deadline (which emits the # `timeout` event). Keep it short so streaming tests finish fast. "SSE_POLL_INTERVAL_SECONDS": "0.25", "SSE_MAX_STREAM_SECONDS": "5", } for k, v in env_vars.items(): os.environ[k] = v @pytest.fixture(scope="session") def infra() -> Iterator[dict[str, str]]: _set_env() get_settings.cache_clear() repo_root = Path(__file__).resolve().parents[2] subprocess.run( [sys.executable, "-m", "alembic", "-c", str(repo_root / "alembic.ini"), "upgrade", "head"], cwd=str(repo_root), env={**os.environ}, check=True, capture_output=False, ) factory = create_session_factory() async def _seed() -> None: async with factory() as session: await session.execute( text( "INSERT INTO service_tokens (name, token_hash, adapter) " "VALUES (:name, :hash, 'bot') " "ON CONFLICT (name) DO UPDATE SET " " token_hash = EXCLUDED.token_hash, revoked = FALSE" ), {"name": "bot-test", "hash": hash_token(_BOT_SERVICE_TOKEN)}, ) await session.commit() import asyncio asyncio.run(_seed()) yield { "database_url": _DB_URL, "rabbitmq_url": _AMQP_URL, "s3_endpoint_url": _S3_URL, "s3_access_key": "contract_check", "s3_secret_key": "contract_check", "token": _BOT_SERVICE_TOKEN, } get_settings.cache_clear() @pytest_asyncio.fixture async def client(infra: dict[str, str]) -> httpx.AsyncClient: get_settings.cache_clear() _set_env() get_settings() app = create_app() async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://test" ) as c: async with LifespanManager(app): yield c get_settings.cache_clear() @pytest.fixture def auth_header(infra: dict[str, str]) -> dict[str, str]: return {"Authorization": f"Bearer {infra['token']}"} async def user_token(client: httpx.AsyncClient, infra: dict[str, str], telegram_id: int) -> str: """Call /auth/telegram/bot and return a user JWT for the given telegram_id.""" r = await client.post( "/api/v1/auth/telegram/bot", json={"telegram_id": telegram_id}, headers={"Authorization": f"Bearer {infra['token']}"}, ) assert r.status_code == 200, f"auth failed: {r.status_code} {r.text}" return r.json()["access_token"] @pytest.fixture async def db_session(): _set_env() get_settings.cache_clear() factory = create_session_factory() async with factory() as session: yield session await session.close() get_settings.cache_clear()