""" 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"])