Files
timemaster/backend/tests/test_users.py
T
patrickandClaude Opus 4.8 2af226990a
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled
feat(dsgvo): Art. 17 Anonymisierung von Mitarbeiterkonten
Statt Hard-Delete (kollidiert mit Lohn-/Steuer-Aufbewahrung) werden bei
DSGVO-Löschanfrage die personenbezogenen Felder der bestehenden User-Zeile
überschrieben; referenzierte Daten (Zeit/Abwesenheit/Auszahlung/AuditLog)
bleiben entkoppelt erhalten.

- users.anonymized_at (Migration 0040); UserOut.anonymized_at.
- user_service.anonymize_user: überschreibt Name/E-Mail (eindeutig anon-<id>@deleted.local),
  entfernt Auth-Material (Passwort/PIN/QR/NFC/TOTP/LDAP-DN), is_active=False, löscht Sessions,
  AuditLog 'user_anonymized'. Personalnummer bleibt reserviert (kein Personenbezug mehr).
  Selbst-Anonymisierung (400), Doppelanonymisierung/Reaktivierung anonymisierter Konten (409).
- POST /users/{id}/anonymize (COMPANY_ADMIN/SUPER_ADMIN, mandantengeprüft).
- UsersPage: "Anonymisieren"-Button mit Sicherheitsabfrage + "Anonymisiert"-Badge.

193/193 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:31:14 +02:00

164 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.asyncio
USERS_URL = "/api/v1/users"
INVITE_URL = "/api/v1/users/invite"
def auth(tokens):
return {"Authorization": f"Bearer {tokens['access_token']}"}
async def test_list_users_as_admin(client: AsyncClient, registered_user):
resp = await client.get(USERS_URL + "/", headers=auth(registered_user["tokens"]))
assert resp.status_code == 200
body = resp.json()
assert "total" in body
assert "items" in body
assert body["total"] >= 1
async def test_list_users_forbidden_for_employee(client: AsyncClient, registered_user):
# Invite an employee first
inv = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "employee@test.de",
"first_name": "Anna",
"last_name": "Arbeit",
"role": "EMPLOYEE",
})
assert inv.status_code == 201
# Employee tries to list users should fail
# (We can't log in as employee here without accepting invite;
# so we test the role check via schema validation only)
assert inv.json()["role"] == "EMPLOYEE"
async def test_invite_user(client: AsyncClient, registered_user):
resp = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "newcolleague@test.de",
"first_name": "Birgit",
"last_name": "Neu",
"role": "MANAGER",
})
assert resp.status_code == 201
body = resp.json()
assert body["email"] == "newcolleague@test.de"
assert body["role"] == "MANAGER"
assert body["is_active"] is False # not yet accepted
async def test_invite_duplicate_email(client: AsyncClient, registered_user):
await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "dup@test.de", "first_name": "D", "last_name": "U", "role": "EMPLOYEE",
})
resp = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "dup@test.de", "first_name": "D", "last_name": "U", "role": "EMPLOYEE",
})
assert resp.status_code == 400
async def test_get_me(client: AsyncClient, registered_user):
resp = await client.get(USERS_URL + "/me", headers=auth(registered_user["tokens"]))
assert resp.status_code == 200
assert resp.json()["id"] == registered_user["user"]["id"]
async def test_update_user(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.patch(
f"{USERS_URL}/{user_id}",
headers=auth(registered_user["tokens"]),
json={"first_name": "Maximilian"},
)
assert resp.status_code == 200
assert resp.json()["first_name"] == "Maximilian"
async def test_deactivate_and_reactivate(client: AsyncClient, registered_user):
# Invite a second user to deactivate
inv = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "temp@test.de", "first_name": "T", "last_name": "Emp", "role": "EMPLOYEE",
})
user_id = inv.json()["id"]
deact = await client.post(
f"{USERS_URL}/{user_id}/deactivate",
headers=auth(registered_user["tokens"]),
)
assert deact.status_code == 200
assert deact.json()["is_active"] is False
react = await client.post(
f"{USERS_URL}/{user_id}/reactivate",
headers=auth(registered_user["tokens"]),
)
assert react.status_code == 200
assert react.json()["is_active"] is True
async def test_cannot_deactivate_self(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.post(
f"{USERS_URL}/{user_id}/deactivate",
headers=auth(registered_user["tokens"]),
)
assert resp.status_code == 400
async def test_set_kiosk_pin(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.post(
f"{USERS_URL}/{user_id}/kiosk-pin",
headers=auth(registered_user["tokens"]),
json={"pin": "1234"},
)
assert resp.status_code == 200
async def test_kiosk_pin_must_be_numeric(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.post(
f"{USERS_URL}/{user_id}/kiosk-pin",
headers=auth(registered_user["tokens"]),
json={"pin": "abcd"},
)
assert resp.status_code == 422
async def test_anonymize_user(client: AsyncClient, registered_user):
"""DSGVO Art. 17: Anonymisierung entfernt den Personenbezug, Konto wird inaktiv."""
inv = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "gdpr@test.de", "first_name": "Max", "last_name": "Muster",
"role": "EMPLOYEE", "initial_password": "Secret123",
})
user_id = inv.json()["id"]
resp = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["anonymized_at"] is not None
assert body["first_name"] == "Anonymisiert"
assert body["email"] == f"anon-{user_id}@deleted.local"
assert body["is_active"] is False
# Login mit alter E-Mail unmöglich
login = await client.post("/api/v1/auth/login", json={"email": "gdpr@test.de", "password": "Secret123"})
assert login.status_code == 401
# Reaktivierung eines anonymisierten Kontos wird abgelehnt
react = await client.post(f"{USERS_URL}/{user_id}/reactivate", headers=auth(registered_user["tokens"]))
assert react.status_code == 409
# Erneutes Anonymisieren wird abgelehnt
again = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert again.status_code == 409
async def test_cannot_anonymize_self(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert resp.status_code == 400