"""Shared pytest fixtures. Tests always run with LLM_PROVIDER=mock, SEARCH_PROVIDER=mock, a throwaway SQLite DB, and Celery in eager mode - never against a paid provider or a real network target. """ from __future__ import annotations import os os.environ.setdefault("APP_ENV", "test") os.environ.setdefault("AUTH_MODE", "jwt") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_ciagent.db") os.environ.setdefault("REDIS_URL", "redis://localhost:6379/15") os.environ.setdefault("LLM_PROVIDER", "mock") os.environ.setdefault("SEARCH_PROVIDER", "mock") os.environ.setdefault("CELERY_TASK_ALWAYS_EAGER", "true") os.environ.setdefault("JWT_SECRET", "test-secret-please-change-32-characters") # No artificial per-domain delay in tests - the collector tests hit many # distinct mocked hostnames and shouldn't pay the real-world crawl-politeness cost. os.environ.setdefault("SCRAPER_DOMAIN_DELAY_SECONDS", "0") import asyncio from pathlib import Path import pytest from fastapi.testclient import TestClient from app.core.config import Settings, get_settings from app.db.base import Base from app.db.session import get_engine, get_sessionmaker from app.main import app @pytest.fixture(scope="session", autouse=True) def _setup_database(): # The DB is a throwaway file-based SQLite DB (aiosqlite doesn't support # ":memory:" across the multiple connections a test session opens), so it # must be deleted up front - otherwise companies/runs left behind by a # previous test session accumulate and pollute count-based assertions # (e.g. the scheduler "how many companies got enqueued" tests). db_path = Path("test_ciagent.db") db_path.unlink(missing_ok=True) async def _create() -> None: engine = get_engine() async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # auth_service.register() auto-promotes the first-ever registration # to admin (see its docstring) so a fresh deployment always has a # bootstrap path to admin access. Without this seed row, whichever # test happens to register a "plain" user first in this shared # session-wide DB would non-deterministically become an admin, # breaking every non-admin-should-get-403 test depending on # execution order. Seeding one admin up front keeps that dedicated # to test_first_registered_user_becomes_admin (which starts from a # genuinely empty users table), while every other test's "plain" # registration behaves exactly as before. from app.models.user import User async with get_sessionmaker()() as session: session.add( User( email="seed-admin@test.invalid", password_hash=None, display_name="Seed Admin", timezone="UTC", is_admin=True, email_verified=True, ) ) await session.commit() asyncio.run(_create()) yield @pytest.fixture(scope="session") def settings() -> Settings: return get_settings() @pytest.fixture() def client() -> TestClient: with TestClient(app) as c: yield c @pytest.fixture() async def db_session(): """Raw AsyncSession for tests that exercise services/repositories directly rather than through the HTTP API.""" session_factory = get_sessionmaker() async with session_factory() as session: yield session @pytest.fixture() def local_mode_client(settings: Settings) -> TestClient: """A client with AUTH_MODE forced to "local" via dependency override, independent of whatever AUTH_MODE the rest of the suite runs under - and with its TCP peer set to loopback, since the local-dev-user bypass (app.auth.dependencies.get_current_user) only fires for a genuinely local request, not merely AUTH_MODE=local. Starlette's TestClient defaults to a fake ("testclient", 50000) peer otherwise.""" local_settings = settings.model_copy(update={"auth_mode": "local"}) app.dependency_overrides[get_settings] = lambda: local_settings with TestClient(app, client=("127.0.0.1", 51234)) as c: yield c app.dependency_overrides.pop(get_settings, None)