410 lines
24 KiB
JavaScript
410 lines
24 KiB
JavaScript
import { test, after, beforeEach } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { startFakeStripe } from './support/fakeStripe.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
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', '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,
|
||
},
|
||
})
|
||
const st = await startFakeStripe()
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||
const { config } = await import('../server/config.js')
|
||
const { wartungDoc, hosting2Doc } = await import('./support/fixtures.js')
|
||
const an = await import('../server/services/analyse.js')
|
||
const sf = await import('../server/services/siteFeatures.js')
|
||
const cl = await import('../server/services/analyseConsent.js')
|
||
const priv = await import('../server/services/analysePrivacy.js')
|
||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
|
||
const mails = mailSink()
|
||
setMailTransportForTests(mails)
|
||
after(() => {
|
||
setMailTransportForTests(null)
|
||
return Promise.all([aw.close(), st.close()])
|
||
})
|
||
|
||
const puts = []
|
||
const rebuilds = []
|
||
an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey, ...body }) }
|
||
an.analyseDeps.rebuild = async (project) => rebuilds.push(project.$id)
|
||
|
||
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
|
||
const actor = { name: 'Kunde Eins', email: 'eins@example.invalid' }
|
||
const cust = (extra = {}) => ({ consent: consent(), actor, ...extra })
|
||
const textform = { date: '2026-10-02', channel: 'email' }
|
||
const NOW = new Date('2026-10-05T10:00:00.000Z')
|
||
const consents = (p) => cl.listConsents(p)
|
||
const analyseTickets = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).includes('Hinweis auf Analyse'))
|
||
const doc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
|
||
|
||
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', email: 'eins@example.invalid', location: 'Berlin' })
|
||
aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei', email: 'zwei@example.invalid' })
|
||
aw.seed('customers', { $id: 'c4', name: '', email: 'vier@example.invalid' })
|
||
aw.seed('customers', { $id: 'c5', name: 'Kunde Fuenf', email: 'x' })
|
||
aw.seed('customers', { $id: 'c3', name: 'Kunde Drei', email: 'drei@example.invalid' })
|
||
aw.seed('subscriptions', wartungDoc)
|
||
aw.seed('subscriptions', hosting2Doc)
|
||
for (const c of ['c4', 'c5']) aw.seed('subscriptions', { ...wartungDoc, $id: `sub_w_${c}`, stripeSubscriptionId: `sub_w_${c}`, customerId: c })
|
||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'https://Baeckerei.DE/start', isPublic: true, repoFullName: 'WEBklar/baeckerei' })
|
||
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Privat', subdomain: 'privat', isPublic: false })
|
||
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Ohne Sub', isPublic: true })
|
||
aw.seed('websiteProjects', { $id: 'p4', customerId: 'c2', projectName: 'Hosting-Kunde', subdomain: 'hosting', isPublic: true })
|
||
aw.seed('websiteProjects', { $id: 'p11', customerId: 'c4', projectName: 'Ohne Name', subdomain: 'ohnename', isPublic: true, repoFullName: 'WEBklar/ohnename' })
|
||
aw.seed('websiteProjects', { $id: 'p12', customerId: 'c5', projectName: 'Schlechte Mail', subdomain: 'schlechtemail', isPublic: true })
|
||
aw.seed('websiteProjects', { $id: 'p5', customerId: 'c3', projectName: 'Ohne Abo', subdomain: 'ohneabo', isPublic: true })
|
||
|
||
beforeEach(() => {
|
||
puts.length = 0
|
||
rebuilds.length = 0
|
||
mails.sent.length = 0
|
||
})
|
||
|
||
test('Berechtigung: Wartungs-Abo, öffentlich, Subdomain, Eigentum', async () => {
|
||
assert.equal((await an.analyseEligibility('c1', 'p1')).ok, true)
|
||
assert.deepEqual(await an.analyseEligibility('c1', 'p2'), { ok: false, reason: 'nicht_oeffentlich' })
|
||
assert.deepEqual(await an.analyseEligibility('c1', 'p3'), { ok: false, reason: 'keine_subdomain' })
|
||
assert.deepEqual(await an.analyseEligibility('c2', 'p4'), { ok: false, reason: 'kein_abo' }, 'Hosting-Abo reicht nicht')
|
||
assert.deepEqual(await an.analyseEligibility('c3', 'p5'), { ok: false, reason: 'kein_abo' })
|
||
assert.deepEqual(await an.analyseEligibility('c1', 'p4'), { ok: false, reason: 'nicht_dein_projekt' })
|
||
assert.deepEqual(await an.analyseEligibility('c1', 'gibts-nicht'), { ok: false, reason: 'nicht_dein_projekt' })
|
||
})
|
||
|
||
test('Einschalten abgelehnt: Hosting-Abo, kein Abo, nicht öffentlich, AVV fehlt, Farbe', async () => {
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c2', projectId: 'p4', ...cust() }), { status: 403 })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c3', projectId: 'p5', ...cust() }), { status: 403 })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p2', ...cust() }), { status: 409 })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1' }), { status: 400, message: 'Bitte den Auftragsverarbeitungsvertrag bestätigen' })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: true, avvVersion: 'alt' } }), { status: 400 })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: false, avvVersion: config.analyse.avvVersion } }), { status: 400 })
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: 'rot', ...cust() }), { status: 400 })
|
||
assert.equal(puts.length, 0)
|
||
assert.equal(analyseTickets().length, 0)
|
||
})
|
||
|
||
test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () => {
|
||
const { doc: d, changed } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', ...cust() })
|
||
assert.equal(changed, true)
|
||
assert.equal(d.featureKey, 'analyse')
|
||
assert.equal(d.state, 'active')
|
||
assert.equal(d.enabled, true)
|
||
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 <eins@example.invalid>')
|
||
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'])
|
||
const tickets = analyseTickets()
|
||
assert.equal(tickets.length, 1)
|
||
assert.equal(tickets[0].topic, 'Datenschutzerklärung von Bäckerei um Hinweis auf Analyse ergänzen')
|
||
assert.match(tickets[0].details, /Funktion: analyse/)
|
||
assert.match(tickets[0].details, /Domain\(s\): baeckerei\.project\.webklar\.com, baeckerei\.de/)
|
||
assert.match(tickets[0].details, /https:\/\/baeckerei\.de\/datenschutz-analyse\.html/)
|
||
assert.match(tickets[0].details, /Satz \+ Link in die bestehende Datenschutzerklärung einfügen/)
|
||
assert.equal((await doc('c1', 'p1')).privacyTicketId, tickets[0].$id)
|
||
assert.equal(mails.sent.length, 1)
|
||
assert.equal(mails.sent[0].subject, 'Deine Website-Analyse ist aktiv – Bäckerei')
|
||
assert.deepEqual(mails.sent[0].to.map((t) => t.address), ['eins@example.invalid'])
|
||
})
|
||
|
||
test('Doppelt einschalten: kein zweites Ticket, keine zweite Mail, nur Farbe', async () => {
|
||
const before = await doc('c1', 'p1')
|
||
const same = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', ...cust() })
|
||
assert.equal(same.changed, false)
|
||
assert.equal(rebuilds.length, 0)
|
||
const recolored = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#CC0000', ...cust() })
|
||
assert.equal(recolored.doc.accentColor, '#cc0000')
|
||
assert.equal(recolored.doc.siteKey, before.siteKey)
|
||
assert.deepEqual(rebuilds, ['p1'])
|
||
assert.equal(analyseTickets().length, 1)
|
||
assert.equal(mails.sent.length, 0)
|
||
assert.equal(puts.length, 0)
|
||
assert.equal((await consents('p1')).length, 1, 'Farbwechsel/zweites Aktivieren schreibt keine Historie')
|
||
})
|
||
|
||
test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts', async () => {
|
||
const now = new Date('2026-10-05T10:00:00.000Z')
|
||
const { doc: d, changed } = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })
|
||
assert.equal(changed, true)
|
||
assert.equal(d.state, 'off')
|
||
assert.equal(d.enabled, false)
|
||
assert.equal(d.offSince, '2026-10-05T10:00:00.000Z')
|
||
assert.equal(puts.length, 1)
|
||
assert.equal(puts[0].active, false)
|
||
assert.deepEqual(rebuilds, ['p1'])
|
||
assert.equal((await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })).changed, false)
|
||
await assert.rejects(an.deactivateAnalyse({ customerId: 'c2', projectId: 'p1' }), { status: 403 })
|
||
})
|
||
|
||
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, audienceConfirmed: true, now: NOW })
|
||
assert.equal(d.siteKey, before.siteKey)
|
||
assert.equal(d.state, 'active')
|
||
assert.equal(d.offSince, '')
|
||
assert.equal(d.accentColor, '#111111')
|
||
assert.equal(d.avvAcceptedBy, 'Kenso')
|
||
assert.equal(d.avvConsentDate, '2026-10-02')
|
||
assert.equal(d.avvConsentChannel, 'email')
|
||
assert.equal(analyseTickets().length, 2, 'neues Ticket nach Aus/An')
|
||
})
|
||
|
||
test('Abo-Ende-Job: aktive Analyse ohne Wartungs-Abo wird beendet, K19', async () => {
|
||
// c2 hat nur Hosting-Abo, c3 gar keins: beide Analysen von Hand "aktiv" (z. B. Abo wurde gewechselt)
|
||
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
|
||
const d = await sf.ensureFeatureDoc(c, p, 'analyse')
|
||
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('active'), accentColor: '#111111' })
|
||
}
|
||
const now = new Date('2026-11-01T03:15:00.000Z')
|
||
const result = await an.runAboCheck({ now })
|
||
assert.equal(result.checked, 3)
|
||
assert.equal(result.deactivated.length, 2)
|
||
assert.equal((await doc('c1', 'p1')).state, 'active', 'Wartungs-Abo bleibt aktiv')
|
||
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
|
||
const d = await doc(c, p)
|
||
assert.equal(d.state, 'off')
|
||
assert.equal(d.offSince, now.toISOString())
|
||
}
|
||
assert.deepEqual(mails.sent.map((m) => m.subject).sort(), ['Deine Website-Analyse wurde beendet – Hosting-Kunde', 'Deine Website-Analyse wurde beendet – Ohne Abo'])
|
||
mails.sent.length = 0
|
||
assert.equal((await an.runAboCheck({ now })).deactivated.length, 0, 'zweiter Lauf: nichts mehr zu tun')
|
||
assert.equal(mails.sent.length, 0)
|
||
})
|
||
|
||
test('News-Jobs ignorieren Analyse-Dokumente', async () => {
|
||
const d = await sf.ensureFeatureDoc('c1', 'p2', 'analyse')
|
||
aw.seed('workorders', { $id: 't_an', status: 'Closed', woid: '1', topic: 'x' })
|
||
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('setup'), ticketId: 't_an' })
|
||
assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c1' }), [])
|
||
assert.deepEqual(await sf.runActivation({ customerId: 'c1' }), [])
|
||
assert.equal((await doc('c1', 'p2')).state, 'setup')
|
||
const active = await doc('c1', 'p1')
|
||
assert.equal(await sf.findActiveBySiteKey(active.siteKey.slice(0, 16)), null, 'Analyse-siteKey ist kein News-Schlüssel')
|
||
})
|
||
|
||
test('selbstheilend: Ticket scheitert einmal, Wiederholung holt Ticket, Neubau und Mail genau einmal nach', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p6', customerId: 'c1', projectName: 'Heilung', subdomain: 'heilung', isPublic: true, repoFullName: 'WEBklar/heilung' })
|
||
const original = an.analyseDeps.ticket
|
||
let failOnce = true
|
||
an.analyseDeps.ticket = async (...args) => {
|
||
if (failOnce) { failOnce = false; throw new Error('Ticketsystem down') }
|
||
return original(...args)
|
||
}
|
||
const before = analyseTickets().length
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() }), /Ticketsystem down/)
|
||
assert.equal((await doc('c1', 'p6')).state, 'active')
|
||
assert.equal(rebuilds.length, 0)
|
||
assert.equal(mails.sent.length, 0)
|
||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() })
|
||
assert.equal(r.doc.state, 'active')
|
||
assert.ok(r.doc.privacyTicketId && r.doc.activatedMailAt)
|
||
assert.equal(analyseTickets().length, before + 1)
|
||
assert.deepEqual(rebuilds, ['p6'])
|
||
assert.equal(mails.sent.length, 1)
|
||
// dritter Aufruf: nichts mehr offen
|
||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() })
|
||
assert.equal(analyseTickets().length, before + 1)
|
||
assert.deepEqual(rebuilds, ['p6'])
|
||
assert.equal(mails.sent.length, 1)
|
||
an.analyseDeps.ticket = original
|
||
})
|
||
|
||
test('putSite scheitert: Dokument bleibt unverändert', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p7', customerId: 'c1', projectName: 'Down', subdomain: 'down', isPublic: true })
|
||
const ok = an.analyseDeps.client
|
||
an.analyseDeps.client = { putSite: async () => { throw Object.assign(new Error('Auswertung nicht erreichbar'), { status: 502 }) } }
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p7', ...cust() }), { status: 502 })
|
||
assert.equal(await doc('c1', 'p7') === null || (await doc('c1', 'p7')).state === 'off', true)
|
||
assert.equal(analyseTickets().filter((t) => t.details.includes('Down')).length, 0)
|
||
an.analyseDeps.client = ok
|
||
})
|
||
|
||
test('Neubau scheitert: Analyse bleibt aktiv, kein Fehler', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p8', customerId: 'c1', projectName: 'Bau', subdomain: 'bau', isPublic: true, repoFullName: 'WEBklar/bau' })
|
||
const ok = an.analyseDeps.rebuild
|
||
an.analyseDeps.rebuild = async () => { throw new Error('Runner weg') }
|
||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p8', ...cust() })
|
||
assert.equal(r.doc.state, 'active')
|
||
assert.equal(mails.sent.length, 1)
|
||
an.analyseDeps.rebuild = ok
|
||
})
|
||
|
||
test('parallel einschalten: ein Ticket, eine Mail', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p9', customerId: 'c1', projectName: 'Parallel', subdomain: 'parallel', isPublic: true })
|
||
const before = analyseTickets().length
|
||
mails.sent.length = 0
|
||
await Promise.all([
|
||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', ...cust() }),
|
||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', ...cust() }),
|
||
])
|
||
assert.equal(analyseTickets().length, before + 1)
|
||
assert.equal(mails.sent.length, 1)
|
||
})
|
||
|
||
test('gesperrte Analyse: Kunde 403, Mitarbeiter darf', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p10', customerId: 'c1', projectName: 'Gesperrt', subdomain: 'gesperrt', isPublic: true })
|
||
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, audienceConfirmed: true, now: NOW })
|
||
assert.equal(r.doc.state, 'active')
|
||
})
|
||
|
||
test('analyseDomains: www-/Apex-Gegenstück der Live-Domain, ohne Doppelte, max. 5, nur gültige Hosts', () => {
|
||
const d = (live, sub = 'x') => an.analyseDomains({ subdomain: sub, liveDomain: live })
|
||
assert.deepEqual(d('example.de'), ['x.project.webklar.com', 'example.de', 'www.example.de'])
|
||
assert.deepEqual(d('https://www.Example.de/start'), ['x.project.webklar.com', 'www.example.de', 'example.de'])
|
||
assert.deepEqual(d(''), ['x.project.webklar.com'])
|
||
assert.deepEqual(d('kaputt!'), ['x.project.webklar.com'])
|
||
assert.ok(an.analyseDomains({ subdomain: 'x', liveDomain: 'a.b.c.example.de' }).length <= 5)
|
||
// gespeicherter Host bleibt die Live-Domain (Heatmap-Link, Ticket)
|
||
assert.deepEqual(an.analyseHosts({ subdomain: 'x', liveDomain: 'example.de' }), ['x.project.webklar.com', 'example.de'])
|
||
})
|
||
|
||
// ---- Rechtsrunde Task 5: Verantwortlichen-Angaben, Historie, Textform ----
|
||
|
||
test('analysePrivacyData: Firma/Name, E-Mail, Bereinigung, Adresse', () => {
|
||
const d = priv.analysePrivacyData
|
||
assert.deepEqual(d({ companyName: '', name: '', email: 'a@b.de' }).missing, ['firma'])
|
||
const nameOnly = d({ name: 'Max Muster', email: 'a@b.de' })
|
||
assert.equal(nameOnly.company, 'Max Muster')
|
||
assert.equal(nameOnly.ok, true)
|
||
assert.deepEqual(nameOnly.missing, [])
|
||
assert.deepEqual(d({ name: 'Max', email: 'abc' }).missing, ['email'])
|
||
assert.deepEqual(d({ name: '', email: '' }).missing, ['firma', 'email'])
|
||
assert.deepEqual(d(null).missing, ['firma', 'email'])
|
||
assert.equal(d({ companyName: 'Muster\nGmbH\t& Co', email: 'a@b.de' }).company, 'Muster GmbH & Co')
|
||
assert.equal(d({ name: 'M', email: 'a@b.de', street: 'Weg 1' }).address, 'Weg 1')
|
||
assert.equal(d({ name: 'M', email: 'a@b.de', street: 'Weg 1', postalCode: '12345', city: 'Berlin' }).address, 'Weg 1, 12345 Berlin')
|
||
assert.equal(d({ name: 'x'.repeat(300), email: 'a@b.de' }).company.length, 200)
|
||
assert.deepEqual(d({ name: 'M', email: `${'a'.repeat(200)}@b.de` }).missing, ['email'])
|
||
assert.deepEqual(d({ name: 'M', email: 'a b@c.de' }).missing, ['email'])
|
||
})
|
||
|
||
test('validateTextform: Datum, Zukunft, Fassung, Weg', () => {
|
||
const ctx = { today: '2026-10-05', minDate: '2026-10-02' }
|
||
const bad = (t) => assert.throws(() => an.validateTextform(t, ctx), { status: 400 })
|
||
bad({ date: '2026-10-01', channel: 'chat' })
|
||
bad({ date: '2026-10-06', channel: 'chat' })
|
||
bad({ date: '2026-02-30', channel: 'chat' })
|
||
bad({ date: '02.10.2026', channel: 'chat' })
|
||
bad({ date: '2026-10-02', channel: 'telefon' })
|
||
bad({ date: '2026-10-02' })
|
||
bad(undefined)
|
||
assert.deepEqual(an.validateTextform({ date: '2026-10-02', channel: 'email' }, ctx), { date: '2026-10-02', channel: 'email' })
|
||
assert.deepEqual(an.validateTextform({ date: '2026-10-05', channel: 'chat' }, ctx), { date: '2026-10-05', channel: 'chat' })
|
||
})
|
||
|
||
test('Einschalten ohne Firma/Name oder mit ungültiger E-Mail: 409, nichts passiert', async () => {
|
||
const before = analyseTickets().length
|
||
for (const [c, p] of [['c4', 'p11'], ['c5', 'p12']]) {
|
||
await assert.rejects(an.activateAnalyse({ customerId: c, projectId: p, ...cust() }), {
|
||
status: 409,
|
||
message: 'Für die Zusatzseite fehlen in Deinen Kundendaten Firma oder Name und eine gültige E-Mail-Adresse. Schreib uns im Chat, wir ergänzen sie.',
|
||
})
|
||
assert.equal((await doc(c, p)).state, 'off')
|
||
}
|
||
assert.equal(puts.length, 0)
|
||
assert.equal(analyseTickets().length, before)
|
||
assert.equal(mails.sent.length, 0)
|
||
assert.equal((await consents('p11')).length, 0)
|
||
})
|
||
|
||
test('Kunde ohne actor-Namen oder Mitarbeiter ohne Textform: 400', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p13', customerId: 'c1', projectName: 'Nachweis', subdomain: 'nachweis', isPublic: true })
|
||
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, 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)
|
||
})
|
||
|
||
test('Historie: Kunde, Aus, An durch Mitarbeiter in zeitlicher Folge; Portal-Dokument gleich', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p14', customerId: 'c1', projectName: 'Historie', subdomain: 'historie', isPublic: true })
|
||
const { doc: d1 } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', ...cust() })
|
||
let h = await consents('p14')
|
||
assert.equal(h.length, 1)
|
||
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 <eins@example.invalid>')
|
||
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 <eins@example.invalid>')
|
||
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
|
||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', accentColor: '#222222', ...cust() })
|
||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', ...cust() })
|
||
assert.equal((await consents('p14')).length, 1)
|
||
// 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, 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 <eins@example.invalid>')
|
||
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')
|
||
})
|
||
|
||
test('analyseConsent exportiert nur recordConsent und listConsents', () => {
|
||
assert.deepEqual(Object.keys(cl).sort(), ['listConsents', 'recordConsent'])
|
||
})
|
||
|
||
test('recordConsent scheitert: putSite wird zurückgenommen, Dokument bleibt aus', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p15', customerId: 'c1', projectName: 'Rollback', subdomain: 'rollback', isPublic: true })
|
||
const prev = an.analyseDeps.recordConsent
|
||
an.analyseDeps.recordConsent = async () => { throw new Error('Historie weg') }
|
||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p15', ...cust() }), /Historie weg/)
|
||
an.analyseDeps.recordConsent = prev
|
||
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', 'Dokument zurückgesetzt')
|
||
assert.equal((await doc('c1', 'p15')).offSince, '')
|
||
assert.equal(analyseTickets().filter((t) => t.details.includes('Rollback')).length, 0)
|
||
})
|
||
|
||
test('Ausschalten schreibt off-Historie; Fehler dort verhindert das Ausschalten nicht', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p16', customerId: 'c1', projectName: 'Aus', subdomain: 'aus', isPublic: true })
|
||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p16', ...cust() })
|
||
const prev = an.analyseDeps.recordConsent
|
||
an.analyseDeps.recordConsent = async () => { throw new Error('Historie weg') }
|
||
const r = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p16', now: NOW, actor })
|
||
an.analyseDeps.recordConsent = prev
|
||
assert.equal(r.doc.state, 'off')
|
||
assert.deepEqual((await consents('p16')).map((x) => x.event), ['on'])
|
||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p16', ...cust() })
|
||
await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p16', now: NOW, actor })
|
||
assert.deepEqual((await consents('p16')).map((x) => x.event), ['on', 'on', 'off'])
|
||
})
|