import { test, expect } from '@playwright/test' /** * Banner Consent API Integration Tests * * Tests the complete lifecycle of cookie banner consents: * - Record/retrieve/withdraw consent * - Email linking for DSR integration * - Consent export (Art. 15/20 DSGVO) * - Consent deletion (Art. 17 DSGVO erasure) * - Consent sync to Einwilligungen * - Vendor sync from service registry * - Site config with config_version for consent proof */ const API_BASE = process.env.PLAYWRIGHT_API_URL || 'https://macmini:8093' const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' const HEADERS = { 'Content-Type': 'application/json', 'X-Tenant-ID': TENANT_ID, } // Test data const TEST_SITE_ID = `e2e-banner-test-${Date.now()}` const TEST_DEVICE_FP = `e2e-device-${Date.now()}` const TEST_EMAIL = `e2e-test-${Date.now()}@example.com` test.describe('Banner Consent API — Full Lifecycle', () => { let siteConfigId: string | null = null let consentId: string | null = null // ─── Setup: Create site config ─────────────────────────── test('01 — Create site config', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/admin/sites`, { headers: HEADERS, data: { site_id: TEST_SITE_ID, site_name: 'E2E Test Site', site_url: 'https://e2e-test.example.com', banner_title: 'Cookie-Einstellungen', banner_description: 'Wir verwenden Cookies fuer E2E Tests.', privacy_url: '/datenschutz', imprint_url: '/impressum', }, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.site_id).toBe(TEST_SITE_ID) expect(body.config_version).toBe(1) siteConfigId = body.id }) test('02 — Create categories for site', async ({ request }) => { const categories = [ { category_key: 'necessary', name_de: 'Notwendig', is_required: true, sort_order: 0 }, { category_key: 'statistics', name_de: 'Statistik', is_required: false, sort_order: 1 }, { category_key: 'marketing', name_de: 'Marketing', is_required: false, sort_order: 2 }, ] for (const cat of categories) { const res = await request.post(`${API_BASE}/banner/admin/sites/${TEST_SITE_ID}/categories`, { headers: HEADERS, data: cat, }) expect(res.ok()).toBeTruthy() } }) test('03 — Create vendor configs', async ({ request }) => { const vendors = [ { vendor_name: 'Google Analytics', category_key: 'statistics', retention_days: 790, cookie_names: ['_ga', '_gid'] }, { vendor_name: 'Facebook Pixel', category_key: 'marketing', retention_days: 90, cookie_names: ['_fbp'] }, ] for (const v of vendors) { const res = await request.post(`${API_BASE}/banner/admin/sites/${TEST_SITE_ID}/vendors`, { headers: HEADERS, data: v, }) expect(res.ok()).toBeTruthy() } }) // ─── Core Consent CRUD ─────────────────────────────────── test('04 — Get site config for banner display', async ({ request }) => { const res = await request.get(`${API_BASE}/banner/config/${TEST_SITE_ID}`, { headers: HEADERS }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.site_id).toBe(TEST_SITE_ID) expect(body.banner_title).toBe('Cookie-Einstellungen') expect(body.categories.length).toBe(3) expect(body.vendors.length).toBe(2) expect(body.config_version).toBe(1) }) test('05 — Record consent (accept statistics only)', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/consent`, { headers: HEADERS, data: { site_id: TEST_SITE_ID, device_fingerprint: TEST_DEVICE_FP, categories: ['necessary', 'statistics'], vendors: ['Google Analytics'], ip_address: '192.168.1.100', user_agent: 'Playwright E2E Test', }, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.device_fingerprint).toBe(TEST_DEVICE_FP) expect(body.categories).toEqual(['necessary', 'statistics']) expect(body.vendors).toEqual(['Google Analytics']) expect(body.ip_hash).toBeTruthy() // IP is hashed expect(body.linked_email).toBeNull() consentId = body.id }) test('06 — Retrieve consent for device', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent?site_id=${TEST_SITE_ID}&device_fingerprint=${TEST_DEVICE_FP}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.has_consent).toBe(true) expect(body.consent.categories).toEqual(['necessary', 'statistics']) }) test('07 — Update consent (accept all categories)', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/consent`, { headers: HEADERS, data: { site_id: TEST_SITE_ID, device_fingerprint: TEST_DEVICE_FP, categories: ['necessary', 'statistics', 'marketing'], vendors: ['Google Analytics', 'Facebook Pixel'], ip_address: '192.168.1.100', user_agent: 'Playwright E2E Test', }, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.categories).toEqual(['necessary', 'statistics', 'marketing']) expect(body.id).toBe(consentId) // Same consent row (upsert) }) // ─── Phase 3: Email Linking ────────────────────────────── test('08 — Link email to device fingerprint', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/consent/link-email`, { headers: HEADERS, data: { site_id: TEST_SITE_ID, device_fingerprint: TEST_DEVICE_FP, email: TEST_EMAIL, }, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.linked_email).toBe(TEST_EMAIL.toLowerCase()) expect(body.device_fingerprint).toBe(TEST_DEVICE_FP) }) test('09 — Find consents by email (Art. 15)', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/by-email/${encodeURIComponent(TEST_EMAIL)}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.length).toBe(1) expect(body[0].linked_email).toBe(TEST_EMAIL.toLowerCase()) expect(body[0].device_fingerprint).toBe(TEST_DEVICE_FP) }) test('10 — Export consent data for DSR (Art. 15/20)', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/dsr-export/${encodeURIComponent(TEST_EMAIL)}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.email).toBe(TEST_EMAIL.toLowerCase()) expect(body.banner_consents.length).toBe(1) expect(body.audit_trail.length).toBeGreaterThan(0) // Verify consent proof fields in audit trail const lastAudit = body.audit_trail[0] expect(lastAudit.action).toBeTruthy() expect(lastAudit.site_id).toBe(TEST_SITE_ID) }) // ─── Phase 4: Consent Sync ────────────────────────────── test('11 — Sync banner consent to Einwilligungen', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/consent/sync`, { headers: HEADERS, data: { site_id: TEST_SITE_ID, device_fingerprint: TEST_DEVICE_FP, email: TEST_EMAIL, }, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.synced).toBeGreaterThan(0) expect(body.categories).toContain('necessary') expect(body.categories).toContain('statistics') expect(body.categories).toContain('marketing') expect(body.email).toBe(TEST_EMAIL.toLowerCase()) }) // ─── DSGVO Export ──────────────────────────────────────── test('12 — Export consent per device (existing endpoint)', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/export?site_id=${TEST_SITE_ID}&device_fingerprint=${TEST_DEVICE_FP}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.device_fingerprint).toBe(TEST_DEVICE_FP) expect(body.consents.length).toBe(1) expect(body.audit_trail.length).toBeGreaterThan(0) }) // ─── Stats ─────────────────────────────────────────────── test('13 — Get site consent statistics', async ({ request }) => { const res = await request.get(`${API_BASE}/banner/admin/stats/${TEST_SITE_ID}`, { headers: HEADERS, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.site_id).toBe(TEST_SITE_ID) expect(body.total_consents).toBeGreaterThan(0) expect(body.category_acceptance.necessary).toBeTruthy() expect(body.category_acceptance.statistics).toBeTruthy() }) // ─── Withdraw + Cleanup ───────────────────────────────── test('14 — Withdraw consent', async ({ request }) => { expect(consentId).toBeTruthy() const res = await request.delete(`${API_BASE}/banner/consent/${consentId}`, { headers: HEADERS, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.success).toBe(true) }) test('15 — Verify consent withdrawn (no consent found)', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent?site_id=${TEST_SITE_ID}&device_fingerprint=${TEST_DEVICE_FP}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.has_consent).toBe(false) }) // ─── Cleanup: Delete site config ──────────────────────── test('16 — Cleanup: Delete test site', async ({ request }) => { const res = await request.delete(`${API_BASE}/banner/admin/sites/${TEST_SITE_ID}`, { headers: HEADERS, }) expect(res.status()).toBe(204) }) }) test.describe('Banner Consent API — Art. 17 Erasure via Email', () => { const ERASURE_SITE = `e2e-erasure-${Date.now()}` const ERASURE_DEVICE_1 = `e2e-dev1-${Date.now()}` const ERASURE_DEVICE_2 = `e2e-dev2-${Date.now()}` const ERASURE_EMAIL = `erasure-${Date.now()}@example.com` test.beforeAll(async ({ request }) => { // Create site await request.post(`${API_BASE}/banner/admin/sites`, { headers: HEADERS, data: { site_id: ERASURE_SITE, site_name: 'Erasure Test Site' }, }) // Record consent on two devices with same email for (const fp of [ERASURE_DEVICE_1, ERASURE_DEVICE_2]) { await request.post(`${API_BASE}/banner/consent`, { headers: HEADERS, data: { site_id: ERASURE_SITE, device_fingerprint: fp, categories: ['necessary', 'statistics'], vendors: [], }, }) await request.post(`${API_BASE}/banner/consent/link-email`, { headers: HEADERS, data: { site_id: ERASURE_SITE, device_fingerprint: fp, email: ERASURE_EMAIL }, }) } }) test('should find 2 consents for email', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/by-email/${encodeURIComponent(ERASURE_EMAIL)}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.length).toBe(2) }) test('should delete all consents by email (Art. 17)', async ({ request }) => { const res = await request.delete( `${API_BASE}/banner/consent/by-email/${encodeURIComponent(ERASURE_EMAIL)}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.deleted).toBe(2) expect(body.email).toBe(ERASURE_EMAIL.toLowerCase()) }) test('should find 0 consents after erasure', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/by-email/${encodeURIComponent(ERASURE_EMAIL)}`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.length).toBe(0) }) test.afterAll(async ({ request }) => { await request.delete(`${API_BASE}/banner/admin/sites/${ERASURE_SITE}`, { headers: HEADERS, }) }) }) test.describe('Banner Consent API — Vendor Sync from Registry', () => { const SYNC_SITE = `e2e-sync-${Date.now()}` test.beforeAll(async ({ request }) => { await request.post(`${API_BASE}/banner/admin/sites`, { headers: HEADERS, data: { site_id: SYNC_SITE, site_name: 'Vendor Sync Test' }, }) }) test('should sync vendors from service registry', async ({ request }) => { const res = await request.post( `${API_BASE}/banner/admin/sites/${SYNC_SITE}/sync-vendors`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.total).toBeGreaterThan(0) expect(body.created).toBeGreaterThan(0) }) test('should list synced vendors', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/admin/sites/${SYNC_SITE}/vendors`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const vendors = await res.json() expect(vendors.length).toBeGreaterThan(0) // Verify vendor structure const ga = vendors.find((v: any) => v.vendor_name === 'Google Analytics') if (ga) { expect(ga.category_key).toBe('statistics') expect(ga.retention_days).toBe(790) // 26 months } }) test.afterAll(async ({ request }) => { await request.delete(`${API_BASE}/banner/admin/sites/${SYNC_SITE}`, { headers: HEADERS, }) }) }) test.describe('Banner Consent API — Edge Cases & Validation', () => { test('should return has_consent=false for unknown device', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent?site_id=nonexistent&device_fingerprint=unknown`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.has_consent).toBe(false) }) test('should return default config for unconfigured site', async ({ request }) => { const res = await request.get(`${API_BASE}/banner/config/nonexistent-site`, { headers: HEADERS, }) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body.site_id).toBe('nonexistent-site') expect(body.banner_title).toBe('Cookie-Einstellungen') expect(body.categories).toEqual([]) }) test('should reject invalid email for link-email', async ({ request }) => { const res = await request.post(`${API_BASE}/banner/consent/link-email`, { headers: HEADERS, data: { site_id: 'test', device_fingerprint: 'test', email: 'not-an-email', }, }) // Should fail — either 400 or 404 (no consent found) expect(res.status()).toBeGreaterThanOrEqual(400) }) test('should return empty list for unknown email in by-email', async ({ request }) => { const res = await request.get( `${API_BASE}/banner/consent/by-email/nobody@nowhere.test`, { headers: HEADERS }, ) expect(res.ok()).toBeTruthy() const body = await res.json() expect(body).toEqual([]) }) test('should hash IP address (never store plain IP)', async ({ request }) => { const siteId = `e2e-ip-test-${Date.now()}` const fp = `e2e-ip-fp-${Date.now()}` // Create site await request.post(`${API_BASE}/banner/admin/sites`, { headers: HEADERS, data: { site_id: siteId, site_name: 'IP Test' }, }) // Record consent with IP const res = await request.post(`${API_BASE}/banner/consent`, { headers: HEADERS, data: { site_id: siteId, device_fingerprint: fp, categories: ['necessary'], vendors: [], ip_address: '10.0.0.42', }, }) expect(res.ok()).toBeTruthy() const body = await res.json() // IP should be hashed, not stored plain expect(body.ip_hash).toBeTruthy() expect(body.ip_hash).not.toBe('10.0.0.42') expect(body.ip_hash.length).toBe(16) // SHA256[:16] // Cleanup await request.delete(`${API_BASE}/banner/consent/${body.id}`, { headers: HEADERS }) await request.delete(`${API_BASE}/banner/admin/sites/${siteId}`, { headers: HEADERS }) }) }) test.describe('Banner Consent API — Retention per Category', () => { const RET_SITE = `e2e-retention-${Date.now()}` test.beforeAll(async ({ request }) => { // Create site with categories and vendors await request.post(`${API_BASE}/banner/admin/sites`, { headers: HEADERS, data: { site_id: RET_SITE, site_name: 'Retention Test' }, }) await request.post(`${API_BASE}/banner/admin/sites/${RET_SITE}/categories`, { headers: HEADERS, data: { category_key: 'necessary', name_de: 'Notwendig', is_required: true }, }) await request.post(`${API_BASE}/banner/admin/sites/${RET_SITE}/categories`, { headers: HEADERS, data: { category_key: 'marketing', name_de: 'Marketing', is_required: false }, }) await request.post(`${API_BASE}/banner/admin/sites/${RET_SITE}/vendors`, { headers: HEADERS, data: { vendor_name: 'FB Pixel', category_key: 'marketing', retention_days: 90 }, }) }) test('consent expiry should match max vendor retention', async ({ request }) => { const fp = `e2e-ret-fp-${Date.now()}` const res = await request.post(`${API_BASE}/banner/consent`, { headers: HEADERS, data: { site_id: RET_SITE, device_fingerprint: fp, categories: ['necessary', 'marketing'], vendors: ['FB Pixel'], }, }) expect(res.ok()).toBeTruthy() const body = await res.json() // Expiry should be based on max vendor retention (90 days for marketing) const expiresAt = new Date(body.expires_at) const createdAt = new Date(body.created_at) const diffDays = Math.round((expiresAt.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)) // Should be 90 days (marketing) not 365 (default) expect(diffDays).toBeGreaterThanOrEqual(89) expect(diffDays).toBeLessThanOrEqual(91) // Cleanup await request.delete(`${API_BASE}/banner/consent/${body.id}`, { headers: HEADERS }) }) test.afterAll(async ({ request }) => { await request.delete(`${API_BASE}/banner/admin/sites/${RET_SITE}`, { headers: HEADERS, }) }) })