| """ |
| Integration Tests — AuditMiddleware (I-7 through I-11) |
| Verifies that every API request is recorded in the audit_logs table. |
| |
| The AuditMiddleware uses AsyncSessionLocal from backend.database, which points to |
| the production DB. We patch it to use TestSessionLocal so the middleware writes |
| to the same in-memory SQLite that fixtures use. |
| """ |
|
|
| import pytest |
| from unittest.mock import patch |
| from sqlalchemy import select |
|
|
| from backend.db_models.audit_log import AuditLog |
|
|
| |
| from tests.conftest import TestSessionLocal |
|
|
| HEART_PAYLOAD = { |
| "Age": 55, "Sex": "M", "ChestPainType": "ATA", "RestingBP": 130, |
| "Cholesterol": 250, "FastingBS": 0, "RestingECG": "Normal", |
| "MaxHR": 150, "ExerciseAngina": "N", "Oldpeak": 1.5, "ST_Slope": "Up", |
| } |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def patch_middleware_session(db_tables): |
| """Route AuditMiddleware DB writes to the test in-memory SQLite database.""" |
| with patch("backend.middleware.audit.AsyncSessionLocal", TestSessionLocal): |
| yield |
|
|
|
|
| class TestAuditMiddlewareLogsRequests: |
| async def test_authenticated_predict_creates_audit_log(self, client, doctor_token, db_session): |
| resp = await client.post( |
| "/api/v4/heart_disease/predict", |
| json=HEART_PAYLOAD, |
| headers={"Authorization": f"Bearer {doctor_token}"}, |
| ) |
| assert resp.status_code == 200 |
|
|
| result = await db_session.execute( |
| select(AuditLog).where(AuditLog.endpoint == "/api/v4/heart_disease/predict") |
| ) |
| logs = result.scalars().all() |
| assert len(logs) >= 1, "Expected at least one audit log entry for /predict" |
|
|
| async def test_audit_log_has_correct_status_code(self, client, doctor_token, db_session): |
| resp = await client.post( |
| "/api/v4/heart_disease/predict", |
| json=HEART_PAYLOAD, |
| headers={"Authorization": f"Bearer {doctor_token}"}, |
| ) |
| result = await db_session.execute( |
| select(AuditLog).where(AuditLog.endpoint == "/api/v4/heart_disease/predict") |
| ) |
| logs = result.scalars().all() |
| assert any(log.status_code == 200 for log in logs) |
|
|
| async def test_audit_log_duration_ms_positive(self, client, doctor_token, db_session): |
| await client.post( |
| "/api/v4/heart_disease/predict", |
| json=HEART_PAYLOAD, |
| headers={"Authorization": f"Bearer {doctor_token}"}, |
| ) |
| result = await db_session.execute( |
| select(AuditLog).where(AuditLog.endpoint == "/api/v4/heart_disease/predict") |
| ) |
| logs = result.scalars().all() |
| assert all(log.duration_ms is not None and log.duration_ms > 0 for log in logs) |
|
|
| async def test_audit_log_records_user_id_for_authenticated_request( |
| self, client, doctor_token, db_session |
| ): |
| await client.post( |
| "/api/v4/heart_disease/predict", |
| json=HEART_PAYLOAD, |
| headers={"Authorization": f"Bearer {doctor_token}"}, |
| ) |
| result = await db_session.execute( |
| select(AuditLog).where(AuditLog.endpoint == "/api/v4/heart_disease/predict") |
| ) |
| logs = result.scalars().all() |
| authenticated_logs = [log for log in logs if log.user_id is not None] |
| assert len(authenticated_logs) >= 1 |
|
|
| async def test_unauthenticated_request_logs_null_user_id(self, client, db_session): |
| |
| await client.post( |
| "/auth/login", |
| json={"email": "nonexistent@test.com", "password": "wrongpassword"}, |
| ) |
| result = await db_session.execute( |
| select(AuditLog).where( |
| AuditLog.endpoint == "/auth/login", |
| AuditLog.user_id.is_(None), |
| ) |
| ) |
| logs = result.scalars().all() |
| assert len(logs) >= 1, "Expected audit log with null user_id for failed login" |
|
|
| async def test_audit_log_records_http_method(self, client, doctor_token, db_session): |
| await client.post( |
| "/api/v4/heart_disease/predict", |
| json=HEART_PAYLOAD, |
| headers={"Authorization": f"Bearer {doctor_token}"}, |
| ) |
| result = await db_session.execute( |
| select(AuditLog).where(AuditLog.endpoint == "/api/v4/heart_disease/predict") |
| ) |
| logs = result.scalars().all() |
| assert any(log.method == "POST" for log in logs) |
|
|