""" Tests for Banner Consent routes — device-based cookie consents. """ import uuid import os import sys 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.banner_models import ( BannerConsentDB, BannerConsentAuditLogDB, BannerSiteConfigDB, BannerCategoryConfigDB, BannerVendorConfigDB, ) from compliance.api.banner_routes import router as banner_router SQLALCHEMY_DATABASE_URL = "sqlite:///./test_banner.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(banner_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 # ============================================================================= def _create_site(site_id="example.com"): r = client.post("/api/compliance/banner/admin/sites", json={ "site_id": site_id, "site_name": "Example", "banner_title": "Cookies", }, headers=HEADERS) assert r.status_code == 200, r.text return r.json() def _record_consent(site_id="example.com", fingerprint="fp-123", categories=None): r = client.post("/api/compliance/banner/consent", json={ "site_id": site_id, "device_fingerprint": fingerprint, "categories": categories or ["necessary"], "ip_address": "1.2.3.4", }, headers=HEADERS) assert r.status_code == 200, r.text return r.json() # ============================================================================= # Public Consent Endpoints # ============================================================================= class TestRecordConsent: def test_record_consent(self): c = _record_consent() assert c["site_id"] == "example.com" assert c["device_fingerprint"] == "fp-123" assert c["categories"] == ["necessary"] assert c["ip_hash"] is not None assert c["expires_at"] is not None def test_upsert_consent(self): c1 = _record_consent(categories=["necessary"]) c2 = _record_consent(categories=["necessary", "analytics"]) assert c1["id"] == c2["id"] assert c2["categories"] == ["necessary", "analytics"] def test_different_devices(self): c1 = _record_consent(fingerprint="device-A") c2 = _record_consent(fingerprint="device-B") assert c1["id"] != c2["id"] class TestGetConsent: def test_get_existing(self): _record_consent() r = client.get("/api/compliance/banner/consent?site_id=example.com&device_fingerprint=fp-123", headers=HEADERS) assert r.status_code == 200 assert r.json()["has_consent"] is True def test_get_nonexistent(self): r = client.get("/api/compliance/banner/consent?site_id=example.com&device_fingerprint=unknown", headers=HEADERS) assert r.status_code == 200 assert r.json()["has_consent"] is False class TestWithdrawConsent: def test_withdraw(self): c = _record_consent() r = client.delete(f"/api/compliance/banner/consent/{c['id']}", headers=HEADERS) assert r.status_code == 200 assert r.json()["success"] is True # Verify gone r2 = client.get("/api/compliance/banner/consent?site_id=example.com&device_fingerprint=fp-123", headers=HEADERS) assert r2.json()["has_consent"] is False def test_withdraw_not_found(self): r = client.delete(f"/api/compliance/banner/consent/{uuid.uuid4()}", headers=HEADERS) assert r.status_code == 404 class TestExportConsent: def test_export(self): _record_consent() r = client.get( "/api/compliance/banner/consent/export?site_id=example.com&device_fingerprint=fp-123", headers=HEADERS, ) assert r.status_code == 200 data = r.json() assert len(data["consents"]) == 1 assert len(data["audit_trail"]) >= 1 assert data["audit_trail"][0]["action"] == "consent_given" # ============================================================================= # Site Config Admin # ============================================================================= class TestSiteConfig: def test_create_site(self): s = _create_site() assert s["site_id"] == "example.com" assert s["banner_title"] == "Cookies" def test_create_duplicate(self): _create_site() r = client.post("/api/compliance/banner/admin/sites", json={ "site_id": "example.com", }, headers=HEADERS) assert r.status_code == 409 def test_list_sites(self): _create_site("site-a.com") _create_site("site-b.com") r = client.get("/api/compliance/banner/admin/sites", headers=HEADERS) assert r.status_code == 200 assert len(r.json()) == 2 def test_update_site(self): _create_site() r = client.put("/api/compliance/banner/admin/sites/example.com", json={ "banner_title": "Neue Cookies", "dsb_name": "Max DSB", }, headers=HEADERS) assert r.status_code == 200 assert r.json()["banner_title"] == "Neue Cookies" assert r.json()["dsb_name"] == "Max DSB" def test_update_not_found(self): r = client.put("/api/compliance/banner/admin/sites/nonexistent.com", json={ "banner_title": "X", }, headers=HEADERS) assert r.status_code == 404 def test_delete_site(self): _create_site() r = client.delete("/api/compliance/banner/admin/sites/example.com", headers=HEADERS) assert r.status_code == 204 def test_get_config_default(self): r = client.get("/api/compliance/banner/config/unknown-site", headers=HEADERS) assert r.status_code == 200 assert r.json()["banner_title"] == "Cookie-Einstellungen" assert r.json()["categories"] == [] def test_get_config_with_categories(self): _create_site() client.post("/api/compliance/banner/admin/sites/example.com/categories", json={ "category_key": "necessary", "name_de": "Notwendig", "is_required": True, }, headers=HEADERS) r = client.get("/api/compliance/banner/config/example.com", headers=HEADERS) data = r.json() assert len(data["categories"]) == 1 assert data["categories"][0]["category_key"] == "necessary" # ============================================================================= # Categories Admin # ============================================================================= class TestCategories: def test_create_category(self): _create_site() r = client.post("/api/compliance/banner/admin/sites/example.com/categories", json={ "category_key": "analytics", "name_de": "Analyse", "name_en": "Analytics", "sort_order": 20, }, headers=HEADERS) assert r.status_code == 200 assert r.json()["category_key"] == "analytics" assert r.json()["name_de"] == "Analyse" def test_list_categories(self): _create_site() client.post("/api/compliance/banner/admin/sites/example.com/categories", json={ "category_key": "marketing", "name_de": "Marketing", "sort_order": 30, }, headers=HEADERS) client.post("/api/compliance/banner/admin/sites/example.com/categories", json={ "category_key": "necessary", "name_de": "Notwendig", "sort_order": 0, "is_required": True, }, headers=HEADERS) r = client.get("/api/compliance/banner/admin/sites/example.com/categories", headers=HEADERS) data = r.json() assert len(data) == 2 assert data[0]["category_key"] == "necessary" # sorted by sort_order def test_delete_category(self): _create_site() cr = client.post("/api/compliance/banner/admin/sites/example.com/categories", json={ "category_key": "temp", "name_de": "Temp", }, headers=HEADERS) cat_id = cr.json()["id"] r = client.delete(f"/api/compliance/banner/admin/categories/{cat_id}") assert r.status_code == 204 def test_site_not_found(self): r = client.post("/api/compliance/banner/admin/sites/nonexistent/categories", json={ "category_key": "x", "name_de": "X", }, headers=HEADERS) assert r.status_code == 404 # ============================================================================= # Vendors Admin # ============================================================================= class TestVendors: def test_create_vendor(self): _create_site() r = client.post("/api/compliance/banner/admin/sites/example.com/vendors", json={ "vendor_name": "Google Analytics", "category_key": "analytics", "cookie_names": ["_ga", "_gid"], "retention_days": 730, }, headers=HEADERS) assert r.status_code == 200 assert r.json()["vendor_name"] == "Google Analytics" assert r.json()["cookie_names"] == ["_ga", "_gid"] def test_list_vendors(self): _create_site() client.post("/api/compliance/banner/admin/sites/example.com/vendors", json={ "vendor_name": "GA", "category_key": "analytics", }, headers=HEADERS) r = client.get("/api/compliance/banner/admin/sites/example.com/vendors", headers=HEADERS) assert len(r.json()) == 1 def test_delete_vendor(self): _create_site() cr = client.post("/api/compliance/banner/admin/sites/example.com/vendors", json={ "vendor_name": "Temp", "category_key": "analytics", }, headers=HEADERS) vid = cr.json()["id"] r = client.delete(f"/api/compliance/banner/admin/vendors/{vid}") assert r.status_code == 204 # ============================================================================= # Stats # ============================================================================= class TestStats: def test_stats_empty(self): r = client.get("/api/compliance/banner/admin/stats/example.com", headers=HEADERS) assert r.status_code == 200 assert r.json()["total_consents"] == 0 def test_stats_with_data(self): _record_consent(fingerprint="d1", categories=["necessary", "analytics"]) _record_consent(fingerprint="d2", categories=["necessary"]) _record_consent(fingerprint="d3", categories=["necessary", "analytics", "marketing"]) r = client.get("/api/compliance/banner/admin/stats/example.com", headers=HEADERS) data = r.json() assert data["total_consents"] == 3 assert data["category_acceptance"]["necessary"]["count"] == 3 assert data["category_acceptance"]["analytics"]["count"] == 2 assert data["category_acceptance"]["marketing"]["count"] == 1