From 65f978368d7ccacc5cff6d7a255d8ac701821779 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 14 May 2026 18:45:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(cmp):=20Phase=203=20=E2=80=94=20admin=20wi?= =?UTF-8?q?derruf,=20email-linking,=20vendor=20display,=20TCF,=20E2E=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin Modal: - vendor_consents as green/red badges - Consent withdraw button (DELETE /consent/{id}) with confirmation - Email-linking inline input (POST /consent/link-email) Cookie Banner Admin: - TCF toggle reads tcf_enabled from site config (was hardcoded false) - BannerSite interface extended with tcf_enabled Document Generator: - Backend Banner-Config auto-fetch when SDK state has no banner - Maps vendors to CONSENT (analytics tools, marketing partners) E2E Tests (cmp-phase3-dsr.spec.ts): - Vendor-agnostic consent fields (20+ fields, upsert) - DSR Art. 15 Auskunft (multi-device, email-link, export) - DSR Art. 17 Löschung (erasure by email) - Anonymous cookie banner user (export, withdraw) - Customer lifecycle (consent → login → link → Art.15 → Art.17) - Admin dashboard integration (list, stats) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cookie-banner/_hooks/useCookieBanner.ts | 1 + .../app/sdk/cookie-banner/page.tsx | 2 +- .../app/sdk/document-generator/page.tsx | 30 +- .../_components/BannerConsentsTab.tsx | 75 +++- .../app/sdk/einwilligungen/_types.ts | 2 + .../e2e/specs/cmp-phase3-dsr.spec.ts | 371 ++++++++++++++++++ 6 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 admin-compliance/e2e/specs/cmp-phase3-dsr.spec.ts diff --git a/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts index cc4b092a..1776e492 100644 --- a/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts +++ b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts @@ -102,6 +102,7 @@ export interface BannerSite { site_name: string site_url: string is_active: boolean + tcf_enabled?: boolean } export function useCookieBanner() { diff --git a/admin-compliance/app/sdk/cookie-banner/page.tsx b/admin-compliance/app/sdk/cookie-banner/page.tsx index cfcf14ee..fc48bebf 100644 --- a/admin-compliance/app/sdk/cookie-banner/page.tsx +++ b/admin-compliance/app/sdk/cookie-banner/page.tsx @@ -105,7 +105,7 @@ export default function CookieBannerPage() { {/* Tab: TCF/IAB */} {activeTab === 'tcf' && ( - s.site_id === activeSiteId)?.tcf_enabled ?? false} onToggle={(enabled) => { if (activeSiteId) { fetch(`/api/sdk/v1/banner/admin/sites/${activeSiteId}`, { diff --git a/admin-compliance/app/sdk/document-generator/page.tsx b/admin-compliance/app/sdk/document-generator/page.tsx index aef91cc6..22d8dde3 100644 --- a/admin-compliance/app/sdk/document-generator/page.tsx +++ b/admin-compliance/app/sdk/document-generator/page.tsx @@ -101,7 +101,35 @@ function DocumentGeneratorPageInner() { } }, [state?.complianceScope?.determinedLevel, state?.companyProfile]) - // ── MODULE WIRING: CookieBanner → CONSENT + FEATURES ───────────────────── + // ── MODULE WIRING: Backend Banner-Config → CONSENT + FEATURES ──────────── + useEffect(() => { + // Fetch real vendor/category data from backend if SDK state has no banner + if (state?.cookieBanner) return // SDK state takes priority + fetch('/api/sdk/v1/banner/admin/sites', { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } }) + .then(r => r.json()) + .then((sites: Array<{ site_id: string }>) => { + if (!sites?.length) return + return fetch(`/api/sdk/v1/banner/config/${sites[0].site_id}`, { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } }) + }) + .then(r => r?.json()) + .then(config => { + if (!config?.vendors?.length) return + const analytics = config.vendors.filter((v: { category_key: string }) => v.category_key === 'statistics' || v.category_key === 'analytics').map((v: { vendor_name: string }) => v.vendor_name) + const marketing = config.vendors.filter((v: { category_key: string }) => v.category_key === 'marketing').map((v: { vendor_name: string }) => v.vendor_name) + setContext(prev => ({ + ...prev, + CONSENT: { + ...prev.CONSENT, + ANALYTICS_TOOLS: analytics.length > 0 ? analytics.join(', ') : prev.CONSENT.ANALYTICS_TOOLS, + MARKETING_PARTNERS: marketing.length > 0 ? marketing.join(', ') : prev.CONSENT.MARKETING_PARTNERS, + }, + FEATURES: { ...prev.FEATURES, CMP_NAME: 'BreakPilot CMP', CMP_LOGS_CONSENTS: true }, + })) + }) + .catch(() => {}) + }, [state?.cookieBanner]) + + // ── MODULE WIRING: CookieBanner SDK State → CONSENT + FEATURES ────────── useEffect(() => { const banner = state?.cookieBanner if (!banner) return diff --git a/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx b/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx index d18a43f4..9ef967d4 100644 --- a/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx +++ b/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx @@ -1,9 +1,12 @@ 'use client' -import { useState } from 'react' +import { useState, useCallback } from 'react' import { useBannerConsents } from '../_hooks/useBannerConsents' import { BannerConsentRecord, PAGE_SIZE } from '../_types' +const BANNER_API = '/api/sdk/v1/banner' +const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' + function formatDate(iso: string | null): string { if (!iso) return '—' return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) @@ -42,12 +45,35 @@ const methodColors: Record = { export default function BannerConsentsTab() { const { records, sites, selectedSite, changeSite, - stats, currentPage, setCurrentPage, totalRecords, loading, + stats, currentPage, setCurrentPage, totalRecords, loading, reload, } = useBannerConsents() const [detail, setDetail] = useState(null) + const [linkEmailInput, setLinkEmailInput] = useState('') + const [linkingEmail, setLinkingEmail] = useState(false) const totalPages = Math.ceil(totalRecords / PAGE_SIZE) + const withdrawConsent = useCallback(async (id: string) => { + if (!confirm('Consent wirklich widerrufen? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return + await fetch(`${BANNER_API}/consent/${id}`, { method: 'DELETE', headers: { 'x-tenant-id': TENANT_ID } }) + setDetail(null) + reload() + }, [reload]) + + const linkEmail = useCallback(async (record: BannerConsentRecord) => { + if (!linkEmailInput.includes('@')) return + setLinkingEmail(true) + await fetch(`${BANNER_API}/consent/link-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-tenant-id': TENANT_ID }, + body: JSON.stringify({ site_id: record.site_id, device_fingerprint: record.device_fingerprint, email: linkEmailInput }), + }) + setLinkingEmail(false) + setLinkEmailInput('') + setDetail({ ...record, linked_email: linkEmailInput }) + reload() + }, [linkEmailInput, reload]) + return (
{/* Stats + Site Selector */} @@ -184,6 +210,18 @@ export default function BannerConsentsTab() { ))}
+ {detail.vendor_consents && Object.keys(detail.vendor_consents).length > 0 && ( +
+ Vendors +
+ {Object.entries(detail.vendor_consents).map(([name, accepted]) => ( + + {name} + + ))} +
+
+ )}
Methode {detail.consent_method ? ( @@ -192,9 +230,28 @@ export default function BannerConsentsTab() { ) : '—'}
-
+
Verknüpft mit - {detail.linked_email || '— (anonym)'} + {detail.linked_email ? ( + {detail.linked_email} + ) : ( +
+ setLinkEmailInput(e.target.value)} + className="text-xs border border-gray-200 rounded px-2 py-1 w-40" + /> + +
+ )}
Erteilt{formatDate(detail.created_at)}
Ablauf{formatDate(detail.expires_at)}
@@ -264,6 +321,16 @@ export default function BannerConsentsTab() { {detail.banner_config_hash &&
Config-Hash

{detail.banner_config_hash}

}
+ + {/* Widerruf-Button */} +
+ +
diff --git a/admin-compliance/app/sdk/einwilligungen/_types.ts b/admin-compliance/app/sdk/einwilligungen/_types.ts index e44bb07d..3e462b25 100644 --- a/admin-compliance/app/sdk/einwilligungen/_types.ts +++ b/admin-compliance/app/sdk/einwilligungen/_types.ts @@ -108,6 +108,7 @@ export interface BannerConsentRecord { device_fingerprint: string categories: string[] vendors: string[] + vendor_consents: Record ip_hash: string | null user_agent: string | null linked_email: string | null @@ -144,4 +145,5 @@ export interface BannerSite { site_id: string site_name: string site_url: string + tcf_enabled?: boolean } diff --git a/admin-compliance/e2e/specs/cmp-phase3-dsr.spec.ts b/admin-compliance/e2e/specs/cmp-phase3-dsr.spec.ts new file mode 100644 index 00000000..b74f3087 --- /dev/null +++ b/admin-compliance/e2e/specs/cmp-phase3-dsr.spec.ts @@ -0,0 +1,371 @@ +import { test, expect } from '@playwright/test' + +/** + * CMP Phase 3 + DSR Integration Tests + * + * Tests the complete CMP lifecycle including: + * - Vendor-agnostic consent fields (consent_method, browser, os, etc.) + * - Script/cookie tracking (scripts_blocked, scripts_released, cookies_set) + * - Session ID tracking + * - GeoIP via timezone mapping + * - Vendor-level consent (vendor_consents dict) + * - DSR scenarios: Art. 15 Auskunft, Art. 17 Löschung, Art. 20 Portabilität + * - Email linking for DSR (device → user mapping) + * - Admin modal features (vendor display, withdraw, email linking) + */ + +const API_BASE = process.env.PLAYWRIGHT_API_URL || 'https://macmini:3007/api/sdk/v1/banner' +const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' +const HEADERS = { + 'Content-Type': 'application/json', + 'X-Tenant-ID': TENANT_ID, +} + +const TS = Date.now() +const SITE_ID = `e2e-cmp3-${TS}` +const DEVICE_FP = `e2e-device-${TS}` + +// ============================================================================ +// 1. Vendor-Agnostic Consent Fields +// ============================================================================ + +test.describe('Vendor-Agnostic Consent Fields', () => { + test('should store all 20+ fields on consent', async ({ request }) => { + // Create site config first + await request.post(`${API_BASE}/admin/sites`, { + headers: HEADERS, + data: { site_id: SITE_ID, site_name: 'E2E CMP Phase 3', site_url: 'https://test.example.com' }, + }) + + // Record consent with all vendor-agnostic fields + const res = await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: DEVICE_FP, + categories: ['essential', 'functional', 'analytics'], + vendors: ['Google Analytics', 'Matomo'], + vendor_consents: { 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false }, + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) E2E-Test', + consent_method: 'custom_selection', + page_url: 'https://test.example.com/pricing', + referrer: 'https://google.com', + device_type: 'desktop', + browser: 'Chrome/120.0', + os: 'Mac OS X 10.15.7', + screen_resolution: '1920x1080', + consent_scope: 'domain', + session_id: 'e2e-session-001', + timezone: 'Europe/Berlin', + scripts_blocked: [{ src: 'https://connect.facebook.net/fbevents.js', category: 'marketing' }], + scripts_released: [{ src: 'https://www.googletagmanager.com/gtag/js', category: 'analytics' }], + cookies_set: [ + { name: '_ga', domain: '.test.example.com', expiry_days: 730, category: 'analytics' }, + { name: 'bp_consent', domain: '.test.example.com', expiry_days: 365, category: 'essential' }, + ], + }, + }) + + expect(res.status()).toBe(200) + const consent = await res.json() + expect(consent.id).toBeTruthy() + expect(consent.consent_method).toBe('custom_selection') + expect(consent.device_type).toBe('desktop') + expect(consent.browser).toBe('Chrome/120.0') + expect(consent.os).toBe('Mac OS X 10.15.7') + expect(consent.page_url).toBe('https://test.example.com/pricing') + expect(consent.session_id).toBe('e2e-session-001') + expect(consent.geo_country).toBe('DE') // Europe/Berlin → DE + expect(consent.scripts_released).toHaveLength(1) + expect(consent.cookies_set).toHaveLength(2) + expect(consent.vendor_consents).toEqual({ 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false }) + }) + + test('should update consent on same fingerprint (upsert)', async ({ request }) => { + const res = await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: DEVICE_FP, + categories: ['essential'], // changed from all 3 to essential only + vendors: [], + consent_method: 'reject_all', + page_url: 'https://test.example.com/settings', + timezone: 'Europe/Vienna', + }, + }) + + expect(res.status()).toBe(200) + const consent = await res.json() + expect(consent.consent_method).toBe('reject_all') + expect(consent.geo_country).toBe('AT') // Europe/Vienna → AT + expect(consent.categories).toEqual(['essential']) + }) +}) + +// ============================================================================ +// 2. DSR Scenarios — Art. 15 Auskunft +// ============================================================================ + +test.describe('DSR — Art. 15 Auskunftsrecht', () => { + const DSR_EMAIL = `dsr-user-${TS}@example.com` + const DSR_DEVICE_1 = `dsr-desktop-${TS}` + const DSR_DEVICE_2 = `dsr-mobile-${TS}` + + test.beforeAll(async ({ request }) => { + // Scenario: User visited website from 2 devices, then linked their email + + // Device 1: Desktop consent + await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: DSR_DEVICE_1, + categories: ['essential', 'analytics'], + consent_method: 'accept_all', + device_type: 'desktop', + browser: 'Firefox/121.0', + page_url: 'https://test.example.com/', + timezone: 'Europe/Berlin', + }, + }) + + // Device 2: Mobile consent + await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: DSR_DEVICE_2, + categories: ['essential'], + consent_method: 'reject_all', + device_type: 'mobile', + browser: 'Safari/17.0', + page_url: 'https://test.example.com/pricing', + timezone: 'Europe/Berlin', + }, + }) + + // User logs in and links email to both devices + await request.post(`${API_BASE}/consent/link-email`, { + headers: HEADERS, + data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_1, email: DSR_EMAIL }, + }) + await request.post(`${API_BASE}/consent/link-email`, { + headers: HEADERS, + data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_2, email: DSR_EMAIL }, + }) + }) + + test('Art. 15 — should find all consents by email', async ({ request }) => { + const res = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS }) + expect(res.status()).toBe(200) + const consents = await res.json() + expect(consents).toHaveLength(2) + expect(consents.map((c: { device_fingerprint: string }) => c.device_fingerprint).sort()).toEqual( + [DSR_DEVICE_1, DSR_DEVICE_2].sort() + ) + }) + + test('Art. 15/20 — should export all consent data for DSR', async ({ request }) => { + const res = await request.get(`${API_BASE}/consent/dsr-export/${DSR_EMAIL}`, { headers: HEADERS }) + expect(res.status()).toBe(200) + const exportData = await res.json() + expect(exportData.consents).toHaveLength(2) + expect(exportData.audit_trail.length).toBeGreaterThan(0) + }) + + test('Art. 17 — should delete all consents by email (erasure)', async ({ request }) => { + const res = await request.delete(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS }) + expect(res.status()).toBe(200) + const result = await res.json() + expect(result.deleted_count).toBe(2) + + // Verify deletion + const check = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS }) + const remaining = await check.json() + expect(remaining).toHaveLength(0) + }) +}) + +// ============================================================================ +// 3. DSR Scenarios — Cookie Banner User (anonymous) +// ============================================================================ + +test.describe('DSR — Anonymous Cookie Banner User', () => { + const ANON_DEVICE = `anon-user-${TS}` + + test.beforeAll(async ({ request }) => { + await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: ANON_DEVICE, + categories: ['essential', 'functional'], + consent_method: 'custom_selection', + device_type: 'tablet', + browser: 'Chrome/120.0', + }, + }) + }) + + test('should export consent by device fingerprint', async ({ request }) => { + const res = await request.get( + `${API_BASE}/consent/export?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`, + { headers: HEADERS } + ) + expect(res.status()).toBe(200) + const data = await res.json() + expect(data.device_fingerprint).toBe(ANON_DEVICE) + expect(data.consents).toHaveLength(1) + expect(data.audit_trail.length).toBeGreaterThan(0) + }) + + test('should withdraw consent by ID', async ({ request }) => { + // Get consent ID first + const getRes = await request.get( + `${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`, + { headers: HEADERS } + ) + const { consent } = await getRes.json() + expect(consent).toBeTruthy() + + // Withdraw + const delRes = await request.delete(`${API_BASE}/consent/${consent.id}`, { headers: HEADERS }) + expect(delRes.status()).toBe(200) + + // Verify + const checkRes = await request.get( + `${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`, + { headers: HEADERS } + ) + const result = await checkRes.json() + expect(result.has_consent).toBe(false) + }) +}) + +// ============================================================================ +// 4. DSR Scenarios — Login User (Customer) who also used Cookie Banner +// ============================================================================ + +test.describe('DSR — Customer with Banner + Login', () => { + const CUSTOMER_EMAIL = `customer-${TS}@company.com` + const CUSTOMER_DEVICE = `customer-device-${TS}` + + test('full lifecycle: consent → login → link → Art.15 → Art.17', async ({ request }) => { + // Step 1: Anonymous visit → cookie consent + const consentRes = await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: CUSTOMER_DEVICE, + categories: ['essential', 'analytics'], + consent_method: 'accept_all', + device_type: 'desktop', + browser: 'Edge/120.0', + page_url: 'https://test.example.com/', + timezone: 'Europe/Zurich', + scripts_released: [{ src: 'https://cdn.matomo.cloud/test.js', category: 'analytics' }], + cookies_set: [{ name: '_pk_id', domain: '.test.example.com', expiry_days: 393, category: 'analytics' }], + }, + }) + expect(consentRes.status()).toBe(200) + const consent = await consentRes.json() + expect(consent.geo_country).toBe('CH') // Europe/Zurich → CH + + // Step 2: Customer logs in → email linked + const linkRes = await request.post(`${API_BASE}/consent/link-email`, { + headers: HEADERS, + data: { site_id: SITE_ID, device_fingerprint: CUSTOMER_DEVICE, email: CUSTOMER_EMAIL }, + }) + expect(linkRes.status()).toBe(200) + + // Step 3: Art. 15 — Customer requests their data + const exportRes = await request.get(`${API_BASE}/consent/dsr-export/${CUSTOMER_EMAIL}`, { headers: HEADERS }) + expect(exportRes.status()).toBe(200) + const exportData = await exportRes.json() + expect(exportData.consents.length).toBeGreaterThan(0) + expect(exportData.audit_trail.length).toBeGreaterThan(0) + + // Verify export contains all consent details + const exported = exportData.consents[0] + expect(exported.categories).toContain('analytics') + expect(exported.linked_email).toBe(CUSTOMER_EMAIL) + + // Step 4: Art. 17 — Customer requests erasure + const deleteRes = await request.delete(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS }) + expect(deleteRes.status()).toBe(200) + + // Step 5: Verify complete erasure + const verifyRes = await request.get(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS }) + const remaining = await verifyRes.json() + expect(remaining).toHaveLength(0) + }) +}) + +// ============================================================================ +// 5. Admin Dashboard Integration +// ============================================================================ + +test.describe('Admin Dashboard — Consent Management', () => { + const ADMIN_DEVICE = `admin-test-${TS}` + + test.beforeAll(async ({ request }) => { + await request.post(`${API_BASE}/consent`, { + headers: HEADERS, + data: { + site_id: SITE_ID, + device_fingerprint: ADMIN_DEVICE, + categories: ['essential', 'functional', 'analytics'], + vendors: ['Matomo'], + vendor_consents: { Matomo: true }, + consent_method: 'accept_all', + device_type: 'desktop', + browser: 'Chrome/121.0', + os: 'Windows NT 10.0', + screen_resolution: '2560x1440', + page_url: 'https://test.example.com/dashboard', + session_id: 'admin-session-001', + timezone: 'Europe/Berlin', + }, + }) + }) + + test('should list consents with new fields', async ({ request }) => { + const res = await request.get(`${API_BASE}/admin/consents?site_id=${SITE_ID}`, { headers: HEADERS }) + expect(res.status()).toBe(200) + const data = await res.json() + expect(data.total).toBeGreaterThan(0) + + const consent = data.consents.find((c: { device_fingerprint: string }) => c.device_fingerprint === ADMIN_DEVICE) + expect(consent).toBeTruthy() + expect(consent.consent_method).toBe('accept_all') + expect(consent.device_type).toBe('desktop') + expect(consent.browser).toBe('Chrome/121.0') + expect(consent.os).toBe('Windows NT 10.0') + expect(consent.screen_resolution).toBe('2560x1440') + expect(consent.session_id).toBe('admin-session-001') + expect(consent.geo_country).toBe('DE') + expect(consent.vendor_consents).toEqual({ Matomo: true }) + }) + + test('should show site stats with category acceptance', async ({ request }) => { + const res = await request.get(`${API_BASE}/admin/stats/${SITE_ID}`, { headers: HEADERS }) + expect(res.status()).toBe(200) + const stats = await res.json() + expect(stats.total_consents).toBeGreaterThan(0) + expect(stats.category_acceptance).toBeTruthy() + expect(stats.category_acceptance.essential).toBeTruthy() + expect(stats.category_acceptance.essential.rate).toBeGreaterThan(0) + }) +}) + +// ============================================================================ +// 6. Cleanup +// ============================================================================ + +test.describe('Cleanup', () => { + test('should delete test site config', async ({ request }) => { + const res = await request.delete(`${API_BASE}/admin/sites/${SITE_ID}`, { headers: HEADERS }) + expect(res.status()).toBe(204) + }) +})