139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
|
|
"""Tests for the DSAR worker job (G1 DSGVO: Art. 15/17).
|
||
|
|
|
||
|
|
Proves the previously missing "process_dsar" job works end-to-end:
|
||
|
|
- access: collects profile + contacts + audit_log + notifications
|
||
|
|
- deletion: soft-deletes contacts, hard-deletes notifications, anonymizes
|
||
|
|
and deactivates the account, writes a dsar_erasure audit entry
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import pytest_asyncio
|
||
|
|
from sqlalchemy import select as sa_select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||
|
|
|
||
|
|
from app.core.db import reset_engine_for_testing
|
||
|
|
from app.core.jobs import _dsar_collect_user_data, _dsar_execute_deletion, process_dsar
|
||
|
|
from app.models.audit import AuditLog
|
||
|
|
from app.models.contact import Contact
|
||
|
|
from app.models.notification import Notification
|
||
|
|
from app.models.user import User
|
||
|
|
from tests.conftest import seed_tenant_and_users
|
||
|
|
|
||
|
|
|
||
|
|
@pytest_asyncio.fixture
|
||
|
|
async def dsar_session(engine) -> AsyncSession:
|
||
|
|
reset_engine_for_testing(engine)
|
||
|
|
sf = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
|
||
|
|
async with sf() as session:
|
||
|
|
yield session
|
||
|
|
await session.rollback()
|
||
|
|
|
||
|
|
|
||
|
|
def test_process_dsar_registered():
|
||
|
|
"""The job is registered so enqueue_job('process_dsar') can find it."""
|
||
|
|
from app.core.job_registry import get_job
|
||
|
|
|
||
|
|
assert get_job("process_dsar") is not None
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_dsar_access_collects_all_categories(dsar_session: AsyncSession):
|
||
|
|
"""Art. 15: collect_user_data returns all promised categories."""
|
||
|
|
seed = await seed_tenant_and_users(dsar_session)
|
||
|
|
data = await _dsar_collect_user_data(
|
||
|
|
dsar_session,
|
||
|
|
str(seed["tenant_a"].id),
|
||
|
|
str(seed["admin_a"].id),
|
||
|
|
)
|
||
|
|
assert data["legal_basis"]
|
||
|
|
for category in ("profile", "contacts", "audit_log", "notifications"):
|
||
|
|
assert category in data["data"], f"missing category {category}"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_dsar_deletion_anonymizes_and_soft_deletes(dsar_session: AsyncSession):
|
||
|
|
"""Art. 17: contacts soft-deleted, notifications gone, user anonymized."""
|
||
|
|
seed = await seed_tenant_and_users(dsar_session)
|
||
|
|
uid = seed["admin_a"].id
|
||
|
|
tid = seed["tenant_a"].id
|
||
|
|
|
||
|
|
# Fixture: one contact + one notification owned by admin_a
|
||
|
|
contact = Contact(
|
||
|
|
id=uuid.uuid4(), tenant_id=tid, owner_id=uid,
|
||
|
|
type="person", displayname="To Erase", email_1="erase@test.local",
|
||
|
|
firstname="Era", surname="SeMe",
|
||
|
|
discount_crew=0, discount_transport=0, discount_rental=0,
|
||
|
|
discount_sale=0, discount_subrent=0, discount_total=0,
|
||
|
|
)
|
||
|
|
notification = Notification(
|
||
|
|
id=uuid.uuid4(), tenant_id=tid, owner_id=uid,
|
||
|
|
user_id=uid,
|
||
|
|
type="test",
|
||
|
|
title="to erase",
|
||
|
|
)
|
||
|
|
dsar_session.add_all([contact, notification])
|
||
|
|
await dsar_session.commit()
|
||
|
|
|
||
|
|
counts = await _dsar_execute_deletion(dsar_session, str(tid), str(uid))
|
||
|
|
await dsar_session.commit()
|
||
|
|
|
||
|
|
assert counts["contacts_soft_deleted"] == 1
|
||
|
|
assert counts["notifications_deleted"] == 1
|
||
|
|
assert counts["user_anonymized"] == 1
|
||
|
|
|
||
|
|
# Verify persisted state (fresh query, bypassing identity map)
|
||
|
|
dsar_session.expunge_all()
|
||
|
|
erased_contact = (
|
||
|
|
await dsar_session.execute(sa_select(Contact).where(Contact.id == contact.id))
|
||
|
|
).scalar_one()
|
||
|
|
assert erased_contact.deleted_at is not None
|
||
|
|
|
||
|
|
remaining_notifications = (
|
||
|
|
await dsar_session.execute(
|
||
|
|
sa_select(Notification).where(Notification.owner_id == uid)
|
||
|
|
)
|
||
|
|
).scalars().all()
|
||
|
|
assert len(remaining_notifications) == 0
|
||
|
|
|
||
|
|
erased_user = (
|
||
|
|
await dsar_session.execute(sa_select(User).where(User.id == uid))
|
||
|
|
).scalar_one()
|
||
|
|
assert erased_user.is_active is False
|
||
|
|
assert erased_user.name == "[gelöscht gemäß DSGVO Art. 17]"
|
||
|
|
assert "anonymized.invalid" in erased_user.email
|
||
|
|
|
||
|
|
# Audit entry written
|
||
|
|
audits = (
|
||
|
|
await dsar_session.execute(
|
||
|
|
sa_select(AuditLog).where(AuditLog.action == "dsar_erasure")
|
||
|
|
)
|
||
|
|
).scalars().all()
|
||
|
|
assert len(audits) >= 1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_process_dsar_dispatches_by_type(dsar_session: AsyncSession):
|
||
|
|
"""process_dsar dispatches by request_type without raising."""
|
||
|
|
seed = await seed_tenant_and_users(dsar_session)
|
||
|
|
|
||
|
|
result = await process_dsar(
|
||
|
|
{},
|
||
|
|
user_id=str(seed["admin_a"].id),
|
||
|
|
tenant_id=str(seed["tenant_a"].id),
|
||
|
|
request_type="access",
|
||
|
|
)
|
||
|
|
assert result["status"] == "completed"
|
||
|
|
assert "categories" in result
|
||
|
|
|
||
|
|
result = await process_dsar(
|
||
|
|
{},
|
||
|
|
user_id=str(seed["editor_a"].id),
|
||
|
|
tenant_id=str(seed["tenant_a"].id),
|
||
|
|
request_type="unknown_type_xyz",
|
||
|
|
)
|
||
|
|
assert result["status"] == "unknown_type"
|