Analyse: K18 mit PDFs und Nachweis, K19 neu, Tickets für Einschalten, Ausschalten und Veröffentlichen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:36 +00:00
1 parent 3bac4093af
commit fbc40252a3
5 files changed
+306 -28

No files matched your search

+122 -5
View File
@@ -46,6 +46,7 @@ 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 topics = () => [...aw.docs('workorders').values()].map((t) => String(t.topic))
const doc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', email: 'eins@example.invalid', location: 'Berlin' })
@@ -111,11 +112,26 @@ test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () =
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.doesNotMatch(tickets[0].details, /Satz \+ Link in die bestehende Datenschutzerklärung einfügen/)
for (const part of ['Textbaustein', 'offensichtlicher Widerspruch', 'externer Generator', 'Stundenkontingent', 'Wir messen mit einer eigenen Reichweitenmessung ohne Cookies', 'Solange Sie im Banner nichts ausgewählt haben', 'Solange Du im Banner nichts ausgewählt hast']) {
assert.ok(tickets[0].details.includes(part), part)
}
assert.ok(tickets[0].details.length < 8000, 'passt in workorders.details (8000)')
assert.equal(topics().filter((t) => t.includes('veröffentlichen')).length, 0, 'Neubau lief: kein Veröffentlichen-Ticket')
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'])
const k18 = mails.sent[0]
assert.deepEqual(k18.attachments.map((a) => a.filename), ['AVV-Website-Analyse-2026-10-02.pdf', 'Leistungsbeschreibung-Website-Analyse-2026-10-02.pdf'])
for (const a of k18.attachments) assert.equal(Buffer.from(a.content, 'base64').subarray(0, 4).toString(), '%PDF')
assert.match(k18.html, /2026-10-02/)
assert.ok(k18.html.includes(`${config.publicBaseUrl}/rechtliches/avv-website-analyse-2026-10-02.html`))
assert.ok(k18.html.includes(`${config.publicBaseUrl}/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html`))
assert.match(k18.html, /durch Kunde Eins &lt;eins@example\.invalid&gt;/)
assert.match(k18.html, /\d\d\.\d\d\.\d{4}, \d\d:\d\d Uhr/)
assert.match(k18.text, /Das Einwilligungsbanner erscheint mit der nächsten Veröffentlichung/)
assert.doesNotMatch(k18.html, /in Textform am/)
})
test('Doppelt einschalten: kein zweites Ticket, keine zweite Mail, nur Farbe', async () => {
@@ -177,6 +193,11 @@ test('Abo-Ende-Job: aktive Analyse ohne Wartungs-Abo wird beendet, K19', async (
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'])
for (const m of mails.sent) {
assert.match(m.text, /keine Daten mehr gespeichert/)
assert.match(m.text, /46\. Tag/)
assert.doesNotMatch(m.text, /Der Banner und die Messung sind von Deiner Website entfernt/)
}
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)
@@ -204,18 +225,17 @@ test('selbstheilend: Ticket scheitert einmal, Wiederholung holt Ticket, Neubau u
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.deepEqual(rebuilds, ['p6', 'p6'], 'Neubau kommt vor dem Ticket, die Wiederholung baut erneut')
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.deepEqual(rebuilds, ['p6', 'p6'])
assert.equal(mails.sent.length, 1)
an.analyseDeps.ticket = original
})
@@ -362,7 +382,7 @@ test('Historie: Kunde, Aus, An durch Mitarbeiter in zeitlicher Folge; Portal-Dok
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, '')
assert.ok(d3.publishTicketId && d3.publishTicketId !== 'u1', 'p14 ohne Repo: neues Veröffentlichen-Ticket statt altem Marker')
h = await consents('p14')
assert.deepEqual(h.map((x) => x.event), ['on', 'off', 'on'])
assert.deepEqual(h.map((x) => x.via), ['portal', 'portal', 'woms'])
@@ -407,3 +427,100 @@ test('Ausschalten schreibt off-Historie; Fehler dort verhindert das Ausschalten
await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p16', now: NOW, actor })
assert.deepEqual((await consents('p16')).map((x) => x.event), ['on', 'on', 'off'])
})
const staff = (extra = {}) => ({ consent: consent(), by: 'Kenso', textform: { date: '2026-10-03', channel: 'email' }, audienceConfirmed: true, now: NOW, ...extra })
test('K18 Mitarbeiter-Weg: in Textform am 03.10.2026 per E-Mail', async () => {
aw.seed('websiteProjects', { $id: 'p20', customerId: 'c1', projectName: 'Textform', subdomain: 'textform', isPublic: true, repoFullName: 'WEBklar/textform' })
await an.activateAnalyse({ customerId: 'c1', projectId: 'p20', ...staff() })
assert.equal(mails.sent.length, 1)
assert.match(mails.sent[0].html, /in Textform am 03\.10\.2026 per E-Mail/)
assert.doesNotMatch(mails.sent[0].html, /Uhr/)
})
test('K18: PDFs nicht lesbar => keine Mail, Marker leer, Aktivierung gültig, spätere Wiederholung sendet', async () => {
aw.seed('websiteProjects', { $id: 'p21', customerId: 'c1', projectName: 'OhnePdf', subdomain: 'ohnepdf', isPublic: true, repoFullName: 'WEBklar/ohnepdf' })
const { legalPdfAttachments } = await import('../server/services/analyseLegal.js')
const ok = an.analyseDeps.legalPdfs
an.analyseDeps.legalPdfs = () => legalPdfAttachments('1999-01-01')
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p21', ...cust() })
assert.equal(r.doc.state, 'active')
assert.equal(r.doc.activatedMailAt, '')
assert.equal(mails.sent.length, 0)
an.analyseDeps.legalPdfs = ok
const again = await an.activateAnalyse({ customerId: 'c1', projectId: 'p21', ...cust() })
assert.ok(again.doc.activatedMailAt)
assert.equal(mails.sent.length, 1)
})
test('Entfernen-Ticket: Kunde, Mitarbeiter, zweimal = ein Ticket, Aus/An/Aus = zwei', async () => {
aw.seed('websiteProjects', { $id: 'p22', customerId: 'c1', projectName: 'Bäckerei Aus', subdomain: 'baeckereiaus', isPublic: true, repoFullName: 'WEBklar/aus' })
const removal = () => topics().filter((t) => t === 'Website-Analyse von Bäckerei Aus entfernen')
await an.activateAnalyse({ customerId: 'c1', projectId: 'p22', ...cust() })
assert.equal(removal().length, 0)
const off1 = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p22', now: NOW, actor })
assert.ok(off1.doc.removalTicketId)
await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p22', now: NOW, actor })
assert.equal(removal().length, 1)
const ticket = [...aw.docs('workorders').values()].find((t) => t.topic === 'Website-Analyse von Bäckerei Aus entfernen')
assert.match(ticket.details, /fünf Werktagen/)
assert.match(ticket.details, /Stundenkontingent/)
await an.activateAnalyse({ customerId: 'c1', projectId: 'p22', ...staff() })
const off2 = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p22', now: NOW, by: 'Kenso' })
assert.equal(removal().length, 2)
assert.notEqual(off2.doc.removalTicketId, off1.doc.removalTicketId)
})
test('Entfernen-Ticket: Ticketfehler verhindert das Ausschalten nicht', async () => {
aw.seed('websiteProjects', { $id: 'p23', customerId: 'c1', projectName: 'TicketFehler', subdomain: 'ticketfehler', isPublic: true, repoFullName: 'WEBklar/tf' })
await an.activateAnalyse({ customerId: 'c1', projectId: 'p23', ...cust() })
const original = an.analyseDeps.ticket
an.analyseDeps.ticket = async () => { throw new Error('Ticketsystem down') }
const r = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p23', now: NOW, actor })
an.analyseDeps.ticket = original
assert.equal(r.doc.state, 'off')
assert.equal(r.changed, true)
})
test('Abo-Ende über runAboCheck: Entfernen-Ticket genau einmal', async () => {
aw.seed('websiteProjects', { $id: 'p24', customerId: 'c3', projectName: 'Abo Ende', subdomain: 'aboende', isPublic: true, repoFullName: 'WEBklar/aboende' })
const d = await sf.ensureFeatureDoc('c3', 'p24', 'analyse')
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('active'), accentColor: '#111111' })
await an.runAboCheck({ now: NOW })
await an.runAboCheck({ now: NOW })
assert.equal(topics().filter((t) => t === 'Website-Analyse von Abo Ende entfernen').length, 1)
})
test('rebuildSafe-Ergebnis: kein_repo, preview_aus, fehler => Veröffentlichen-Ticket; Erfolg => keins', async () => {
const cases = [
['p30', 'KeinRepo', {}, 'kein_repo'],
['p31', 'PreviewAus', { repoFullName: 'WEBklar/pa', previewMode: 'off' }, 'preview_aus'],
['p32', 'BauFehler', { repoFullName: 'WEBklar/bf' }, 'fehler'],
['p33', 'BauErgebnisFehler', { repoFullName: 'WEBklar/bef' }, 'fehler'],
['p34', 'BauOk', { repoFullName: 'WEBklar/bo' }, null],
]
const original = an.analyseDeps.rebuild
for (const [id, name, extra, reason] of cases) {
aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: name, subdomain: name.toLowerCase(), isPublic: true, ...extra })
an.analyseDeps.rebuild = async (p) => {
rebuilds.push(p.$id)
if (id === 'p32') throw new Error('Runner weg')
if (id === 'p33') return { error: 'Build kaputt' }
return { ok: true }
}
rebuilds.length = 0
const r = await an.activateAnalyse({ customerId: 'c1', projectId: id, ...cust() })
const pub = [...aw.docs('workorders').values()].filter((t) => t.topic === `Website-Analyse von ${name} veröffentlichen`)
assert.equal(pub.length, reason ? 1 : 0, name)
if (reason) {
assert.equal(r.doc.publishTicketId, pub[0].$id)
assert.ok(pub[0].details.includes(reason === 'kein_repo' ? 'kein Repo' : reason === 'preview_aus' ? 'ausgeschaltet' : 'fehlgeschlagen'), name)
const privacy = [...aw.docs('workorders').values()].find((t) => t.topic === `Datenschutzerklärung von ${name} um Hinweis auf Analyse ergänzen`)
assert.match(privacy.details, /Veröffentlichung:/)
}
if (id === 'p30' || id === 'p31') assert.deepEqual(rebuilds, [], 'Rebuild-Stub nicht aufgerufen')
await an.activateAnalyse({ customerId: 'c1', projectId: id, ...cust() })
assert.equal([...aw.docs('workorders').values()].filter((t) => t.topic === `Website-Analyse von ${name} veröffentlichen`).length, reason ? 1 : 0, 'genau eines je Aktivierung')
}
an.analyseDeps.rebuild = original
})