Spaces:
Configuration error
Configuration error
| """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 | |
| def db_session(): | |
| connection = engine.connect() | |
| outer_transaction = connection.begin() | |
| session = SessionLocal(bind=connection, autoflush=False, autocommit=False) | |
| nested = connection.begin_nested() | |
| 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() | |
| 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()} | |
| 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 | |
| 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, | |
| } | |