"""Author RAG — Pytest Shared Fixtures. Provides async DB, fakeredis, mocked OpenAI, and mocked Stripe for all test modules. Zero real network calls in tests. """ import pytest import pytest_asyncio from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from unittest.mock import AsyncMock, MagicMock, patch from app.models.base import Base from app.main import create_app from app.dependencies import get_db, get_redis # ── In-Memory SQLite DB ─────────────────────────────────────────────────────── @pytest_asyncio.fixture(scope="function") async def db_engine(): engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture(scope="function") async def db_session(db_engine): factory = async_sessionmaker(db_engine, expire_on_commit=False) async with factory() as session: yield session # ── Fake Redis ──────────────────────────────────────────────────────────────── @pytest.fixture def fake_redis(): try: import fakeredis.aioredis as fakeredis return fakeredis.FakeRedis(decode_responses=True) except ImportError: redis = MagicMock() redis.get = AsyncMock(return_value=None) redis.set = AsyncMock(return_value=True) redis.setex = AsyncMock(return_value=True) redis.delete = AsyncMock(return_value=1) redis.exists = AsyncMock(return_value=0) redis.incr = AsyncMock(return_value=1) redis.expire = AsyncMock(return_value=True) return redis # ── FastAPI Test Client ─────────────────────────────────────────────────────── @pytest_asyncio.fixture(scope="function") async def client(db_session, fake_redis): app = create_app() async def override_db(): yield db_session async def override_redis(): return fake_redis app.dependency_overrides[get_db] = override_db app.dependency_overrides[get_redis] = override_redis async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: yield c app.dependency_overrides.clear() # ── Mock OpenAI ─────────────────────────────────────────────────────────────── @pytest.fixture(autouse=True) def mock_openai(): mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = "This is a mock AI response." mock_response.usage.total_tokens = 50 with patch("openai.AsyncOpenAI") as mock_client: instance = mock_client.return_value instance.chat.completions.create = AsyncMock(return_value=mock_response) instance.embeddings.create = AsyncMock(return_value=MagicMock( data=[MagicMock(embedding=[0.1] * 1536)] )) yield mock_client # ── Mock Stripe ─────────────────────────────────────────────────────────────── @pytest.fixture def mock_stripe(): mock_session = MagicMock() mock_session.id = "cs_test_mock_session" mock_session.url = "https://checkout.stripe.com/test" mock_session.payment_status = "paid" mock_session.customer_details = MagicMock(email="test@example.com", name="Test Author") mock_session.customer = "cus_test_123" mock_session.subscription = "sub_test_123" mock_session.metadata = {"plan_id": "starter"} with patch("stripe.checkout.Session.create", return_value=mock_session), \ patch("stripe.checkout.Session.retrieve", return_value=mock_session), \ patch("stripe.Webhook.construct_event") as mock_webhook, \ patch("stripe.Subscription.retrieve"), \ patch("stripe.billing_portal.Session.create"): mock_webhook.return_value = MagicMock(id="evt_test_123", type="checkout.session.completed") yield {"session": mock_session, "webhook": mock_webhook} # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest_asyncio.fixture async def test_author(db_session): from datetime import datetime, timedelta, timezone from app.models.user import User from app.models.client_access import ClientAccess from app.models.base import generate_uuid import bcrypt user = User( id=generate_uuid(), email="author@test.com", password_hash=bcrypt.hashpw(b"TestPass123!", bcrypt.gensalt()).decode(), role="author", full_name="Test Author", is_active=True, stripe_plan_id="starter", ) db_session.add(user) now = datetime.now(timezone.utc) access = ClientAccess( id=generate_uuid(), author_id=user.id, granted_by=None, plan="starter", plan_id="starter", granted_at=now, expires_at=now + timedelta(days=365), token_hash="test_token_hash", token_budget=500_000, tokens_used=0, auto_renew=True, ) db_session.add(access) await db_session.commit() return user @pytest_asyncio.fixture async def test_pricing_plan(db_session): import json from app.models.pricing_plan import PricingPlan plan = PricingPlan( id="starter", display_name="Starter", monthly_price_usd=49.0, annual_price_usd=39.0, stripe_price_id_monthly="price_test_monthly", stripe_price_id_annual="price_test_annual", max_books=5, max_chats_per_month=2000, max_token_budget=500_000, max_embeds=1, features_json=json.dumps(["5 books", "2,000 chats/month", "Email support"]), is_active=True, is_popular=False, sort_order=1, ) db_session.add(plan) await db_session.commit() return plan