feat(audit): expose structured before/after evidence
audit_events already stored before_json/after_json, but the API and UI only ever surfaced metadata -- the audit trail could say something happened but never show what changed. Add before/after to AuditEventOut, resolve a safe entity_ref/entity_link for vehicle/booking/data-quality-issue entities (customer stays label-only; no customer detail route exists in this PoC), and render a human-readable change summary in the UI with the raw before/after/metadata JSON kept behind a <details> disclosure rather than shown by default.
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db import SessionLocal
|
||||
from app.models.booking import Booking
|
||||
from app.models.vehicle import Vehicle
|
||||
|
||||
|
||||
def test_demo_login_is_audited(ops_client):
|
||||
response = ops_client.get("/api/v1/audit", params={"action": "demo_login"})
|
||||
assert response.status_code == 200
|
||||
@@ -14,3 +21,54 @@ def test_audit_requires_authentication(client):
|
||||
def test_audit_requires_operations_manager(employee_client):
|
||||
response = employee_client.get("/api/v1/audit")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def _activate_booking(vehicle_ref: str, start_odometer_km: int) -> str:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == vehicle_ref))
|
||||
booking = db.scalar(
|
||||
select(Booking).where(Booking.vehicle_id == vehicle.id, Booking.status == "returned")
|
||||
)
|
||||
booking.status = "active"
|
||||
booking.start_odometer_km = start_odometer_km
|
||||
booking.end_odometer_km = None
|
||||
db.commit()
|
||||
return booking.public_ref
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_return_registered_audit_event_exposes_before_after_and_link(ops_client):
|
||||
booking_ref = _activate_booking("MO-015", start_odometer_km=17000)
|
||||
vehicle_before = ops_client.get("/api/v1/vehicles/MO-015").json()
|
||||
ops_client.post(
|
||||
f"/api/v1/bookings/{booking_ref}/return",
|
||||
json={
|
||||
"end_odometer_km": vehicle_before["odometer_km"] + 10,
|
||||
"fuel_level_percent": 50,
|
||||
"cleanliness_ok": True,
|
||||
"damage_reported": False,
|
||||
"technical_warning": False,
|
||||
},
|
||||
headers={"Idempotency-Key": "test-audit-before-after-001"},
|
||||
)
|
||||
|
||||
key = "test-audit-before-after-001"
|
||||
events = ops_client.get(
|
||||
"/api/v1/audit", params={"action": "return_registered"}
|
||||
).json()
|
||||
event = next(e for e in events if e["metadata"]["idempotency_key"] == key)
|
||||
assert event["before"] == {"status": "active"}
|
||||
assert event["after"]["status"] == "returned"
|
||||
assert event["entity_ref"] == booking_ref
|
||||
assert event["entity_link"] == f"/bookings/{booking_ref}"
|
||||
|
||||
vehicle_events = ops_client.get(
|
||||
"/api/v1/audit",
|
||||
params={"action": "vehicle_status_changed", "correlation_id": event["correlation_id"]},
|
||||
).json()
|
||||
assert len(vehicle_events) == 1
|
||||
assert vehicle_events[0]["entity_ref"] == "MO-015"
|
||||
assert vehicle_events[0]["entity_link"] == "/vehicles/MO-015"
|
||||
assert vehicle_events[0]["before"]["odometer_km"] == vehicle_before["odometer_km"]
|
||||
|
||||
Reference in New Issue
Block a user