Gaestetexte/Datenschutz: Storno-Regel im Archiv, PDF-Pruefsumme, PDF-Kopfpruefung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 05:03:54 +00:00
1 parent 9a2a70b491
commit ec320853ce
13 files changed
+124 -12

No files matched your search

+37
View File
@@ -1559,6 +1559,43 @@ test('textform_pflicht_modus_on: fremdes Projekt, Kundensitzung und nicht lesbar
assert.equal((await aktivierenF13(k, feldF13(k))).status, 200)
})
test('textform_pflicht_modus_on: eine leere oder keine PDF-Datei als Unterlage ergibt 503 vor jeder Änderung (R121), danach mit gültigen Dateien 200', async (t) => {
modusMerken(t)
termineMit('on', [])
const fs = await import('node:fs')
const os = await import('node:os')
const path = await import('node:path')
const { legalPdfFiles } = await import('../server/services/legalDocs.js')
const echt = legalPdfFiles('termine', config.termine.avvVersion)
const kopien = fs.mkdtempSync(path.join(os.tmpdir(), 'f13-pdf-'))
fs.mkdirSync(path.join(kopien, 'rechtliches'))
const kopiere = () => {
for (const f of Object.values(echt)) fs.copyFileSync(f.path, path.join(kopien, 'rechtliches', path.basename(f.path)))
}
t.after(() => {
setLegalDirForTests(null)
fs.rmSync(kopien, { recursive: true, force: true })
})
setLegalDirForTests(kopien)
const k = legeKundeF13()
const mailsVorher = mails.sent.length
for (const [doc, inhalt] of [['lb', ''], ['avv', '<html>kein PDF</html>'], ['lb', '%PDF']]) {
kopiere()
fs.writeFileSync(path.join(kopien, 'rechtliches', path.basename(echt[doc].path)), inhalt)
const res = await aktivierenF13(k, feldF13(k))
assert.equal(res.status, 503, `${doc}: ${JSON.stringify(inhalt)}`)
assert.match(res.body.error, /Rechtsunterlagen/)
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null, 'nichts angelegt')
assert.equal(consentsDesF13(k.projectId).length, 0, 'kein Nachweis')
assert.equal(mails.sent.length, mailsVorher)
}
kopiere()
const ok = await aktivierenF13(k, feldF13(k))
assert.equal(ok.status, 200, JSON.stringify(ok.body))
assert.equal(consentsDesF13(k.projectId).length, 1)
assert.equal(k16AnF13(k.email).length, 1)
})
test('get_admin_termine_feld: GET /api/team/funktionen/:kunde liefert termine { textformNoetig, avvVersion, links } je Modus', async (t) => {
modusMerken(t)
const k = legeKundeF13({ email: 'get-f13@example.invalid' })
+33
View File
@@ -200,6 +200,39 @@ test('featureLegalReady("termine"): je PDF stat und "%PDF-", nie den ganzen Inha
assert.equal(await featureLegalReady('termine'), true, 'mit den ausgelieferten Dateien bereit')
})
// R121: ein Anhang ist ein PDF. Eine leere oder keine PDF-Datei wird nie angehängt, sondern wirft (die Aufrufer fangen den Wurf).
test('legalPdfAttachments: eine leere Datei oder eine Datei ohne "%PDF-" am Anfang wirft, ausgenommene Dokumente zählen nicht', async () => {
const kopien = mkdtempSync(join(tmpdir(), 'legal-anhang-'))
mkdirSync(join(kopien, 'rechtliches'))
const echtePdf = Object.fromEntries(Object.entries(legalPdfFiles('termine', V)).map(([doc, f]) => [doc, f.path]))
const pdfPfad = (doc) => join(kopien, 'rechtliches', echtePdf[doc].split('/').at(-1))
const bereit = () => {
for (const pfad of Object.values(echtePdf)) copyFileSync(pfad, join(kopien, 'rechtliches', pfad.split('/').at(-1)))
}
try {
setLegalDirForTests(kopien)
bereit()
const gut = await legalPdfAttachments('termine', V)
assert.equal(gut.length, 3)
for (const a of gut) assert.equal(a.content.subarray(0, 5).toString('latin1'), '%PDF-')
for (const doc of ['lb', 'avv', 'widerruf']) {
for (const [mangel, inhaltDatei] of [['leer', ''], ['kein PDF', '<html>kein PDF</html>'], ['zu kurz', '%PDF'], ['PDF nicht am Anfang', ' %PDF-1.4\n']]) {
writeFileSync(pdfPfad(doc), inhaltDatei)
await assert.rejects(legalPdfAttachments('termine', V), /PDF/, `${doc}: ${mangel}`)
await assert.rejects(featureLegalAttachments('termine'), /PDF/, `${doc}: ${mangel} (Funktion)`)
// ein ausgenommenes Dokument wird nicht gelesen und darf kaputt sein
const ohne = await legalPdfAttachments('termine', V, { ohne: [doc] })
assert.equal(ohne.length, 2, `${doc}: ${mangel}, ausgenommen`)
}
bereit()
assert.equal((await legalPdfAttachments('termine', V)).length, 3, `${doc}: wieder in Ordnung`)
}
} finally {
setLegalDirForTests(null)
rmSync(kopien, { recursive: true, force: true })
}
})
// Fassung wählbar (Task F8, Fix-Runde 1): K15 schickt die Unterlagen der Fassung, der der Kunde zugestimmt hat
test('featureLegalAttachments / featureLegalReady / featureLegalPaths mit opts.fassung', async () => {
assert.deepEqual(featureLegalPaths('termine'), legalPaths('termine', V))
+15 -1
View File
@@ -604,11 +604,25 @@ test('wiederkehr_satz_gleich_lb: Rückkehr des Abos innerhalb von 30 Tagen steht
}
})
test('pdf_pruefsumme: neben dem PDF der Datenschutzerklärung liegt .sha256 mit der SHA-256 von public/datenschutz.html (HTML geändert ohne neues PDF fällt auf)', () => {
const summe = read('docs/WEBklar-Datenschutzerklaerung.pdf.sha256')
assert.match(summe, /^[0-9a-f]{64}\n$/, 'nur die Prüfsumme und ein Zeilenende')
assert.equal(summe.trim(), sha256(DS), 'public/datenschutz.html wurde geändert, ohne das PDF neu zu drucken (scripts/build-legal-pdfs.sh datenschutz)')
// eine Änderung am HTML ergäbe eine andere Prüfsumme
assert.notEqual(createHash('sha256').update(`${read(DS)} `).digest('hex'), summe.trim())
// das Skript schreibt die Prüfsumme und druckt bei passender Prüfsumme nicht neu (FORCE=1 druckt neu)
const sh = read('scripts/build-legal-pdfs.sh')
const modus = sh.slice(sh.indexOf('\ndatenschutz)'), sh.indexOf('\nall)'))
assert.ok(modus.includes('sha256sum "$quelle"') && modus.includes('echo "$summe" > "$ziel.sha256"'))
assert.ok(modus.includes('${FORCE:-}') && modus.includes('(unveraendert)'))
assert.ok(modus.includes('pdf public/datenschutz.html docs/WEBklar-Datenschutzerklaerung.pdf'))
})
test('datenschutz_pdf: das PDF der Datenschutzerklärung ist eingecheckt (> 20 KB, %PDF) und das Skript baut es im Modus datenschutz', () => {
const pdf = readFileSync(file('docs/WEBklar-Datenschutzerklaerung.pdf'))
assert.ok(pdf.length > 20 * 1024, `PDF zu klein: ${pdf.length} Bytes`)
assert.equal(pdf.subarray(0, 5).toString('latin1'), '%PDF-')
assert.ok(pdf.subarray(-2048).toString('latin1').includes('%%EOF'), 'PDF vollständig geschrieben')
const skript = read('scripts/build-legal-pdfs.sh')
assert.match(skript, /datenschutz\)\n\s*pdf public\/datenschutz\.html docs\/WEBklar-Datenschutzerklaerung\.pdf\n\s*;;/)
assert.match(skript, /\ndatenschutz\)[\s\S]*\n\s+pdf public\/datenschutz\.html docs\/WEBklar-Datenschutzerklaerung\.pdf\n[\s\S]*?;;/)
})
+11 -1
View File
@@ -135,7 +135,17 @@ const HASH_2026_10_02 = '920633e2e23c9c98aded0c1509db5cac1465058a025aeddc76a9a12
// SHA-256 der Archivdatei der aktuellen Fassung: ändert sich ein Gästetext, ohne dass die Fassung (RECHTSTEXTE_VERSION) ein neues Datum
// und diese Datei eine neue Archivdatei bekommt, schlägt der Test an. Bei einer neuen Fassung: neue Datei, neues Datum, neuer Pin.
const HASH_AKTUELL = { '2026-10-05': '6bcad4c4a02ea5e39b3ec690d40617fdfb3cb1290aadd3304f0ecabab14c4a59' }
const HASH_AKTUELL = { '2026-10-05': '830bc7364afc852755ca66d2213866160812f35ba7221f59198acf37588a8ada' }
test('archiv_storno_regel: die Fassung 2026-10-05 nennt den Storno-Haken nur für Leistungen mit Preis, die eingefrorene Fassung 2026-10-02 die alte Regel', () => {
const neu = lies(archivPfad())
assert.ok(neu.includes('### Stornobedingungen (Pflicht bei einer Leistung mit Preis, wenn der Betrieb Stornobedingungen hat; bei kostenlosen Leistungen fragt der Terminplaner nicht danach;'))
assert.ok(!neu.includes('### Stornobedingungen (Pflicht, wenn der Betrieb Stornobedingungen hat;'))
const alt = lies(`${ARCHIV_DIR}/termine-gaestetexte-2026-10-02.md`)
assert.ok(alt.includes('### Stornobedingungen (Pflicht, wenn der Betrieb Stornobedingungen hat;'), 'die Altfassung beschreibt die alte Regel')
assert.ok(lies('docs/rechtliches/termine-hinweise.md').includes('| Stornobedingungen | der Betrieb hat Stornobedingungen hinterlegt und die gebuchte Leistung hat einen Preis'))
assert.ok(lies(`${ARCHIV_DIR}/README.md`).includes('Pflicht ist er nur noch bei einer Leistung mit Preis'))
})
test('textstand_pin: Hash der aktuellen Archivdatei ist festgenagelt (Textänderung ohne neues Datum fällt auf)', () => {
assert.ok(Object.hasOwn(HASH_AKTUELL, RECHTSTEXTE_VERSION), `kein Hash-Pin für die Fassung ${RECHTSTEXTE_VERSION}`)