bank-fraud / TESTING.md
root
init
942b115
|
Raw
History Blame Contribute Delete
7.69 kB
# Testing Guide
Three layers: automated `pytest` (unit + integration, including auth/role coverage), manual exploration through the officer console and client app, and a live smoke test script for post-deploy verification of the full officer/client flow.
## 1. Automated tests (pytest)
```bash
source venv/bin/activate
pip install -r requirements-dev.txt
pytest # everything
pytest tests/unit -q # unit tests only (fast, no live DB writes)
pytest tests/integration -q # integration tests (hits the real MySQL DB)
```
### Unit tests (`tests/unit/`)
Small synthetic fixtures, no database:
- `test_config.py` β€” fail-loud `.env` validation (including the auth-related vars: `SEED_OFFICER_EMAIL`, `SEED_OFFICER_PASSWORD`, `SESSION_EXPIRE_MINUTES`)
- `test_features.py` β€” the shared feature-engineering module (`app/features.py`)
- `test_train_split.py` β€” time-respecting split logic
- `test_scoring.py` β€” the scoring service (`app/scoring.py`) against the real trained model, using hand-built transactions
- `test_labeled_fixtures.py` β€” replays `tests/fixtures/labeled_transactions.json` (58 hand-labeled synthetic transactions covering obvious fraud and legitimate patterns across simulated dates) through the real scoring service and asserts β‰₯90% overall accuracy and β‰₯90% fraud recall. Regenerate the fixture set with:
```bash
python -m scripts.generate_fixtures
```
### Integration tests (`tests/integration/`)
`test_api.py` exercises every endpoint through FastAPI's `TestClient` against the real, configured MySQL database, covering:
- Auth: login (correct/wrong password/unknown email β€” all the same generic error), logout invalidating the session, `/api/auth/me`
- Role boundaries: an officer route rejects a client (403) and an anonymous caller (401), and vice versa
- Officer flows: create client, list clients, client detail (transactions + alerts), cross-client alert queue, approve/reject/dismiss (reject reverses the transaction and restores the client's balance), dashboard/insights/model-performance stats, `/api/predict` + `/api/transactions/batch`
- Client flows: submit a transaction, can't overdraw, own transaction history, own alerts
- **Data isolation**: two client fixtures (`two_clients` β€” see below) confirm each client sees only their own transactions/alerts, and that a client hitting an officer-only route for another client's detail gets 403
- All HTML pages return 200 with the right content type
**Fixtures** (`conftest.py`): `officer_identity` logs in as the seed officer; `two_clients` creates two client accounts through the real `/api/officer/clients` endpoint (not inserted directly into the DB) and logs into both, giving you `(client_a, client_b)` β€” each a `{"cookies": {...}, "user": {...}, "client_id": ...}` dict. Because a single `TestClient` only has one cookie jar, tests pass `cookies=identity["cookies"]` explicitly per request to act as several logged-in identities at once without re-running the (model-loading) app lifespan per identity.
**Isolation note**: the configured DB user does not have `CREATE DATABASE` privilege, so these tests can't spin up a literal second schema. Instead, `conftest.py` wraps each test in a SQL SAVEPOINT-nested transaction that is *always rolled back* at the end of the test (SQLAlchemy's documented "join a Session into an external transaction" pattern) β€” route handlers' internal `db.commit()` calls only release the savepoint, never the outer transaction, so nothing a test writes is ever actually persisted. The one exception is the seed officer row itself: the app's startup lifespan creates it (idempotently) against the *real* engine, not the test's savepoint session β€” that's intentional, matching production behavior, and safe to re-run. If your database user does get `CREATE DATABASE` privilege later, point `DATABASE_URL` at a dedicated `..._test` database instead for a literal second-schema setup β€” the tests themselves don't need to change.
## 2. Manual exploration
Start the API (`uvicorn app.main:app --host 0.0.0.0 --port $APP_PORT`) and open `http://localhost:$APP_PORT/login`.
**As the officer** (sign in with `SEED_OFFICER_EMAIL`/`SEED_OFFICER_PASSWORD` from `.env`):
1. `/officer/clients` β†’ "+ Create client" β€” name, email, starting balance, account type. Copy the one-time temp password shown.
2. `/officer/alerts` β€” once a client has submitted a flagged transaction, click "Review" to expand the SHAP breakdown, then Approve, Reject & reverse, or Dismiss it. Reject asks for confirmation and reverses the transaction (excluded from the client's balance).
3. `/officer/insights` and `/officer/model` β€” the EDA and model-selection story behind the score.
**As a client** (log out, sign in with the email + temp password from step 1):
1. `/client/transactions/new` β€” pick a type, amount, destination account, and date/time.
2. **To reproduce the classic fraud pattern**: enter your full current balance as the amount for a Transfer or Cash out. This drains the account to exactly zero β€” the same signature the model learned from real PaySim fraud rows. The result reads "being reviewed for your security," not a raw probability.
3. `/client/alerts` β€” read-only, plain-language status; no admin actions available to a client.
Every client-submitted transaction flows through the same `transactions` β†’ `fraud_predictions` β†’ `fraud_alerts` tables as everything else β€” `/officer/alerts` and `/client/alerts` are just two different filtered views over one table, not separate systems.
## 3. Live smoke test (`scripts/test_live.py`)
Post-deploy (or post-restart) verification against a **running** instance of the API β€” local or behind the reverse proxy:
```bash
# against a local dev server
python -m scripts.test_live --base-url http://127.0.0.1:8811
# against the live deployment
python -m scripts.test_live --base-url https://bank-fraud.hdev.rw
```
What it does:
1. Checks `/health`.
2. Logs in as the seed officer, creates two clients, and submits three transactions across different simulated dates on client A's account (a legit payment, a legit partial transfer, and a full-balance drain) through the real `/api/client/transactions` endpoint β€” printing predicted vs. expected risk tier for each.
3. Confirms role separation live: the officer sees client A's alert in the cross-client queue; client A sees exactly their own alert; client B sees none of it; client B gets a 403 when hitting the officer-only client-detail route for client A.
4. Replays the full `tests/fixtures/labeled_transactions.json` fixture set through the officer-only `/api/predict` endpoint, printing predicted vs. expected risk tier per transaction and a final accuracy summary.
Exits `0` only if accuracy stays at or above 90% (the same bar `test_labeled_fixtures.py` enforces) *and* every role-separation check passed; exits `1` otherwise β€” safe to wire into a post-deploy CI/CD gate.
**Note**: this script creates two real client accounts + a few transactions/predictions/alerts against whatever database the target server is configured against β€” clean those up afterward if you're running it against a database other test suites also read from, e.g.:
```python
from app.db.session import session_scope
from app.db.models import Transaction, FraudPrediction, FraudAlert, Client, User, Session as SessionModel
with session_scope() as db:
db.query(FraudAlert).delete()
db.query(FraudPrediction).delete()
db.query(Transaction).delete()
db.query(SessionModel).delete()
db.query(Client).delete()
db.query(User).filter(User.role == "client").delete() # keeps the seed officer
```