feat(g1): DSGVO Art.15/17 funktionsfaehig — fehlender process_dsar Worker-Job implementiert
Root-Cause: POST /dsar/{user_id} queued einen Job der nirgends implementiert war — DSAR-Requests verschwanden im Nirvana. Implementiert in app/core/jobs.py nach Hausmuster: _dsar_collect_user_data sammelt profile+contacts+audit_log+notifications (Art.15/20), _dsar_execute_deletion fuehrt Art.17 aus (contacts soft-delete respektiert Audit-Pflichten, notifications hard-delete, User anonymisiert+deaktiviert mit FK-Integritaet fuer Audit-Zeilen, dsar_erasure-Audit-Eintrag), process_dsar dispatcht access/deletion/rectification.
Beweis: test_g1_dsar 4/4 gruen; ruff clean.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user