---
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