bank-fraud / tests /integration /conftest.py
root
init
942b115
Raw
History Blame Contribute Delete
5.05 kB
"""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,
}