Files
breakpilot-compliance/admin-compliance/e2e/specs/banner-consent-api.spec.ts
T
Benjamin Admin 44acd68c96 feat: Cookie-Banner ↔ Backend Integration (DSR, Retention, Consent Proof)
Phase 1: Vendor sync from service registry (82+ services → banner vendors)
Phase 2: Category-based retention (marketing=90d, statistics=790d, not hardcoded 365d)
Phase 3: DSR ↔ Banner email linking (link-email, by-email, Art.17 erasure, Art.15/20 export)
Phase 4: Consent sync (Banner → Einwilligungen bridge)
Phase 6: Consent proof (SHA256 config hash + config_version in audit log, Art. 7(1) DSGVO)

New files:
- banner_dsr_service.py — email linking + DSR integration
- vendor_banner_sync.py — service registry → vendor configs
- migration 106 — linked_email, banner_config_hash, consent_version columns

Tests: 20+ new backend tests + 2 Playwright E2E test suites (API + UI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 19:55:13 +02:00

516 lines
18 KiB
TypeScript

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,
})
})
})