"""통합 테스트(D그룹) 공용 fixture — 격리된 임시 DB 전용. H4: `test_multi_tenant.py`와 `test_kis_credentials.py`가 각자 정의하던 이벤트 루프/ lifespan/스키마 생성/DB 가드/HTTP 클라이언트 fixture를 이 파일로 통합한다. 두 파일 모두 이 conftest를 통해 동일한 가드를 거치므로, `tests/integration` 디렉터리를 통째로 실행해도(예: `pytest tests/integration -m integration`) users 테이블 등 스키마가 없어 실패하는 일이 없다. 가드: `settings.DATABASE_URL`의 DB 이름이 `_test`로 끝나지 않으면 즉시 `pytest.exit()` — 라이브 DB(예: 포트 15433의 `autostock_db`)를 실수로 겨냥한 실행을 세션 시작 단계에서 차단한다. `os.environ["DATABASE_URL"]`만 보던 이전 가드는 env 미설정 시 `settings.DATABASE_URL` 기본값(라이브 가능성)으로 조용히 새는 결함이 있었다(구 검수 H4). 실행 예시 (임시 Postgres 컨테이너 안에서): docker run --rm --network host \ -v $(pwd)/backend:/app -w /app \ -e DATABASE_URL=postgresql://autostock:test@127.0.0.1:15499/autostock_test \ -e SECRET_KEY=test-secret-key-for-integration-tests-32chars \ -e CREDENTIALS_ENCRYPTION_KEY=test-cred-key-for-integration-tests-32c \ -e BOOTSTRAP_ADMIN_PASSWORD=bootstrap-pw \ auto-stock-backend:latest \ bash -lc "pip install -q fakeredis pytest-asyncio httpx && python -m pytest tests/integration -m integration -v" """ import asyncio import os import sys from contextlib import asynccontextmanager sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) import httpx import pytest import pytest_asyncio from httpx import ASGITransport from app.core.config import settings # noqa: E402 # ── 임시 DB 강제 확인 — DB 이름이 `_test`로 끝나지 않으면 세션 시작 자체를 중단 ── _db_url = settings.DATABASE_URL or "" _db_name = _db_url.rsplit("/", 1)[-1].split("?", 1)[0] if not _db_name.endswith("_test"): pytest.exit( f"settings.DATABASE_URL의 DB 이름이 '_test'로 끝나지 않습니다(현재: {_db_name!r}). " "통합 테스트는 반드시 임시 DB(예: autostock_test)를 대상으로 실행해야 합니다.", returncode=1, ) @pytest.fixture(scope="session") def event_loop(): """세션 전체가 동일 이벤트 루프를 공유하도록 강제 (asyncpg 풀 재사용). 기본(함수 스코프) 이벤트 루프를 쓰면 테스트마다 새 루프가 생성되는데, 앱 임포트 시 한 번 만들어진 asyncpg 커넥션 풀(`AsyncSessionLocal`)의 유휴 커넥션이 이전 루프에 묶여 있어 다음 테스트에서 "Event loop is closed" 오류가 난다. """ loop = asyncio.new_event_loop() yield loop loop.close() @asynccontextmanager async def _noop_lifespan(app): """실거래 스케줄러/db_writer_worker/부트스트랩 자동 기동 방지 — 필요한 것만 테스트가 직접 기동.""" yield from app.core.database import Base, engine # noqa: E402 from app.main import app as fastapi_app # noqa: E402 fastapi_app.router.lifespan_context = _noop_lifespan @pytest_asyncio.fixture(scope="session", autouse=True) async def _create_schema(): """임시 DB이므로 alembic 대신 `Base.metadata.create_all`로 스키마를 통째로 생성.""" async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield @pytest_asyncio.fixture async def client(): transport = ASGITransport(app=fastapi_app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: yield ac def _auth(token: str) -> dict: return {"X-API-Key": token}