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>
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { navigateToSDK, waitForPageLoad } from '../utils/test-helpers'
|
||||
|
||||
/**
|
||||
* Cookie Banner UI E2E Tests
|
||||
*
|
||||
* Tests the cookie banner configuration page and admin UI.
|
||||
* Verifies that the banner builder, category management,
|
||||
* and code export work correctly.
|
||||
*/
|
||||
|
||||
test.describe('Cookie Banner Configuration Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/cookie-banner')
|
||||
})
|
||||
|
||||
test('should load cookie banner page', async ({ page }) => {
|
||||
// The page should load with the step header
|
||||
await expect(page.getByText('Cookie-Banner')).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('should display category cards', async ({ page }) => {
|
||||
// Wait for content to load
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check for standard cookie categories
|
||||
const pageContent = await page.textContent('body')
|
||||
expect(pageContent).toBeTruthy()
|
||||
|
||||
// At minimum the "Notwendig" category should exist
|
||||
const hasCategories = pageContent?.includes('Notwendig') ||
|
||||
pageContent?.includes('necessary') ||
|
||||
pageContent?.includes('Kategorie')
|
||||
expect(hasCategories).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should have banner preview section', async ({ page }) => {
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check for preview or banner-related UI elements
|
||||
const hasPreview = await page.locator('text=Vorschau').isVisible().catch(() => false) ||
|
||||
await page.locator('text=Preview').isVisible().catch(() => false) ||
|
||||
await page.locator('text=Banner').isVisible().catch(() => false)
|
||||
expect(hasPreview).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should have export/publish buttons', async ({ page }) => {
|
||||
// Check for action buttons
|
||||
const exportBtn = page.getByRole('button', { name: /Code exportieren|Export/ })
|
||||
const publishBtn = page.getByRole('button', { name: /Veroeffentlichen|Speichern|Publish/ })
|
||||
|
||||
const hasExport = await exportBtn.isVisible().catch(() => false)
|
||||
const hasPublish = await publishBtn.isVisible().catch(() => false)
|
||||
|
||||
// At least one action button should be present
|
||||
expect(hasExport || hasPublish).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should display cookie statistics', async ({ page }) => {
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check for statistics display (cookie count, third-party count)
|
||||
const pageContent = await page.textContent('body')
|
||||
const hasStats = pageContent?.includes('Cookie') || pageContent?.includes('Vendor')
|
||||
expect(hasStats).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test.describe('Cookie Banner Navigation', () => {
|
||||
test('should be reachable from SDK sidebar', async ({ page }) => {
|
||||
// Navigate to SDK dashboard first
|
||||
await navigateToSDK(page, '')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Look for cookie banner link in sidebar or navigation
|
||||
const bannerLink = page.locator('a[href*="cookie-banner"]').first()
|
||||
if (await bannerLink.isVisible().catch(() => false)) {
|
||||
await bannerLink.click()
|
||||
await waitForPageLoad(page)
|
||||
await expect(page).toHaveURL(/cookie-banner/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should navigate between consent management pages', async ({ page }) => {
|
||||
await navigateToSDK(page, '/einwilligungen')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check if tabs/links to cookie-banner exist
|
||||
const cookieBannerTab = page.locator('a[href*="cookie-banner"], button:has-text("Cookie")')
|
||||
const isVisible = await cookieBannerTab.first().isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
await cookieBannerTab.first().click()
|
||||
await waitForPageLoad(page)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test.describe('Consent Management Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/consent-management')
|
||||
})
|
||||
|
||||
test('should load consent management page', async ({ page }) => {
|
||||
await page.waitForTimeout(1000)
|
||||
const pageContent = await page.textContent('body')
|
||||
// Page should have consent-related content
|
||||
const hasContent = pageContent?.includes('Consent') ||
|
||||
pageContent?.includes('Einwilligung') ||
|
||||
pageContent?.includes('consent')
|
||||
expect(hasContent).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test.describe('DSR Module — Banner Integration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr')
|
||||
})
|
||||
|
||||
test('should load DSR portal', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: /DSR|Betroffenen/ })).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
})
|
||||
|
||||
test('should have tab navigation for DSR workflow', async ({ page }) => {
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// DSR should have workflow tabs
|
||||
const pageContent = await page.textContent('body')
|
||||
const hasTabs = pageContent?.includes('Eingang') ||
|
||||
pageContent?.includes('Bearbeitung') ||
|
||||
pageContent?.includes('Abgeschlossen')
|
||||
expect(hasTabs).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user