Files
breakpilot-compliance/backend-compliance/tests/test_legal_document_routes_extended.py
Benjamin Admin b7c1a5da1a
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner)
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt.

Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt
Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert
Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien
Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors
Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert

148 neue Tests (50 + 47 + 26 + 25), alle bestanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:36:24 +01:00

428 lines
16 KiB
Python

"""
Tests for Legal Document extended routes (User Consents, Audit Log, Cookie Categories, Public endpoints).
"""
import uuid
import os
import sys
from datetime import datetime
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from classroom_engine.database import Base, get_db
from compliance.db.legal_document_models import (
LegalDocumentDB, LegalDocumentVersionDB, LegalDocumentApprovalDB,
)
from compliance.db.legal_document_extend_models import (
UserConsentDB, ConsentAuditLogDB, CookieCategoryDB,
)
from compliance.api.legal_document_routes import router as legal_document_router
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_legal_docs_ext.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
HEADERS = {"X-Tenant-ID": TENANT_ID}
app = FastAPI()
app.include_router(legal_document_router, prefix="/api/compliance")
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_db():
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
# =============================================================================
# Helpers — use raw SQLAlchemy to avoid UUID-string issue in SQLite
# =============================================================================
def _create_document(doc_type="privacy_policy", name="Datenschutzerklaerung"):
"""Create a doc directly via SQLAlchemy and return dict with string id."""
db = TestingSessionLocal()
doc = LegalDocumentDB(
tenant_id=TENANT_ID,
type=doc_type,
name=name,
)
db.add(doc)
db.commit()
db.refresh(doc)
result = {"id": str(doc.id), "type": doc.type, "name": doc.name}
db.close()
return result
def _create_version(document_id, version="1.0", title="DSE v1", content="<p>Content</p>"):
"""Create a version directly via SQLAlchemy."""
import uuid as uuid_mod
db = TestingSessionLocal()
doc_uuid = uuid_mod.UUID(document_id) if isinstance(document_id, str) else document_id
v = LegalDocumentVersionDB(
document_id=doc_uuid,
version=version,
title=title,
content=content,
language="de",
status="draft",
)
db.add(v)
db.commit()
db.refresh(v)
result = {"id": str(v.id), "document_id": str(v.document_id), "version": v.version, "status": v.status}
db.close()
return result
def _publish_version(version_id):
"""Directly set version to published via SQLAlchemy."""
import uuid as uuid_mod
db = TestingSessionLocal()
vid = uuid_mod.UUID(version_id) if isinstance(version_id, str) else version_id
v = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == vid).first()
v.status = "published"
v.approved_by = "admin"
v.approved_at = datetime.utcnow()
db.commit()
db.refresh(v)
result = {"id": str(v.id), "status": v.status}
db.close()
return result
# =============================================================================
# Public Endpoints
# =============================================================================
class TestPublicDocuments:
def test_list_public_empty(self):
r = client.get("/api/compliance/legal-documents/public", headers=HEADERS)
assert r.status_code == 200
assert r.json() == []
def test_list_public_only_published(self):
doc = _create_document()
v = _create_version(doc["id"])
# Still draft — should not appear
r = client.get("/api/compliance/legal-documents/public", headers=HEADERS)
assert len(r.json()) == 0
# Publish it
_publish_version(v["id"])
r = client.get("/api/compliance/legal-documents/public", headers=HEADERS)
data = r.json()
assert len(data) == 1
assert data[0]["type"] == "privacy_policy"
assert data[0]["version"] == "1.0"
def test_get_latest_published(self):
doc = _create_document()
v = _create_version(doc["id"])
_publish_version(v["id"])
r = client.get("/api/compliance/legal-documents/public/privacy_policy/latest?language=de", headers=HEADERS)
assert r.status_code == 200
data = r.json()
assert data["type"] == "privacy_policy"
assert data["version"] == "1.0"
def test_get_latest_not_found(self):
r = client.get("/api/compliance/legal-documents/public/nonexistent/latest", headers=HEADERS)
assert r.status_code == 404
# =============================================================================
# User Consents
# =============================================================================
class TestUserConsents:
def test_record_consent(self):
doc = _create_document()
r = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-123",
"document_id": doc["id"],
"document_type": "privacy_policy",
"consented": True,
"ip_address": "1.2.3.4",
}, headers=HEADERS)
assert r.status_code == 200
data = r.json()
assert data["user_id"] == "user-123"
assert data["consented"] is True
assert data["withdrawn_at"] is None
def test_record_consent_doc_not_found(self):
r = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-123",
"document_id": str(uuid.uuid4()),
"document_type": "privacy_policy",
}, headers=HEADERS)
assert r.status_code == 404
def test_get_my_consents(self):
doc = _create_document()
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-A",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-B",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/consents/my?user_id=user-A", headers=HEADERS)
assert r.status_code == 200
assert len(r.json()) == 1
assert r.json()[0]["user_id"] == "user-A"
def test_check_consent_exists(self):
doc = _create_document()
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-X",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/consents/check/privacy_policy?user_id=user-X", headers=HEADERS)
assert r.status_code == 200
assert r.json()["has_consent"] is True
def test_check_consent_not_exists(self):
r = client.get("/api/compliance/legal-documents/consents/check/privacy_policy?user_id=nobody", headers=HEADERS)
assert r.status_code == 200
assert r.json()["has_consent"] is False
def test_withdraw_consent(self):
doc = _create_document()
cr = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-W",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
consent_id = cr.json()["id"]
r = client.delete(f"/api/compliance/legal-documents/consents/{consent_id}", headers=HEADERS)
assert r.status_code == 200
assert r.json()["consented"] is False
assert r.json()["withdrawn_at"] is not None
def test_withdraw_already_withdrawn(self):
doc = _create_document()
cr = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-W2",
"document_id": doc["id"],
"document_type": "terms",
}, headers=HEADERS)
consent_id = cr.json()["id"]
client.delete(f"/api/compliance/legal-documents/consents/{consent_id}", headers=HEADERS)
r = client.delete(f"/api/compliance/legal-documents/consents/{consent_id}", headers=HEADERS)
assert r.status_code == 400
def test_check_after_withdraw(self):
doc = _create_document()
cr = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "user-CW",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
client.delete(f"/api/compliance/legal-documents/consents/{cr.json()['id']}", headers=HEADERS)
r = client.get("/api/compliance/legal-documents/consents/check/privacy_policy?user_id=user-CW", headers=HEADERS)
assert r.json()["has_consent"] is False
# =============================================================================
# Consent Statistics
# =============================================================================
class TestConsentStats:
def test_stats_empty(self):
r = client.get("/api/compliance/legal-documents/stats/consents", headers=HEADERS)
assert r.status_code == 200
data = r.json()
assert data["total"] == 0
assert data["active"] == 0
assert data["withdrawn"] == 0
assert data["unique_users"] == 0
def test_stats_with_data(self):
doc = _create_document()
# Two users consent
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "u1", "document_id": doc["id"], "document_type": "privacy_policy",
}, headers=HEADERS)
cr = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "u2", "document_id": doc["id"], "document_type": "privacy_policy",
}, headers=HEADERS)
# Withdraw one
client.delete(f"/api/compliance/legal-documents/consents/{cr.json()['id']}", headers=HEADERS)
r = client.get("/api/compliance/legal-documents/stats/consents", headers=HEADERS)
data = r.json()
assert data["total"] == 2
assert data["active"] == 1
assert data["withdrawn"] == 1
assert data["unique_users"] == 2
assert data["by_type"]["privacy_policy"] == 2
# =============================================================================
# Audit Log
# =============================================================================
class TestAuditLog:
def test_audit_log_empty(self):
r = client.get("/api/compliance/legal-documents/audit-log", headers=HEADERS)
assert r.status_code == 200
assert r.json()["entries"] == []
def test_audit_log_after_consent(self):
doc = _create_document()
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "audit-user",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/audit-log", headers=HEADERS)
entries = r.json()["entries"]
assert len(entries) >= 1
assert entries[0]["action"] == "consent_given"
def test_audit_log_after_withdraw(self):
doc = _create_document()
cr = client.post("/api/compliance/legal-documents/consents", json={
"user_id": "wd-user",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
client.delete(f"/api/compliance/legal-documents/consents/{cr.json()['id']}", headers=HEADERS)
r = client.get("/api/compliance/legal-documents/audit-log", headers=HEADERS)
actions = [e["action"] for e in r.json()["entries"]]
assert "consent_given" in actions
assert "consent_withdrawn" in actions
def test_audit_log_filter(self):
doc = _create_document()
client.post("/api/compliance/legal-documents/consents", json={
"user_id": "f-user",
"document_id": doc["id"],
"document_type": "terms",
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/audit-log?action=consent_given", headers=HEADERS)
assert r.json()["total"] >= 1
for e in r.json()["entries"]:
assert e["action"] == "consent_given"
def test_audit_log_pagination(self):
doc = _create_document()
for i in range(5):
client.post("/api/compliance/legal-documents/consents", json={
"user_id": f"p-user-{i}",
"document_id": doc["id"],
"document_type": "privacy_policy",
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/audit-log?limit=2&offset=0", headers=HEADERS)
data = r.json()
assert data["total"] == 5
assert len(data["entries"]) == 2
# =============================================================================
# Cookie Categories
# =============================================================================
class TestCookieCategories:
def test_list_empty(self):
r = client.get("/api/compliance/legal-documents/cookie-categories", headers=HEADERS)
assert r.status_code == 200
assert r.json() == []
def test_create_category(self):
r = client.post("/api/compliance/legal-documents/cookie-categories", json={
"name_de": "Notwendig",
"name_en": "Necessary",
"is_required": True,
"sort_order": 0,
}, headers=HEADERS)
assert r.status_code == 200
data = r.json()
assert data["name_de"] == "Notwendig"
assert data["is_required"] is True
def test_list_ordered(self):
client.post("/api/compliance/legal-documents/cookie-categories", json={
"name_de": "Marketing", "sort_order": 30,
}, headers=HEADERS)
client.post("/api/compliance/legal-documents/cookie-categories", json={
"name_de": "Notwendig", "sort_order": 0,
}, headers=HEADERS)
r = client.get("/api/compliance/legal-documents/cookie-categories", headers=HEADERS)
data = r.json()
assert len(data) == 2
assert data[0]["name_de"] == "Notwendig"
assert data[1]["name_de"] == "Marketing"
def test_update_category(self):
cr = client.post("/api/compliance/legal-documents/cookie-categories", json={
"name_de": "Analyse", "sort_order": 20,
}, headers=HEADERS)
cat_id = cr.json()["id"]
r = client.put(f"/api/compliance/legal-documents/cookie-categories/{cat_id}", json={
"name_de": "Analytics", "description_de": "Tracking-Cookies",
}, headers=HEADERS)
assert r.status_code == 200
assert r.json()["name_de"] == "Analytics"
assert r.json()["description_de"] == "Tracking-Cookies"
def test_update_not_found(self):
r = client.put(f"/api/compliance/legal-documents/cookie-categories/{uuid.uuid4()}", json={
"name_de": "X",
}, headers=HEADERS)
assert r.status_code == 404
def test_delete_category(self):
cr = client.post("/api/compliance/legal-documents/cookie-categories", json={
"name_de": "Temp",
}, headers=HEADERS)
cat_id = cr.json()["id"]
r = client.delete(f"/api/compliance/legal-documents/cookie-categories/{cat_id}", headers=HEADERS)
assert r.status_code == 204
r = client.get("/api/compliance/legal-documents/cookie-categories", headers=HEADERS)
assert len(r.json()) == 0
def test_delete_not_found(self):
r = client.delete(f"/api/compliance/legal-documents/cookie-categories/{uuid.uuid4()}", headers=HEADERS)
assert r.status_code == 404