fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
287
backend/tests/test_gdpr_api.py
Normal file
287
backend/tests/test_gdpr_api.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
Tests für die GDPR API Endpoints
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestDataCategories:
|
||||
"""Tests für Datenkategorien-Endpoint"""
|
||||
|
||||
def test_data_categories_structure(self):
|
||||
"""Test that data categories have correct structure"""
|
||||
# Import the data categories from the GDPR API
|
||||
try:
|
||||
from gdpr_api import DATA_CATEGORIES
|
||||
except ImportError:
|
||||
pytest.skip("gdpr_api module not available")
|
||||
|
||||
assert "essential" in DATA_CATEGORIES
|
||||
assert "optional" in DATA_CATEGORIES
|
||||
|
||||
# Check essential categories
|
||||
for category in DATA_CATEGORIES["essential"]:
|
||||
assert "name" in category
|
||||
assert "description" in category
|
||||
assert "retention_days" in category
|
||||
assert "legal_basis" in category
|
||||
|
||||
# Check optional categories
|
||||
for category in DATA_CATEGORIES["optional"]:
|
||||
assert "name" in category
|
||||
assert "description" in category
|
||||
assert "retention_days" in category
|
||||
assert "cookie_category" in category
|
||||
|
||||
def test_retention_days_values(self):
|
||||
"""Test that retention days are reasonable"""
|
||||
try:
|
||||
from gdpr_api import DATA_CATEGORIES
|
||||
except ImportError:
|
||||
pytest.skip("gdpr_api module not available")
|
||||
|
||||
all_categories = DATA_CATEGORIES["essential"] + DATA_CATEGORIES["optional"]
|
||||
|
||||
for category in all_categories:
|
||||
retention = category.get("retention_days")
|
||||
if retention is not None and isinstance(retention, int):
|
||||
assert retention > 0, f"Retention for {category['name']} should be positive"
|
||||
assert retention <= 3650, f"Retention for {category['name']} shouldn't exceed 10 years"
|
||||
|
||||
|
||||
class TestGDPRCompliance:
|
||||
"""Tests für GDPR Compliance"""
|
||||
|
||||
def test_gdpr_rights_covered(self):
|
||||
"""Test that all GDPR rights are addressable"""
|
||||
gdpr_rights = {
|
||||
"art_15": "Right of access", # Auskunftsrecht
|
||||
"art_16": "Right to rectification", # Berichtigungsrecht
|
||||
"art_17": "Right to erasure", # Löschungsrecht
|
||||
"art_18": "Right to restriction", # Einschränkungsrecht
|
||||
"art_20": "Right to portability", # Datenübertragbarkeit
|
||||
"art_21": "Right to object", # Widerspruchsrecht
|
||||
}
|
||||
|
||||
# These should all be implementable via the consent service
|
||||
for article, right in gdpr_rights.items():
|
||||
assert right is not None, f"GDPR {article} ({right}) should be covered"
|
||||
|
||||
def test_mandatory_documents(self):
|
||||
"""Test that mandatory legal documents are defined"""
|
||||
mandatory_docs = ["terms", "privacy"]
|
||||
|
||||
for doc in mandatory_docs:
|
||||
assert doc in mandatory_docs, f"Document {doc} should be mandatory"
|
||||
|
||||
def test_cookie_categories_defined(self):
|
||||
"""Test that cookie categories follow GDPR requirements"""
|
||||
expected_categories = ["necessary", "functional", "analytics", "marketing"]
|
||||
|
||||
# Necessary cookies must be allowed without consent
|
||||
assert "necessary" in expected_categories
|
||||
|
||||
# Optional categories require consent
|
||||
optional = [c for c in expected_categories if c != "necessary"]
|
||||
assert len(optional) > 0
|
||||
|
||||
|
||||
class TestRetentionPolicies:
|
||||
"""Tests für Löschfristen"""
|
||||
|
||||
def test_session_data_retention(self):
|
||||
"""Test that session data has short retention"""
|
||||
session_retention_days = 1 # Expected: 24 hours max
|
||||
assert session_retention_days <= 7, "Session data should be retained for max 7 days"
|
||||
|
||||
def test_audit_log_retention(self):
|
||||
"""Test audit log retention complies with legal requirements"""
|
||||
# Audit logs must be kept for compliance but not indefinitely
|
||||
audit_retention_days = 1095 # 3 years
|
||||
assert audit_retention_days >= 365, "Audit logs should be kept for at least 1 year"
|
||||
assert audit_retention_days <= 3650, "Audit logs shouldn't be kept more than 10 years"
|
||||
|
||||
def test_consent_record_retention(self):
|
||||
"""Test that consent records are kept long enough for proof"""
|
||||
# § 7a UWG requires proof of consent for 3 years
|
||||
consent_retention_days = 1095
|
||||
assert consent_retention_days >= 1095, "Consent records must be kept for at least 3 years"
|
||||
|
||||
def test_ip_address_retention(self):
|
||||
"""Test IP address retention is minimized"""
|
||||
ip_retention_days = 28 # 4 weeks
|
||||
assert ip_retention_days <= 90, "IP addresses should not be stored longer than 90 days"
|
||||
|
||||
|
||||
class TestDataMinimization:
|
||||
"""Tests für Datensparsamkeit"""
|
||||
|
||||
def test_password_not_stored_plain(self):
|
||||
"""Test that passwords are never stored in plain text"""
|
||||
# This is a design requirement test
|
||||
assert True, "Passwords must be hashed with bcrypt"
|
||||
|
||||
def test_unnecessary_data_not_collected(self):
|
||||
"""Test that only necessary data is collected"""
|
||||
# User model should only contain necessary fields
|
||||
required_fields = ["id", "email", "password_hash", "created_at"]
|
||||
optional_fields = ["name", "role"]
|
||||
|
||||
# No excessive personal data
|
||||
forbidden_fields = ["ssn", "credit_card", "date_of_birth", "address"]
|
||||
|
||||
for field in forbidden_fields:
|
||||
assert field not in required_fields, f"Field {field} should not be required"
|
||||
|
||||
|
||||
class TestAnonymization:
|
||||
"""Tests für Anonymisierung"""
|
||||
|
||||
def test_ip_anonymization(self):
|
||||
"""Test IP address anonymization logic"""
|
||||
def anonymize_ip(ip: str) -> str:
|
||||
"""Anonymize IPv4 by zeroing last octet"""
|
||||
parts = ip.split(".")
|
||||
if len(parts) == 4:
|
||||
parts[3] = "0"
|
||||
return ".".join(parts)
|
||||
return ip
|
||||
|
||||
test_cases = [
|
||||
("192.168.1.100", "192.168.1.0"),
|
||||
("10.0.0.1", "10.0.0.0"),
|
||||
("172.16.255.255", "172.16.255.0"),
|
||||
]
|
||||
|
||||
for original, expected in test_cases:
|
||||
assert anonymize_ip(original) == expected
|
||||
|
||||
def test_user_data_anonymization(self):
|
||||
"""Test user data anonymization for deleted accounts"""
|
||||
def anonymize_user_data(user_data: dict) -> dict:
|
||||
"""Anonymize user data while keeping audit trail"""
|
||||
anonymized = user_data.copy()
|
||||
anonymized["email"] = f"deleted-{user_data['id']}@anonymized.local"
|
||||
anonymized["name"] = None
|
||||
anonymized["password_hash"] = None
|
||||
return anonymized
|
||||
|
||||
original = {
|
||||
"id": "123",
|
||||
"email": "real@example.com",
|
||||
"name": "John Doe",
|
||||
"password_hash": "bcrypt_hash"
|
||||
}
|
||||
|
||||
anonymized = anonymize_user_data(original)
|
||||
|
||||
assert "@anonymized.local" in anonymized["email"]
|
||||
assert anonymized["name"] is None
|
||||
assert anonymized["password_hash"] is None
|
||||
assert anonymized["id"] == original["id"] # ID preserved for audit
|
||||
|
||||
|
||||
class TestExportFormat:
|
||||
"""Tests für Datenexport-Format"""
|
||||
|
||||
def test_export_includes_all_user_data(self):
|
||||
"""Test that export includes all required data sections"""
|
||||
required_sections = [
|
||||
"user", # Personal data
|
||||
"consents", # Consent history
|
||||
"cookie_consents", # Cookie preferences
|
||||
"audit_log", # Activity log
|
||||
"exported_at", # Export timestamp
|
||||
]
|
||||
|
||||
# Mock export response
|
||||
mock_export = {
|
||||
"user": {},
|
||||
"consents": [],
|
||||
"cookie_consents": [],
|
||||
"audit_log": [],
|
||||
"exported_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
for section in required_sections:
|
||||
assert section in mock_export, f"Export must include {section}"
|
||||
|
||||
def test_export_is_machine_readable(self):
|
||||
"""Test that export can be provided in machine-readable format"""
|
||||
import json
|
||||
|
||||
mock_data = {
|
||||
"user": {"email": "test@example.com"},
|
||||
"exported_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
# Should be valid JSON
|
||||
json_str = json.dumps(mock_data)
|
||||
parsed = json.loads(json_str)
|
||||
|
||||
assert parsed == mock_data
|
||||
|
||||
|
||||
class TestConsentValidation:
|
||||
"""Tests für Consent-Validierung"""
|
||||
|
||||
def test_consent_requires_version_id(self):
|
||||
"""Test that consent requires a specific document version"""
|
||||
consent_request = {
|
||||
"document_type": "terms",
|
||||
"version_id": "version-123",
|
||||
"consented": True
|
||||
}
|
||||
|
||||
assert "version_id" in consent_request
|
||||
assert consent_request["version_id"] is not None
|
||||
|
||||
def test_consent_tracks_ip_and_timestamp(self):
|
||||
"""Test that consent tracks IP and timestamp for proof"""
|
||||
consent_record = {
|
||||
"user_id": "user-123",
|
||||
"document_version_id": "version-123",
|
||||
"consented": True,
|
||||
"ip_address": "192.168.1.1",
|
||||
"consented_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert "ip_address" in consent_record
|
||||
assert "consented_at" in consent_record
|
||||
|
||||
def test_withdrawal_is_possible(self):
|
||||
"""Test that consent can be withdrawn (Art. 7(3) GDPR)"""
|
||||
# Withdrawal should be as easy as giving consent
|
||||
withdraw_request = {
|
||||
"consent_id": "consent-123"
|
||||
}
|
||||
|
||||
assert "consent_id" in withdraw_request
|
||||
|
||||
|
||||
class TestSecurityHeaders:
|
||||
"""Tests für Sicherheits-Header"""
|
||||
|
||||
def test_required_security_headers(self):
|
||||
"""Test that API responses include security headers"""
|
||||
required_headers = [
|
||||
"X-Content-Type-Options", # nosniff
|
||||
"X-Frame-Options", # DENY or SAMEORIGIN
|
||||
"Content-Security-Policy", # CSP
|
||||
"X-XSS-Protection", # Legacy but useful
|
||||
]
|
||||
|
||||
# These should be set by the application
|
||||
for header in required_headers:
|
||||
assert header is not None, f"Header {header} should be set"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user