--- plan: 06.1-02 title: Promote test_audit.py stubs to real tests (ADMIN-06) wave: 1 depends_on: [] phase: "6.1" requirements_addressed: [ADMIN-06] files_modified: - backend/tests/test_audit.py autonomous: true --- # Plan 06.1-02 — Promote test_audit.py stubs to real tests ## Objective The admin audit log API (`api/audit.py`) is fully implemented with `GET /api/admin/audit-log` (paginated + filtered) and `GET /api/admin/audit-log/export` (CSV streaming). `test_audit.py` contains 4 xfail stubs that call `pytest.xfail("not implemented yet")`. This plan replaces every stub with a real test. ## Context - Backend implementation: `backend/api/audit.py` — `GET /api/admin/audit-log?start=&end=&user_id=&event_type=&page=&per_page=` + CSV export - Router registration: `backend/main.py` line 191-193 — `from api.audit import router as audit_router; app.include_router(audit_router)` - Frontend: `frontend/src/components/admin/AuditLogTab.vue` — full filter UI (start/end date, user_id, event_type) - Test stubs: `backend/tests/test_audit.py` — 4 tests all call `pytest.xfail("not implemented yet")` - Test infrastructure: `async_client`, `auth_user`, `admin_user`, `db_session` from conftest.py The tests need at least one AuditLog row to verify filtering behaviour. The `write_audit_log` service function (`backend/services/audit.py`) can be called directly in tests to seed entries without going through an endpoint. ## Tasks --- ### Task 1 — Implement real tests in test_audit.py - backend/tests/test_audit.py — full file (stubs to replace) - backend/api/audit.py — endpoint response shape: `{"items": [...], "total": int, "page": int, "per_page": int}`; `_audit_to_dict` field names: id, event_type, user_id, actor_id, resource_id, ip_address, metadata_, created_at - backend/services/audit.py — `write_audit_log` signature for seeding entries in tests Rewrite `backend/tests/test_audit.py` entirely. Add imports at the top: ``` from __future__ import annotations import pytest ``` Add `pytestmark = pytest.mark.asyncio` at the module level so all test coroutines are discovered without per-test decorators. Add a module-level helper `async def _seed_audit(db_session, user_id)` that calls `write_audit_log(session=db_session, event_type="document.uploaded", user_id=user_id, actor_id=user_id, resource_id=None, ip_address=None, metadata_={"size_bytes": 100})` followed by `await db_session.commit()`. Import `write_audit_log` from `services.audit` inside the function body to avoid top-level import ordering issues. Then implement each test without any `@pytest.mark.xfail` decorator: **test_audit_log_viewer(async_client, admin_user, db_session)** - Seed one audit entry via `_seed_audit(db_session, admin_user["user"].id)` - GET /api/admin/audit-log with admin_user headers - Assert response status 200 - Assert response body has keys: "items", "total", "page", "per_page" - Assert "total" >= 1 (the seeded entry is present) - Assert items is a list and items[0] has keys: "id", "event_type", "user_id", "created_at" **test_audit_log_no_doc_content(async_client, admin_user, db_session)** - Seed an entry whose metadata_ contains `{"size_bytes": 100}` (no filename, no extracted_text) - GET /api/admin/audit-log with admin_user headers - Assert status 200 - For every item in response["items"]: - Assert "filename" not in item (ADMIN-06, D-15) - Assert "extracted_text" not in item - Assert "password_hash" not in item - Assert "credentials_enc" not in item - Assert no item has a "metadata_" key whose value (if a dict) contains "filename" or "extracted_text" **test_audit_log_regular_user_403(async_client, auth_user)** - GET /api/admin/audit-log with auth_user headers (regular user, not admin) - Assert status 403 **test_audit_log_export_csv(async_client, admin_user, db_session)** - Seed one entry via `_seed_audit` - GET /api/admin/audit-log/export?format=csv with admin_user headers - Assert status 200 - Assert response headers["content-type"] starts with "text/csv" - Assert response headers["content-disposition"] contains "audit-export.csv" - Assert response text contains the CSV header line: "id,event_type,user_id,actor_id,resource_id,ip_address,metadata_,created_at" - `test_audit.py` has zero `pytest.xfail` calls — every test has real assertions - `test_audit.py` has zero `@pytest.mark.xfail` decorators - `import os` is removed (was unused) - Running `docker compose exec backend python -m pytest tests/test_audit.py -v` shows 4 PASSED (no XFAIL, no XPASS) - `test_audit_log_regular_user_403` asserts status 403 — admin gate tested - `test_audit_log_no_doc_content` asserts "filename" not in any item and "extracted_text" not in any item - `test_audit_log_export_csv` asserts content-type starts with "text/csv" and disposition contains "audit-export.csv" --- ## Verification ``` docker compose exec backend python -m pytest tests/test_audit.py -v ``` Expected: 4 passed, 0 failed, 0 xfailed, 0 xpassed. ## Must-haves - No test uses `pytest.xfail("not implemented yet")` — all 4 stubs replaced with real assertions - `test_audit_log_regular_user_403` proves the admin gate blocks regular users - `test_audit_log_no_doc_content` proves ADMIN-06 metadata safety invariant: no filename or extracted_text in any response field - `test_audit_log_export_csv` proves the CSV export endpoint is functional