""" Tests for Einwilligungen Routes — 008_einwilligungen migration. Tests: Catalog Upsert, Company Info, Cookie-Config, Consent erfassen, Consent widerrufen, Statistiken. """ 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_catalog(tenant_id='test-tenant'): rec = MagicMock() rec.id = uuid.uuid4() rec.tenant_id = tenant_id rec.selected_data_point_ids = ['dp-001', 'dp-002'] rec.custom_data_points = [] rec.updated_at = datetime.utcnow() return rec def make_company(tenant_id='test-tenant'): rec = MagicMock() rec.id = uuid.uuid4() rec.tenant_id = tenant_id rec.data = {'company_name': 'Test GmbH', 'email': 'datenschutz@test.de'} rec.updated_at = datetime.utcnow() return rec def make_cookies(tenant_id='test-tenant'): rec = MagicMock() rec.id = uuid.uuid4() rec.tenant_id = tenant_id rec.categories = [ {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True}, {'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': False}, ] rec.config = {'position': 'bottom', 'style': 'bar'} rec.updated_at = datetime.utcnow() return rec def make_consent(tenant_id='test-tenant', user_id='user-001', data_point_id='dp-001', granted=True): rec = MagicMock() rec.id = uuid.uuid4() rec.tenant_id = tenant_id rec.user_id = user_id rec.data_point_id = data_point_id rec.granted = granted rec.granted_at = datetime.utcnow() rec.revoked_at = None rec.consent_version = '1.0' rec.source = 'website' rec.ip_address = None rec.user_agent = None rec.created_at = datetime.utcnow() return rec # ============================================================================ # Pydantic Schema Tests # ============================================================================ class TestCatalogUpsert: def test_catalog_upsert_defaults(self): from compliance.api.einwilligungen_routes import CatalogUpsert data = CatalogUpsert() assert data.selected_data_point_ids == [] assert data.custom_data_points == [] def test_catalog_upsert_with_data(self): from compliance.api.einwilligungen_routes import CatalogUpsert data = CatalogUpsert( selected_data_point_ids=['dp-001', 'dp-002', 'dp-003'], custom_data_points=[{'id': 'custom-1', 'name': 'Eigener Datenpunkt'}], ) assert len(data.selected_data_point_ids) == 3 assert len(data.custom_data_points) == 1 class TestCompanyUpsert: def test_company_upsert_empty(self): from compliance.api.einwilligungen_routes import CompanyUpsert data = CompanyUpsert() assert data.data == {} def test_company_upsert_with_data(self): from compliance.api.einwilligungen_routes import CompanyUpsert data = CompanyUpsert(data={ 'company_name': 'Test GmbH', 'address': 'Musterstraße 1, 12345 Berlin', 'email': 'datenschutz@test.de', 'dpo_name': 'Max Mustermann', }) assert data.data['company_name'] == 'Test GmbH' assert data.data['dpo_name'] == 'Max Mustermann' class TestCookiesUpsert: def test_cookies_upsert_defaults(self): from compliance.api.einwilligungen_routes import CookiesUpsert data = CookiesUpsert() assert data.categories == [] assert data.config == {} def test_cookies_upsert_with_categories(self): from compliance.api.einwilligungen_routes import CookiesUpsert data = CookiesUpsert( categories=[ {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True}, {'id': 'analytics', 'name': 'Analyse', 'isRequired': False}, ], config={'position': 'bottom'}, ) assert len(data.categories) == 2 assert data.config['position'] == 'bottom' class TestConsentCreate: def test_consent_create_valid(self): from compliance.api.einwilligungen_routes import ConsentCreate data = ConsentCreate( user_id='user-001', data_point_id='dp-marketing', granted=True, ) assert data.user_id == 'user-001' assert data.granted is True assert data.consent_version == '1.0' assert data.source is None def test_consent_create_revoke(self): from compliance.api.einwilligungen_routes import ConsentCreate data = ConsentCreate( user_id='user-001', data_point_id='dp-analytics', granted=False, consent_version='2.0', source='cookie-banner', ) assert data.granted is False assert data.consent_version == '2.0' # ============================================================================ # Catalog Upsert Tests # ============================================================================ class TestCatalogDB: def test_catalog_returns_empty_when_not_found(self): """GET catalog should return empty defaults when no record exists.""" from compliance.db.einwilligungen_models import EinwilligungenCatalogDB mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None result = mock_db.query(EinwilligungenCatalogDB).filter().first() assert result is None def test_catalog_upsert_creates_new(self): """PUT catalog should create a new record if none exists.""" from compliance.db.einwilligungen_models import EinwilligungenCatalogDB mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None new_record = EinwilligungenCatalogDB( tenant_id='test-tenant', selected_data_point_ids=['dp-001'], custom_data_points=[], ) assert new_record.tenant_id == 'test-tenant' assert new_record.selected_data_point_ids == ['dp-001'] def test_catalog_upsert_updates_existing(self): """PUT catalog should update existing record.""" existing = make_catalog() existing.selected_data_point_ids = ['dp-001', 'dp-002', 'dp-003'] existing.custom_data_points = [{'id': 'custom-1'}] assert len(existing.selected_data_point_ids) == 3 assert len(existing.custom_data_points) == 1 # ============================================================================ # Cookie Config Tests # ============================================================================ class TestCookieConfig: def test_cookie_config_returns_empty_when_not_found(self): """GET cookies should return empty defaults for new tenant.""" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None result = mock_db.query().filter().first() assert result is None def test_cookie_config_upsert_with_categories(self): """PUT cookies should store categories and config.""" from compliance.db.einwilligungen_models import EinwilligungenCookiesDB categories = [ {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True}, ] config = {'position': 'bottom', 'primaryColor': '#6366f1'} rec = EinwilligungenCookiesDB( tenant_id='test-tenant', categories=categories, config=config, ) assert rec.categories[0]['id'] == 'necessary' assert rec.config['position'] == 'bottom' def test_essential_cookies_cannot_be_disabled(self): """Category with isRequired=True should not allow enabled=False.""" categories = [ {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True}, {'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': True}, ] # Simulate the toggle logic category_id = 'necessary' enabled = False updated = [] for cat in categories: if cat['id'] == category_id: if cat.get('isRequired') and not enabled: updated.append(cat) # Not changed else: updated.append({**cat, 'defaultEnabled': enabled}) else: updated.append(cat) necessary_cat = next(c for c in updated if c['id'] == 'necessary') assert necessary_cat['defaultEnabled'] is True # Not changed # ============================================================================ # Consent Tests # ============================================================================ class TestConsentDB: def test_consent_record_creation(self): """Consent record should store all required fields.""" from compliance.db.einwilligungen_models import EinwilligungenConsentDB consent = EinwilligungenConsentDB( tenant_id='test-tenant', user_id='user-001', data_point_id='dp-marketing', granted=True, granted_at=datetime.utcnow(), consent_version='1.0', source='website', ) assert consent.tenant_id == 'test-tenant' assert consent.granted is True assert consent.revoked_at is None def test_consent_revoke_sets_revoked_at(self): """Revoking a consent should set revoked_at timestamp.""" consent = make_consent() assert consent.revoked_at is None consent.revoked_at = datetime.utcnow() assert consent.revoked_at is not None def test_cannot_revoke_already_revoked(self): """Should not be possible to revoke an already revoked consent.""" consent = make_consent() consent.revoked_at = datetime.utcnow() # Simulate the guard logic from the route already_revoked = consent.revoked_at is not None assert already_revoked is True # Route would raise HTTPException 400 # ============================================================================ # Statistics Tests # ============================================================================ class TestConsentStats: def test_stats_empty_tenant(self): """Stats for tenant with no consents should return zeros.""" consents = [] total = len(consents) active = sum(1 for c in consents if c.granted and not c.revoked_at) revoked = sum(1 for c in consents if c.revoked_at) unique_users = len(set(c.user_id for c in consents)) assert total == 0 assert active == 0 assert revoked == 0 assert unique_users == 0 def test_stats_with_mixed_consents(self): """Stats should correctly count active and revoked consents.""" consents = [ make_consent(user_id='user-1', data_point_id='dp-1', granted=True), make_consent(user_id='user-1', data_point_id='dp-2', granted=True), make_consent(user_id='user-2', data_point_id='dp-1', granted=True), ] # Revoke one consents[1].revoked_at = datetime.utcnow() total = len(consents) active = sum(1 for c in consents if c.granted and not c.revoked_at) revoked = sum(1 for c in consents if c.revoked_at) unique_users = len(set(c.user_id for c in consents)) assert total == 3 assert active == 2 assert revoked == 1 assert unique_users == 2 def test_stats_conversion_rate(self): """Conversion rate = users with active consent / total unique users.""" consents = [ make_consent(user_id='user-1', granted=True), make_consent(user_id='user-2', granted=True), make_consent(user_id='user-3', granted=True), ] consents[2].revoked_at = datetime.utcnow() # user-3 revoked unique_users = len(set(c.user_id for c in consents)) users_with_active = len(set(c.user_id for c in consents if c.granted and not c.revoked_at)) rate = round((users_with_active / unique_users * 100), 1) if unique_users > 0 else 0.0 assert unique_users == 3 assert users_with_active == 2 assert rate == pytest.approx(66.7, 0.1) def test_stats_by_data_point(self): """Stats should group consents by data_point_id.""" consents = [ make_consent(data_point_id='dp-marketing', granted=True), make_consent(data_point_id='dp-marketing', granted=True), make_consent(data_point_id='dp-analytics', granted=True), ] by_dp: dict = {} for c in consents: dp = c.data_point_id if dp not in by_dp: by_dp[dp] = {'total': 0, 'active': 0} by_dp[dp]['total'] += 1 if c.granted and not c.revoked_at: by_dp[dp]['active'] += 1 assert by_dp['dp-marketing']['total'] == 2 assert by_dp['dp-analytics']['total'] == 1 # ============================================================================ # Model Repr Tests # ============================================================================ class TestModelReprs: def test_catalog_repr(self): from compliance.db.einwilligungen_models import EinwilligungenCatalogDB rec = EinwilligungenCatalogDB(tenant_id='my-tenant') assert 'my-tenant' in repr(rec) def test_cookies_repr(self): from compliance.db.einwilligungen_models import EinwilligungenCookiesDB rec = EinwilligungenCookiesDB(tenant_id='my-tenant') assert 'my-tenant' in repr(rec) def test_consent_repr(self): from compliance.db.einwilligungen_models import EinwilligungenConsentDB rec = EinwilligungenConsentDB( tenant_id='t1', user_id='u1', data_point_id='dp1', granted=True ) assert 'u1' in repr(rec) assert 'dp1' in repr(rec) # ============================================================================ # Consent Response Field Tests (Regression: Phase 3 — ip_address + user_agent) # ============================================================================ class TestConsentResponseFields: def test_consent_response_includes_ip_address(self): """GET /consents Serialisierung muss ip_address enthalten.""" c = make_consent() c.ip_address = '10.0.0.1' row = { "id": str(c.id), "tenant_id": c.tenant_id, "user_id": c.user_id, "data_point_id": c.data_point_id, "granted": c.granted, "granted_at": c.granted_at, "revoked_at": c.revoked_at, "consent_version": c.consent_version, "source": c.source, "ip_address": c.ip_address, "user_agent": c.user_agent, "created_at": c.created_at, } assert "ip_address" in row assert row["ip_address"] == '10.0.0.1' def test_consent_response_includes_user_agent(self): """GET /consents Serialisierung muss user_agent enthalten.""" c = make_consent() c.user_agent = 'Mozilla/5.0 (Test)' row = { "id": str(c.id), "tenant_id": c.tenant_id, "user_id": c.user_id, "data_point_id": c.data_point_id, "granted": c.granted, "granted_at": c.granted_at, "revoked_at": c.revoked_at, "consent_version": c.consent_version, "source": c.source, "ip_address": c.ip_address, "user_agent": c.user_agent, "created_at": c.created_at, } assert "user_agent" in row assert row["user_agent"] == 'Mozilla/5.0 (Test)' def test_consent_response_ip_and_ua_none_by_default(self): """ip_address und user_agent sind None wenn nicht gesetzt (make_consent Default).""" c = make_consent() row = {"ip_address": c.ip_address, "user_agent": c.user_agent} assert row["ip_address"] is None assert row["user_agent"] is None