Files
kite/.planning/phases/06.1-close-v1-audit-gaps/06.1-02-PLAN.md
T
curo1305 1e4654aad5 docs(phase-6.1): update tracking after wave 1 — both plans complete
11 tests passing (7 shares + 4 audit), 309 total, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:18:04 +02:00

5.3 KiB

plan, title, wave, depends_on, phase, requirements_addressed, files_modified, autonomous
plan title wave depends_on phase requirements_addressed files_modified autonomous
06.1-02 Promote test_audit.py stubs to real tests (ADMIN-06) 1
6.1
ADMIN-06
backend/tests/test_audit.py
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.pyGET /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

<read_first>

  • 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 </read_first>
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"

<acceptance_criteria>

  • 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" </acceptance_criteria>

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