"""Integration test fixtures (TEST-02). The configured DB user lacks `CREATE DATABASE` privilege, so a literal second MySQL schema isn't available here. Instead, each test runs inside a SAVEPOINT-nested transaction against the real configured database that is *always rolled back* at the end of the test (SQLAlchemy's documented "join a Session into an external transaction" recipe) -- route handlers' internal `db.commit()` calls only release the savepoint, they never commit the outer transaction, so nothing a test writes is ever actually persisted. This achieves the requirement's real goal (integration tests never leave data behind in the database other code reads) without needing a schema the credentials can't create. Note: the app's lifespan startup calls `seed_officer_if_missing`, which runs against the *real* engine (via `session_scope()`), not the savepoint-wrapped test session -- so the seed officer row is a genuine, persistent row (idempotent: it no-ops if already present), not test data that gets rolled back. That's intentional and matches production behavior. """ from __future__ import annotations import app.auth as auth_module import pytest from fastapi.testclient import TestClient from sqlalchemy import event from app.config import settings from app.db.session import SessionLocal, engine, get_session from app.main import app # TestClient's default base URL is http://testserver -- a "Secure" cookie # (correct for the real deployment behind an HTTPS reverse proxy) would # never be sent back over that scheme, breaking every authenticated request # in tests. Same fix as local dev: force it off for the test process. auth_module.COOKIE_SECURE = False @pytest.fixture() def db_session(): connection = engine.connect() outer_transaction = connection.begin() session = SessionLocal(bind=connection, autoflush=False, autocommit=False) nested = connection.begin_nested() @event.listens_for(session, "after_transaction_end") def _restart_savepoint(sess, transaction): nonlocal nested if not nested.is_active: nested = connection.begin_nested() try: yield session finally: session.close() outer_transaction.rollback() connection.close() @pytest.fixture() def api_client(db_session): def _override_get_session(): yield db_session app.dependency_overrides[get_session] = _override_get_session with TestClient(app) as test_client: yield test_client app.dependency_overrides.pop(get_session, None) def login(api_client: TestClient, email: str, password: str) -> dict: """Logs in and returns `{"cookies": {...}}` for use as `api_client.get(url, cookies=identity["cookies"])` -- lets a single TestClient (one shared DB transaction) act as several logged-in identities at once without re-running the (expensive, model-loading) lifespan per identity. """ res = api_client.post("/api/auth/login", json={"email": email, "password": password}) assert res.status_code == 200, res.text token = res.cookies.get("session_token") return {"cookies": {"session_token": token}, "user": res.json()} @pytest.fixture() def officer_identity(api_client): return login(api_client, settings.SEED_OFFICER_EMAIL, settings.SEED_OFFICER_PASSWORD) def create_client(api_client: TestClient, officer_identity: dict, *, email: str, name: str, balance: float = 5000) -> dict: res = api_client.post( "/api/officer/clients", json={ "name": name, "email": email, "temp_password": "TempPass123!", "starting_balance": balance, "account_type": "checking", }, cookies=officer_identity["cookies"], ) assert res.status_code == 200, res.text body = res.json() identity = login(api_client, email, "TempPass123!") identity["client_id"] = body["client"]["id"] return identity @pytest.fixture() def two_clients(api_client, officer_identity): """Two client identities with distinct starting balances, per the role-separation testing requirement (one seeded officer, two seeded clients).""" client_a = create_client( api_client, officer_identity, email="fixture.alpha@fakebankmail.com", name="Fixture Alpha", balance=5000 ) client_b = create_client( api_client, officer_identity, email="fixture.beta@fakebankmail.com", name="Fixture Beta", balance=3000 ) return client_a, client_b TRANSFER_FRAUD_PATTERN = { "step": 5, "type": "TRANSFER", "amount": 181.0, "nameOrig": "CTEST_FRAUD_ORIG", "oldbalanceOrg": 181.0, "newbalanceOrig": 0.0, "nameDest": "CTEST_FRAUD_DEST", "oldbalanceDest": 0.0, "newbalanceDest": 0.0, } PAYMENT_LEGIT_PATTERN = { "step": 5, "type": "PAYMENT", "amount": 50.0, "nameOrig": "CTEST_LEGIT_ORIG", "oldbalanceOrg": 5000.0, "newbalanceOrig": 4950.0, "nameDest": "MTEST_MERCHANT", "oldbalanceDest": 0.0, "newbalanceDest": 0.0, }