From f401dc14d7e0ff9fc503d095b8c236b22b34cc03 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 18:20:19 +0000 Subject: [PATCH] =?UTF-8?q?Analyse:=20Zielgruppen-Best=C3=A4tigung=20in=20?= =?UTF-8?q?Historie,=20R=C3=BCcknahme=20bei=20Fehler=20nach=20Dokument-Upd?= =?UTF-8?q?ate,=20handelnde=20Person=20mit=20Name=20und=20E-Mail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/analyse-setup.mjs | 5 +++ server/routes/admin/analyse.js | 1 + server/routes/funktionen.js | 10 ++++- server/services/analyse.js | 76 ++++++++++++++++++++-------------- test/analyse.test.js | 30 +++++++++----- test/analyseRoutes.test.js | 19 +++++++-- test/analyseSetup.test.js | 23 +++++----- 7 files changed, 108 insertions(+), 56 deletions(-) diff --git a/scripts/analyse-setup.mjs b/scripts/analyse-setup.mjs index 454a39a..52fc17e 100644 --- a/scripts/analyse-setup.mjs +++ b/scripts/analyse-setup.mjs @@ -23,6 +23,9 @@ export const ANALYSE_ATTRIBUTES = [ { key: 'avvConsentChannel', type: 'string', size: 10, required: false }, { key: 'removalTicketId', type: 'string', size: 40, required: false }, { key: 'publishTicketId', type: 'string', size: 40, required: false }, + // Zielgruppen-Bestaetigung (nicht an Kinder unter 16): bestaetigt + Fassung des Bestaetigungstextes + { key: 'audienceConfirmed', type: 'boolean', required: false }, + { key: 'audienceTextVersion', type: 'string', size: 20, required: false }, ] const str = (key, size) => ({ key, type: 'string', size, required: false }) @@ -40,6 +43,8 @@ export const CONSENT_ATTRIBUTES = [ str('acceptedBy', 100), str('textformDate', 10), str('textformChannel', 10), + { key: 'audienceConfirmed', type: 'boolean', required: false }, + str('audienceTextVersion', 20), ] export const CONSENT_INDEXES = [{ key: 'projectId_idx', type: 'key', attributes: ['projectId'] }] diff --git a/server/routes/admin/analyse.js b/server/routes/admin/analyse.js index ca3ae83..f9f2e47 100644 --- a/server/routes/admin/analyse.js +++ b/server/routes/admin/analyse.js @@ -81,6 +81,7 @@ router.post('/:projectId', async (req, res) => { consent: { accepted: true, avvVersion: config.analyse.avvVersion }, by, textform, + audienceConfirmed, }) : await deactivateAnalyse({ customerId, projectId: project.$id, by }) res.json({ ok: true, state: doc?.state || 'off' }) diff --git a/server/routes/funktionen.js b/server/routes/funktionen.js index 6332f7a..03d7faf 100644 --- a/server/routes/funktionen.js +++ b/server/routes/funktionen.js @@ -37,6 +37,12 @@ function fail(res, err, fallback) { return res.status(500).json({ error: fallback }) } +// Handelnde Person: bei Admin-Sitzung (adminName/adminEmail, siehe auth.js) der Admin, sonst der Kunde +const actingPerson = (session) => + session.adminName || session.adminEmail + ? { name: session.adminName, email: session.adminEmail } + : { name: session.name, email: session.email } + const ANALYSE_OFF = 'Website-Analyse ist noch nicht freigeschaltet' const ANALYSE_ERROR = 'Die Website-Analyse konnte gerade nicht geändert werden. Bitte versuch es später noch einmal oder schreib uns im Chat.' @@ -183,7 +189,7 @@ router.post('/analyse/activate', analyseGuard, async (req, res) => { accentColor, consent, by: 'customer', - actor: { name: req.session.name, email: req.session.email }, + actor: actingPerson(req.session), }) res.json({ ok: true, state: doc.state }) } catch (err) { @@ -200,7 +206,7 @@ router.post('/analyse/deactivate', analyseGuard, async (req, res) => { customerId: getSessionCustomerId(req), projectId: analyseProjectId(req.body?.projectId), by: 'customer', - actor: { name: req.session.name, email: req.session.email }, + actor: actingPerson(req.session), }) res.json({ ok: true, state: doc?.state || 'off' }) } catch (err) { diff --git a/server/services/analyse.js b/server/services/analyse.js index 1769c7c..04a36b3 100644 --- a/server/services/analyse.js +++ b/server/services/analyse.js @@ -102,6 +102,9 @@ export async function controllerView(customerId) { return { company, address, email, ok, missing } } +/** "Name " (oder was davon da ist), max. 100 Zeichen */ +const personLabel = (p) => (p?.name && p?.email ? `${p.name} <${p.email}>` : p?.name || p?.email || '').slice(0, 100) + const berlinToday = (now) => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(now) /** @@ -168,7 +171,7 @@ function privacyTicket(customerId, project, siteKey) { }) } -export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, now = new Date() }) { +export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, audienceConfirmed, now = new Date() }) { const viaCustomer = by === 'customer' let accepted let proof = { date: '', channel: '' } @@ -176,9 +179,11 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons if (!consent || consent.accepted !== true || consent.avvVersion !== config.analyse.avvVersion) { throw httpError(400, 'Bitte den Auftragsverarbeitungsvertrag bestätigen') } - accepted = String(actor?.name || actor?.email || '').slice(0, 100) + // Name und E-Mail der handelnden Person (bei Admin-Sitzung der Admin, siehe Route) + accepted = personLabel(actor) if (!accepted) throw httpError(400, 'Bitte melde Dich neu an') } else { + if (audienceConfirmed !== true) throw httpError(400, 'Bitte bestätigen, dass die Website sich nicht gezielt an Kinder unter 16 Jahren richtet') const t = validateTextform(textform, { today: berlinToday(now) }) proof = { date: t.date, channel: t.channel } accepted = String(by).slice(0, 100) @@ -206,42 +211,53 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons // Erst beim Sammeldienst anmelden: schlaegt das fehl, bleibt das Dokument unveraendert (erneut versuchbar) await analyseDeps.client.putSite(siteKey, { projectId, domains: analyseDomains(project), active: true }) const at = now.toISOString() + const entry = { + customerId, + projectId, + siteKey, + event: 'on', + avvVersion: config.analyse.avvVersion, + via: viaCustomer ? 'portal' : 'woms', + acceptedAt: at, + acceptedBy: accepted, + textformDate: proof.date, + textformChannel: proof.channel, + audienceConfirmed: true, // Kunden-Haken enthaelt die Zielgruppen-Aussage; Mitarbeiter bestaetigen sie getrennt + audienceTextVersion: config.analyse.avvVersion, + } + let updated = false try { - await analyseDeps.recordConsent({ - customerId, - projectId, + doc = await updateDocument(config.collections.portalFeatures, existing.$id, { + ...stateFields('active'), siteKey, - event: 'on', - avvVersion: config.analyse.avvVersion, - via: viaCustomer ? 'portal' : 'woms', - acceptedAt: at, - acceptedBy: accepted, - textformDate: proof.date, - textformChannel: proof.channel, + accentColor: color, + avvVersion: entry.avvVersion, + avvAcceptedAt: at, + avvAcceptedBy: accepted, + avvConsentDate: proof.date, + avvConsentChannel: proof.channel, + audienceConfirmed: true, + audienceTextVersion: entry.audienceTextVersion, + activatedAt: at, + offSince: '', + activatedMailAt: '', // jede neue Aktivierung bekommt ihre K18 + privacyTicketId: '', // und ihre Tickets + removalTicketId: '', + publishTicketId: '', }) + updated = true + // Nachweis erst nach dem Dokument: scheitert er, wird alles zurueckgenommen (kein "on" ohne aktives Dokument) + await analyseDeps.recordConsent(entry) } catch (err) { - // Ohne Nachweis keine Aktivierung: Messdienst zuruecknehmen await analyseDeps.client .putSite(siteKey, { projectId, domains: analyseDomains(project), active: false }) .catch((e) => console.warn(`[analyse] Rücknahme für Projekt ${projectId} fehlgeschlagen:`, e.message)) + if (updated) { + await updateDocument(config.collections.portalFeatures, existing.$id, { ...stateFields(existing.state || 'off'), offSince: existing.offSince || '' }) + .catch((e) => console.warn(`[analyse] Zurücksetzen für Projekt ${projectId} fehlgeschlagen:`, e.message)) + } throw err } - doc = await updateDocument(config.collections.portalFeatures, existing.$id, { - ...stateFields('active'), - siteKey, - accentColor: color, - avvVersion: config.analyse.avvVersion, - avvAcceptedAt: at, - avvAcceptedBy: accepted, - avvConsentDate: proof.date, - avvConsentChannel: proof.channel, - activatedAt: at, - offSince: '', - activatedMailAt: '', // jede neue Aktivierung bekommt ihre K18 - privacyTicketId: '', // und ihre Tickets - removalTicketId: '', - publishTicketId: '', - }) changed = true } else if (existing.accentColor !== color) { doc = await updateDocument(config.collections.portalFeatures, existing.$id, { accentColor: color }) @@ -288,7 +304,7 @@ export async function deactivateAnalyse({ customerId, projectId, now = new Date( avvVersion: config.analyse.avvVersion, via: by === 'customer' || by === 'system' ? 'portal' : 'woms', acceptedAt: now.toISOString(), - acceptedBy: String((by === 'customer' && (actor?.name || actor?.email)) || by).slice(0, 100), + acceptedBy: (by === 'customer' && personLabel(actor)) || String(by).slice(0, 100), textformDate: '', textformChannel: '', }) diff --git a/test/analyse.test.js b/test/analyse.test.js index 1d4d301..1ed2ade 100644 --- a/test/analyse.test.js +++ b/test/analyse.test.js @@ -7,8 +7,8 @@ import { schemaKeys } from './support/schema.js' import { mailSink } from './support/mailSink.js' // Das Setup-Skript laedt config.js: erst nach useTestEnv() importieren, daher die Attributnamen hier fest -const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId'] -const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel'] +const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId', 'audienceConfirmed', 'audienceTextVersion'] +const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel', 'audienceConfirmed', 'audienceTextVersion'] const aw = await startAppwriteMock({ schema: { portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS], @@ -101,7 +101,7 @@ test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () = assert.match(d.siteKey, /^[a-z0-9]{16,40}$/) assert.equal(d.accentColor, '#1a73e8') assert.equal(d.avvVersion, config.analyse.avvVersion) - assert.equal(d.avvAcceptedBy, 'Kunde Eins') + assert.equal(d.avvAcceptedBy, 'Kunde Eins ') assert.ok(d.avvAcceptedAt && d.activatedAt) assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de', 'www.baeckerei.de'], active: true }]) assert.deepEqual(rebuilds, ['p1']) @@ -149,7 +149,7 @@ test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts' test('Wieder einschalten: gleicher siteKey, offSince leer, neues Ticket und Historie', async () => { const before = await doc('c1', 'p1') - const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso', textform, now: NOW }) + const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso', textform, audienceConfirmed: true, now: NOW }) assert.equal(d.siteKey, before.siteKey) assert.equal(d.state, 'active') assert.equal(d.offSince, '') @@ -257,7 +257,7 @@ test('gesperrte Analyse: Kunde 403, Mitarbeiter darf', async () => { const g = await sf.ensureFeatureDoc('c1', 'p10', 'analyse') await sf.disable(g.$id, {}) await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p10', ...cust() }), { status: 403, message: 'Analyse wurde gesperrt – bitte melde dich bei uns' }) - const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent(), by: 'Kenso', textform, now: NOW }) + const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent(), by: 'Kenso', textform, audienceConfirmed: true, now: NOW }) assert.equal(r.doc.state, 'active') }) @@ -326,7 +326,8 @@ test('Kunde ohne actor-Namen oder Mitarbeiter ohne Textform: 400', async () => { await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent() }), { status: 400 }) await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent(), actor: {} }), { status: 400 }) await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent(), by: 'Kenso', now: NOW }), { status: 400 }) - await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', by: 'Kenso', textform: { date: '2026-10-01', channel: 'chat' }, now: NOW }), { status: 400 }) + await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', by: 'Kenso', textform, now: NOW }), { status: 400 }) // ohne Zielgruppen-Bestätigung + await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', by: 'Kenso', audienceConfirmed: true, textform: { date: '2026-10-01', channel: 'chat' }, now: NOW }), { status: 400 }) assert.equal(puts.length, 0) assert.equal((await consents('p13')).length, 0) }) @@ -339,12 +340,16 @@ test('Historie: Kunde, Aus, An durch Mitarbeiter in zeitlicher Folge; Portal-Dok assert.equal(h[0].event, 'on') assert.equal(h[0].via, 'portal') assert.equal(h[0].avvVersion, '2026-10-02') - assert.equal(h[0].acceptedBy, 'Kunde Eins') + assert.equal(h[0].acceptedBy, 'Kunde Eins ') assert.equal(h[0].customerId, 'c1') assert.equal(h[0].siteKey, d1.siteKey) assert.match(h[0].acceptedAt, /^\d{4}-\d\d-\d\dT[\d:.]+Z$/) assert.equal(h[0].acceptedAt, d1.avvAcceptedAt) - assert.equal(d1.avvAcceptedBy, 'Kunde Eins') + assert.equal(d1.avvAcceptedBy, 'Kunde Eins ') + assert.equal(d1.audienceConfirmed, true) + assert.equal(d1.audienceTextVersion, '2026-10-02') + assert.equal(h[0].audienceConfirmed, true) + assert.equal(h[0].audienceTextVersion, '2026-10-02') assert.equal(d1.avvConsentDate, '') assert.equal(d1.avvConsentChannel, '') // Farbwechsel und erneutes Aktivieren: keine neue Zeile @@ -354,17 +359,19 @@ test('Historie: Kunde, Aus, An durch Mitarbeiter in zeitlicher Folge; Portal-Dok // Marker zurücksetzen beim Wiedereinschalten await updateDocument(config.collections.portalFeatures, d1.$id, { removalTicketId: 'r1', publishTicketId: 'u1' }) await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p14', now: NOW, actor }) - const { doc: d3 } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', consent: consent(), by: 'Kenso', textform, now: NOW }) + const { doc: d3 } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', consent: consent(), by: 'Kenso', textform, audienceConfirmed: true, now: NOW }) assert.equal(d3.privacyTicketId && d3.privacyTicketId !== d1.privacyTicketId, true, 'neues Datenschutz-Ticket') assert.equal(d3.removalTicketId, '') assert.equal(d3.publishTicketId, '') h = await consents('p14') assert.deepEqual(h.map((x) => x.event), ['on', 'off', 'on']) assert.deepEqual(h.map((x) => x.via), ['portal', 'portal', 'woms']) - assert.equal(h[1].acceptedBy, 'Kunde Eins') + assert.equal(h[1].acceptedBy, 'Kunde Eins ') assert.equal(h[2].acceptedBy, 'Kenso') assert.equal(h[2].textformDate, '2026-10-02') assert.equal(h[2].textformChannel, 'email') + assert.equal(h[2].audienceConfirmed, true) + assert.equal(d3.audienceConfirmed, true) assert.equal(d3.avvConsentDate, '2026-10-02') assert.equal(d3.avvAcceptedBy, 'Kenso') }) @@ -382,7 +389,8 @@ test('recordConsent scheitert: putSite wird zurückgenommen, Dokument bleibt aus assert.equal(puts.length, 2) assert.equal(puts[0].active, true) assert.equal(puts[1].active, false) - assert.equal((await doc('c1', 'p15')).state, 'off') + assert.equal((await doc('c1', 'p15')).state, 'off', 'Dokument zurückgesetzt') + assert.equal((await doc('c1', 'p15')).offSince, '') assert.equal(analyseTickets().filter((t) => t.details.includes('Rollback')).length, 0) }) diff --git a/test/analyseRoutes.test.js b/test/analyseRoutes.test.js index 93c06ed..2b14ce7 100644 --- a/test/analyseRoutes.test.js +++ b/test/analyseRoutes.test.js @@ -6,8 +6,8 @@ import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' import { mailSink } from './support/mailSink.js' -const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId'] -const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel'] +const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId', 'audienceConfirmed', 'audienceTextVersion'] +const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel', 'audienceConfirmed', 'audienceTextVersion'] const aw = await startAppwriteMock({ schema: { portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS], subscriptions: schemaKeys('subscriptions'), analyseConsents: CONSENT_KEYS }, }) @@ -221,6 +221,9 @@ test('Admin: 401, 400, 200', async () => { assert.equal(h.length, 1) assert.equal(h[0].via, 'woms') assert.equal(h[0].acceptedBy, 'kenso@webklar.com') + assert.equal(h[0].audienceConfirmed, true) + assert.equal(h[0].audienceTextVersion, config.analyse.avvVersion) + assert.equal(d.audienceConfirmed, true) assert.equal(h[0].textformDate, todayBerlin()) assert.equal(h[0].textformChannel, 'chat') const off = await adminPost('p6', { on: false }) @@ -316,8 +319,18 @@ test('Kunden-Aktivierung schreibt Historie mit Sitzungsnamen; fehlende Angaben: await post(`${A}/activate`, { projectId: 'p3', consent: consent() }, { cookie }) const h = consentsOf('p3') assert.equal(h.length, before + 1) - assert.equal(h.at(-1).acceptedBy, 'Kunde Eins') + assert.equal(h.at(-1).acceptedBy, 'Kunde Eins ') + assert.equal(h.at(-1).audienceConfirmed, true) assert.equal(h.at(-1).via, 'portal') const admin = (await rawGet('/api/admin/analyse/p9', { headers: { Authorization: 'Bearer jwt-admin' } })).body assert.deepEqual(admin.privacy, { ok: false, missing: ['firma', 'email'] }) }) + +test('Admin-Sitzung (Impersonation): handelnde Person ist der Admin, nicht der Kunde', async () => { + aw.seed('websiteProjects', { $id: 'p20', customerId: 'c1', projectName: 'Imp', subdomain: 'imp', isPublic: true }) + const c = sessionCookie({ customerId: 'c1', appwriteUserId: 'u1', name: 'Kunde Eins', email: 'k@example.invalid', adminName: 'Kenso', adminEmail: 'kenso@webklar.com' }) + assert.equal((await post(`${A}/activate`, { projectId: 'p20', consent: consent() }, { cookie: c })).status, 200) + const h = consentsOf('p20') + assert.equal(h[0].acceptedBy, 'Kenso ') + assert.equal((await featureDoc('c1', 'p20')).avvAcceptedBy, 'Kenso ') +}) diff --git a/test/analyseSetup.test.js b/test/analyseSetup.test.js index 46fcebc..4fd4b48 100644 --- a/test/analyseSetup.test.js +++ b/test/analyseSetup.test.js @@ -30,8 +30,8 @@ function mockClient({ attrs = [], consentsExist = false, consentAttrs = [], inde } } -test('Schema: 12 Attribute mit Groessen, alle optional', () => { - const sizes = Object.fromEntries(ANALYSE_ATTRIBUTES.map((a) => [a.key, a.size])) +test('Schema: 14 Attribute mit Groessen, alle optional', () => { + const sizes = Object.fromEntries(ANALYSE_ATTRIBUTES.filter((a) => a.type === 'string').map((a) => [a.key, a.size])) assert.deepEqual(sizes, { accentColor: 7, avvVersion: 20, @@ -45,30 +45,33 @@ test('Schema: 12 Attribute mit Groessen, alle optional', () => { avvConsentChannel: 10, removalTicketId: 40, publishTicketId: 40, + audienceTextVersion: 20, }) - assert.equal(ANALYSE_ATTRIBUTES.length, 12) - assert.ok(ANALYSE_ATTRIBUTES.every((a) => a.required === false && a.type === 'string')) + assert.equal(ANALYSE_ATTRIBUTES.length, 14) + assert.equal(ANALYSE_ATTRIBUTES.find((a) => a.key === 'audienceConfirmed').type, 'boolean') + assert.ok(ANALYSE_ATTRIBUTES.every((a) => a.required === false)) }) test('Schema: Historien-Collection analyseConsents', () => { assert.equal(config.collections.analyseConsents, 'analyseConsents') - const sizes = Object.fromEntries(CONSENT_ATTRIBUTES.map((a) => [a.key, a.size])) + const sizes = Object.fromEntries(CONSENT_ATTRIBUTES.filter((a) => a.type === 'string').map((a) => [a.key, a.size])) assert.deepEqual(sizes, { customerId: 36, projectId: 36, siteKey: 40, event: 8, avvVersion: 20, via: 8, - acceptedAt: 40, acceptedBy: 100, textformDate: 10, textformChannel: 10, + acceptedAt: 40, acceptedBy: 100, textformDate: 10, textformChannel: 10, audienceTextVersion: 20, }) + assert.equal(CONSENT_ATTRIBUTES.find((a) => a.key === 'audienceConfirmed').type, 'boolean') assert.deepEqual(CONSENT_INDEXES.map((i) => i.attributes), [['projectId']]) }) test('main legt nur fehlende Attribute an und die fehlende Collection samt Index', async () => { const client = mockClient({ attrs: ['accentColor'] }) const result = await main({ client, log: () => {} }) - assert.equal(client.calls.attrs.filter((k) => k.startsWith('portalFeatures.')).length, 11) + assert.equal(client.calls.attrs.filter((k) => k.startsWith('portalFeatures.')).length, 13) assert.ok(!client.calls.attrs.includes('portalFeatures.accentColor')) - assert.equal(client.calls.attrs.filter((k) => k.startsWith('analyseConsents.')).length, 10) + assert.equal(client.calls.attrs.filter((k) => k.startsWith('analyseConsents.')).length, 12) assert.deepEqual(client.calls.collections, [{ id: 'analyseConsents', name: 'analyseConsents' }]) assert.deepEqual(client.calls.indexes, ['analyseConsents.projectId_idx']) - assert.equal(result.missing.length, 21) + assert.equal(result.missing.length, 25) }) test('main ueberspringt vorhandene Collection, Attribute und Index', async () => { @@ -87,7 +90,7 @@ test('main --check legt nichts an und meldet missing', async () => { const client = mockClient({ attrs: ['accentColor'] }) const result = await main({ check: true, client, log: () => {} }) assert.deepEqual(client.calls, { attrs: [], collections: [], indexes: [] }) - assert.equal(result.missing.length, 21) + assert.equal(result.missing.length, 25) assert.equal(result.missingCollection, true) })