M24: implement privacy governance
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
from app.core.db import SessionLocal
|
||||
from app.models.audit import AuditEvent
|
||||
from app.models.customer import Customer
|
||||
|
||||
|
||||
def test_privacy_retention_uses_persisted_counts(ops_client):
|
||||
response = ops_client.get("/api/v1/privacy/retention")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["customers_total"] >= 180
|
||||
assert body["minimum_booking_retention_days"] == 30
|
||||
assert body["audit_retention_days"] == 2555
|
||||
|
||||
|
||||
def test_privacy_retention_requires_manager(employee_client):
|
||||
assert employee_client.get("/api/v1/privacy/retention").status_code == 403
|
||||
|
||||
|
||||
def test_customer_export_is_downloadable_and_audited_without_mutation(ops_client):
|
||||
response = ops_client.get("/api/v1/privacy/customers/CUS-0001/export")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-disposition"] == 'attachment; filename="CUS-0001-privacy.json"'
|
||||
assert response.json()["customer"]["public_ref"] == "CUS-0001"
|
||||
assert isinstance(response.json()["bookings"], list)
|
||||
events = ops_client.get("/api/v1/audit", params={"action": "privacy_customer_exported"}).json()
|
||||
assert any(event["metadata"]["customer_ref"] == "CUS-0001" for event in events)
|
||||
|
||||
|
||||
def test_active_customer_cannot_be_anonymized(ops_client):
|
||||
response = ops_client.post(
|
||||
"/api/v1/privacy/customers/CUS-0042/anonymize",
|
||||
json={"confirmation": "CUS-0042", "reason": "Verified erasure request"},
|
||||
)
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_eligible_customer_is_irreversibly_anonymized_without_pii_in_audit(ops_client):
|
||||
with SessionLocal() as db:
|
||||
customer = Customer(
|
||||
public_ref="CUS-PRIVACY",
|
||||
first_name="Private",
|
||||
last_name="Person",
|
||||
email="private.person@example.test",
|
||||
phone="+32000000000",
|
||||
postal_code="1000",
|
||||
city="Brussels",
|
||||
date_of_birth=None,
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
customer_id = customer.id
|
||||
try:
|
||||
mismatch = ops_client.post(
|
||||
"/api/v1/privacy/customers/CUS-PRIVACY/anonymize",
|
||||
json={"confirmation": "CUS-WRONG", "reason": "Verified erasure request"},
|
||||
)
|
||||
assert mismatch.status_code == 422
|
||||
response = ops_client.post(
|
||||
"/api/v1/privacy/customers/CUS-PRIVACY/anonymize",
|
||||
json={"confirmation": "CUS-PRIVACY", "reason": "Verified erasure request"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "anonymized"
|
||||
with SessionLocal() as db:
|
||||
customer = db.scalar(select(Customer).where(Customer.id == customer_id))
|
||||
assert customer is not None
|
||||
assert customer.email is None
|
||||
assert customer.phone is None
|
||||
assert customer.first_name == "Anoniem"
|
||||
event = db.scalar(
|
||||
select(AuditEvent)
|
||||
.where(AuditEvent.action == "privacy_customer_anonymized")
|
||||
.order_by(AuditEvent.occurred_at.desc())
|
||||
)
|
||||
assert event is not None
|
||||
serialized = f"{event.before_json}{event.after_json}{event.metadata_json}"
|
||||
assert "private.person@example.test" not in serialized
|
||||
finally:
|
||||
with SessionLocal() as db:
|
||||
db.execute(delete(AuditEvent).where(AuditEvent.entity_id == customer_id))
|
||||
db.execute(delete(Customer).where(Customer.id == customer_id))
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_audit_csv_export_is_bounded_and_audited(ops_client):
|
||||
response = ops_client.get("/api/v1/audit/export.csv")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/csv")
|
||||
assert response.text.startswith("id,occurred_at,actor_type")
|
||||
invalid = ops_client.get(
|
||||
"/api/v1/audit/export.csv",
|
||||
params={"occurred_from": "2025-01-01T00:00:00Z", "occurred_to": "2026-01-01T00:00:00Z"},
|
||||
)
|
||||
assert invalid.status_code == 422
|
||||
Reference in New Issue
Block a user