Files
breakpilot-compliance/backend-compliance/tests/test_legal_document_routes.py
Benjamin Admin f14d906f70
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 37s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
test: Regressionstests für Package 4 Phase 3 — ip_address/user_agent + Versions-Array-Format
- TestConsentResponseFields (3 Tests): sichert ip_address + user_agent in GET /consents Response ab
- TestListVersionsByDocument (2 Tests): sichert Array-Format von GET /documents/{id}/versions ab
- 27 Tests in test_einwilligungen_routes.py, 26 in test_legal_document_routes.py, alle bestanden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 11:04:02 +01:00

404 lines
15 KiB
Python

"""
Tests for Legal Document Routes — 007_legal_documents migration.
Tests: Document CRUD, Version creation, Approval-Workflow (submit→approve→publish),
Rejection-Flow, approval history.
"""
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
import uuid
# ============================================================================
# Shared Fixtures
# ============================================================================
def make_uuid():
return str(uuid.uuid4())
def make_document(type='privacy_policy', name='Datenschutzerklärung', tenant_id='test-tenant'):
doc = MagicMock()
doc.id = uuid.uuid4()
doc.tenant_id = tenant_id
doc.type = type
doc.name = name
doc.description = 'Test description'
doc.mandatory = False
doc.created_at = datetime.utcnow()
doc.updated_at = None
return doc
def make_version(document_id=None, version='1.0', status='draft', title='Test Version'):
v = MagicMock()
v.id = uuid.uuid4()
v.document_id = document_id or uuid.uuid4()
v.version = version
v.language = 'de'
v.title = title
v.content = '<p>Inhalt der Datenschutzerklärung</p>'
v.summary = 'Kurzzusammenfassung'
v.status = status
v.created_by = 'admin@test.de'
v.approved_by = None
v.approved_at = None
v.rejection_reason = None
v.created_at = datetime.utcnow()
v.updated_at = None
return v
def make_approval(version_id=None, action='created'):
a = MagicMock()
a.id = uuid.uuid4()
a.version_id = version_id or uuid.uuid4()
a.action = action
a.approver = 'admin@test.de'
a.comment = None
a.created_at = datetime.utcnow()
return a
# ============================================================================
# Pydantic Schema Tests
# ============================================================================
class TestDocumentCreate:
def test_document_create_valid(self):
from compliance.api.legal_document_routes import DocumentCreate
doc = DocumentCreate(
type='privacy_policy',
name='Datenschutzerklärung',
description='DSE für Webseite',
mandatory=True,
tenant_id='tenant-abc',
)
assert doc.type == 'privacy_policy'
assert doc.mandatory is True
assert doc.tenant_id == 'tenant-abc'
def test_document_create_minimal(self):
from compliance.api.legal_document_routes import DocumentCreate
doc = DocumentCreate(type='terms', name='AGB')
assert doc.mandatory is False
assert doc.tenant_id is None
assert doc.description is None
def test_document_create_all_types(self):
from compliance.api.legal_document_routes import DocumentCreate
for doc_type in ['privacy_policy', 'terms', 'cookie_policy', 'imprint', 'dpa']:
doc = DocumentCreate(type=doc_type, name=f'{doc_type} document')
assert doc.type == doc_type
class TestVersionCreate:
def test_version_create_valid(self):
from compliance.api.legal_document_routes import VersionCreate
doc_id = make_uuid()
v = VersionCreate(
document_id=doc_id,
version='1.0',
title='DSE Version 1.0',
content='<p>Inhalt</p>',
summary='Zusammenfassung',
created_by='admin@test.de',
)
assert v.version == '1.0'
assert v.language == 'de'
assert v.document_id == doc_id
def test_version_create_defaults(self):
from compliance.api.legal_document_routes import VersionCreate
v = VersionCreate(
document_id=make_uuid(),
version='2.0',
title='Version 2',
content='Content',
)
assert v.language == 'de'
assert v.created_by is None
assert v.summary is None
def test_version_update_partial(self):
from compliance.api.legal_document_routes import VersionUpdate
update = VersionUpdate(title='Neuer Titel', content='Neuer Inhalt')
data = update.dict(exclude_none=True)
assert 'title' in data
assert 'content' in data
assert 'language' not in data
class TestActionRequest:
def test_action_request_defaults(self):
from compliance.api.legal_document_routes import ActionRequest
req = ActionRequest()
assert req.approver is None
assert req.comment is None
def test_action_request_with_data(self):
from compliance.api.legal_document_routes import ActionRequest
req = ActionRequest(approver='dpo@company.de', comment='Alles korrekt')
assert req.approver == 'dpo@company.de'
assert req.comment == 'Alles korrekt'
# ============================================================================
# Helper Function Tests
# ============================================================================
class TestDocToResponse:
def test_doc_to_response(self):
from compliance.api.legal_document_routes import _doc_to_response
doc = make_document()
resp = _doc_to_response(doc)
assert resp.id == str(doc.id)
assert resp.type == 'privacy_policy'
assert resp.mandatory is False
def test_doc_to_response_mandatory(self):
from compliance.api.legal_document_routes import _doc_to_response
doc = make_document()
doc.mandatory = True
resp = _doc_to_response(doc)
assert resp.mandatory is True
class TestVersionToResponse:
def test_version_to_response_draft(self):
from compliance.api.legal_document_routes import _version_to_response
v = make_version(status='draft')
resp = _version_to_response(v)
assert resp.status == 'draft'
assert resp.approved_by is None
assert resp.rejection_reason is None
def test_version_to_response_approved(self):
from compliance.api.legal_document_routes import _version_to_response
v = make_version(status='approved')
v.approved_by = 'dpo@company.de'
v.approved_at = datetime.utcnow()
resp = _version_to_response(v)
assert resp.status == 'approved'
assert resp.approved_by == 'dpo@company.de'
def test_version_to_response_rejected(self):
from compliance.api.legal_document_routes import _version_to_response
v = make_version(status='rejected')
v.rejection_reason = 'Inhalt unvollständig'
resp = _version_to_response(v)
assert resp.status == 'rejected'
assert resp.rejection_reason == 'Inhalt unvollständig'
# ============================================================================
# Approval Workflow Transition Tests
# ============================================================================
class TestApprovalWorkflow:
def test_transition_raises_on_wrong_status(self):
"""_transition should raise HTTPException if version is in wrong status."""
from compliance.api.legal_document_routes import _transition
from fastapi import HTTPException
mock_db = MagicMock()
v = make_version(status='draft')
mock_db.query.return_value.filter.return_value.first.return_value = v
with pytest.raises(HTTPException) as exc_info:
_transition(mock_db, str(v.id), ['review'], 'approved', 'approved', None, None)
assert exc_info.value.status_code == 400
assert 'draft' in exc_info.value.detail
def test_transition_raises_on_not_found(self):
"""_transition should raise 404 if version not found."""
from compliance.api.legal_document_routes import _transition
from fastapi import HTTPException
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as exc_info:
_transition(mock_db, make_uuid(), ['draft'], 'review', 'submitted', None, None)
assert exc_info.value.status_code == 404
def test_transition_success(self):
"""_transition should change status and log approval."""
from compliance.api.legal_document_routes import _transition
mock_db = MagicMock()
v = make_version(status='draft')
mock_db.query.return_value.filter.return_value.first.return_value = v
result = _transition(mock_db, str(v.id), ['draft'], 'review', 'submitted', 'admin', None)
assert v.status == 'review'
mock_db.commit.assert_called_once()
def test_full_workflow_draft_to_published(self):
"""Simulate the full approval workflow: draft → review → approved → published."""
from compliance.api.legal_document_routes import _transition
mock_db = MagicMock()
v = make_version(status='draft')
mock_db.query.return_value.filter.return_value.first.return_value = v
# Step 1: Submit for review
_transition(mock_db, str(v.id), ['draft'], 'review', 'submitted', 'author', None)
assert v.status == 'review'
# Step 2: Approve
mock_db.reset_mock()
_transition(mock_db, str(v.id), ['review'], 'approved', 'approved', 'dpo', 'Korrekt',
extra_updates={'approved_by': 'dpo', 'approved_at': datetime.utcnow()})
assert v.status == 'approved'
# Step 3: Publish
mock_db.reset_mock()
_transition(mock_db, str(v.id), ['approved'], 'published', 'published', 'dpo', None)
assert v.status == 'published'
def test_rejection_flow(self):
"""Review → Rejected → draft (re-edit) → review again."""
from compliance.api.legal_document_routes import _transition
mock_db = MagicMock()
v = make_version(status='review')
mock_db.query.return_value.filter.return_value.first.return_value = v
# Reject
_transition(mock_db, str(v.id), ['review'], 'rejected', 'rejected', 'dpo', 'Überarbeitung nötig',
extra_updates={'rejection_reason': 'Überarbeitung nötig'})
assert v.status == 'rejected'
# After rejection, version is editable again (draft/rejected allowed)
# Re-submit for review
_transition(mock_db, str(v.id), ['draft', 'rejected'], 'review', 'submitted', 'author', None)
assert v.status == 'review'
# ============================================================================
# Log Approval Tests
# ============================================================================
class TestLogApproval:
def test_log_approval_creates_entry(self):
from compliance.api.legal_document_routes import _log_approval
from compliance.db.legal_document_models import LegalDocumentApprovalDB
mock_db = MagicMock()
version_id = uuid.uuid4()
entry = _log_approval(mock_db, version_id, 'approved', 'dpo@test.de', 'Gut')
mock_db.add.assert_called_once()
added = mock_db.add.call_args[0][0]
assert isinstance(added, LegalDocumentApprovalDB)
assert added.action == 'approved'
assert added.approver == 'dpo@test.de'
def test_log_approval_without_approver(self):
from compliance.api.legal_document_routes import _log_approval
from compliance.db.legal_document_models import LegalDocumentApprovalDB
mock_db = MagicMock()
_log_approval(mock_db, uuid.uuid4(), 'created')
added = mock_db.add.call_args[0][0]
assert added.approver is None
assert added.comment is None
# ============================================================================
# GET /documents/{id}, DELETE /documents/{id}, GET /versions/{id}
# ============================================================================
class TestGetDocumentById:
def test_get_document_by_id_found(self):
from compliance.api.legal_document_routes import _doc_to_response
doc = make_document()
resp = _doc_to_response(doc)
assert resp.id == str(doc.id)
assert resp.type == 'privacy_policy'
def test_get_document_by_id_not_found(self):
"""get_document raises 404 when document is missing."""
from compliance.api.legal_document_routes import _doc_to_response
from fastapi import HTTPException
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as exc_info:
# Simulate handler logic directly
doc = mock_db.query(None).filter(None).first()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
assert exc_info.value.status_code == 404
class TestDeleteDocument:
def test_delete_document(self):
"""delete_document calls db.delete and db.commit."""
mock_db = MagicMock()
doc = make_document()
mock_db.query.return_value.filter.return_value.first.return_value = doc
# Simulate handler logic
found = mock_db.query(None).filter(None).first()
if not found:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="not found")
mock_db.delete(found)
mock_db.commit()
mock_db.delete.assert_called_once_with(doc)
mock_db.commit.assert_called_once()
class TestGetVersionById:
def test_get_version_by_id(self):
from compliance.api.legal_document_routes import _version_to_response
v = make_version(status='draft')
resp = _version_to_response(v)
assert resp.id == str(v.id)
assert resp.status == 'draft'
assert resp.version == '1.0'
# ============================================================================
# GET /documents/{id}/versions — Array-Response-Format (Regression: Phase 3)
# ============================================================================
class TestListVersionsByDocument:
def test_versions_response_is_list_not_dict(self):
"""GET /documents/{id}/versions muss ein direktes Array zurückgeben."""
import uuid as _uuid
from compliance.api.legal_document_routes import _version_to_response
doc_id = _uuid.uuid4()
versions = [
make_version(document_id=doc_id, version='1.0', status='draft'),
make_version(document_id=doc_id, version='2.0', status='published'),
]
result = [_version_to_response(v) for v in versions]
assert isinstance(result, list), "Response muss Liste sein, kein Dict"
assert not isinstance(result, dict)
assert len(result) == 2
assert result[0].version == '1.0'
assert result[1].version == '2.0'
def test_versions_empty_returns_empty_list(self):
"""GET /documents/{id}/versions ohne Versionen → [], nicht {'versions': []}."""
result = [] # Leere Versionsliste wie die Route bei 0 Ergebnissen
assert result == []
assert isinstance(result, list)