Files
Webklar-Kundenbereich/test/termineRechtstexte.test.js
T
ClaudeandClaude Sonnet 5.5 06ebcd1b0a Terminplaner: Datenschutz-Absatz als Rechtstext-Baustein
datenschutzAbsatz(einstellung, anrede) liefert den Absatz "Online-Terminbuchung" fuer die
Datenschutzerklaerung der Betriebs-Website als reinen Text (Sie- und Du-Fassung, fuenf bedingte
Saetze: Telefon-Feld, Nachricht/Zusatzfelder, Erinnerung, Widerruf ueber zeigtWiderruf, Telefon des
Betriebs). RECHTSTEXTE_ENTWURF = true. Test pinnt den Wortlaut gegen die Markdown-Quelle in allen
32 Bedingungskombinationen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:47:58 +00:00

484 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import {
RECHTSTEXTE_VERSION,
RECHTSTEXTE_ENTWURF,
widerrufsbelehrung,
formularHinweis,
datenschutzAbsatz,
ZUSTIMMUNG_VORZEITIG,
ZUSTIMMUNG_STORNO,
ZUSTIMMUNG_ERINNERUNG,
GESUNDHEIT_HINWEIS,
} from '../server/services/termine/rechtstexte.js'
// ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen.
// Die Belehrung wird gegen den Wortlaut der Anlagen 1 und 2 zu Art. 246a EGBGB geprueft (docs/rechtliches/quellen/,
// abgerufen 2026-10-02): jeder Absatz des Musters muss unveraendert im Ergebnis stehen, nur die Luecken sind gefuellt.
const KOPF = 'ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen'
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
const zeilen = (text) => text.split('\n').map((z) => z.trim())
// Wortlisten fuer die Anredepruefung: Du-Woerter duerfen in der Sie-Fassung nicht stehen und umgekehrt
const DU_WOERTER = /\bdein|\bdu\b|\bdir\b|\bdich\b/i
const SIE_WOERTER = /\bIhr|\bIhnen\b|\bSie (haben|können|erhalten|wünschen|machen|buchen|klicken|sind|wollen)\b/
const BETRIEB = { name: 'Salon Müller & Söhne', adresse: 'Hauptstraße 1, 67722 Winnweiler', telefon: '06301 1234', email: 'info@salon-mueller.de' }
const KONTAKT = 'Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, Telefon: 06301 1234, E-Mail: info@salon-mueller.de'
// Platzhalter-Betrieb: so steht die Belehrung in docs/rechtliches/termine-widerrufsbelehrung-muster.md
const PLATZHALTER = { name: '{{Name des Betriebs}}', adresse: '{{Anschrift}}', telefon: '{{Telefon}}', email: '{{E-Mail}}' }
const ANLAGE_1 = lies('docs/rechtliches/quellen/anlage-1.txt')
const ANLAGE_2 = lies('docs/rechtliches/quellen/anlage-2.txt')
/** Muster-Absaetze der Anlage 1 (zwischen Titel und Gestaltungshinweisen) mit den Luecken des Gesetzestexts. */
function musterZeilen() {
const z = zeilen(ANLAGE_1)
const von = z.indexOf('Widerrufsbelehrung')
const bis = z.findIndex((s) => s.startsWith('Gestaltungshinweise:'))
assert.ok(von >= 0 && bis > von, 'Aufbau von anlage-1.txt hat sich geaendert')
return z.slice(von, bis).filter(Boolean)
}
/** Textbaustein a) fuer Dienstleistungsvertraege: „des Vertragsabschlusses.“ */
function fristBaustein() {
const m = /Dienstleistungsvertrags[^„]*„([^“]+)“/.exec(ANLAGE_1)
assert.ok(m, 'Gestaltungshinweis a) fehlt in anlage-1.txt')
return m[1]
}
const belehrung = (betrieb = BETRIEB) => widerrufsbelehrung(betrieb)
test('Fassung der Rechtstexte', () => {
assert.equal(RECHTSTEXTE_VERSION, '2026-10-02')
})
// ---------------------------------------------------------------- Widerrufsbelehrung
test('Belehrung: alle Muster-Absaetze der Anlage 1 stehen wörtlich darin, nur die Lücken sind gefüllt', () => {
const text = belehrung()
const muster = musterZeilen()
assert.equal(muster.length, 8, 'Anlage 1: erwartet Titel, Widerrufsrecht, 3 Absätze, Folgen des Widerrufs und 1 Absatz')
assert.equal(fristBaustein(), 'des Vertragsabschlusses.')
for (const zeile of muster) {
const erwartet = zeile
.replace('ab dem Tag .', `ab dem Tag ${fristBaustein()}`)
.replace('uns ( )', `uns (${KONTAKT})`)
assert.ok(text.includes(erwartet), `Absatz fehlt oder ist verändert: ${erwartet}`)
}
})
test('Belehrung: Satzrahmen der Widerrufsfunktion wörtlich aus der Anlage, nur die Lücke „…“ ist mit einem geeigneten Hinweis gefüllt', () => {
const text = belehrung()
// Rahmen und Gesetzesteil kommen aus anlage-1.txt, nichts davon ist hier abgeschrieben:
// „Sie können Ihr Widerrufsrecht auch online unter … [Internetadresse oder anderen geeigneten Hinweis …] ausüben. Wenn Sie diese Online-Funktion nutzen, …“
const m = /„(Sie können Ihr Widerrufsrecht auch online unter) … \[[^\]]+\] (ausüben\.) (Wenn Sie diese Online-Funktion nutzen, [^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(m, 'Gestaltungshinweis zur Widerrufsfunktion fehlt oder hat sich geändert in anlage-1.txt')
assert.ok(m[3].includes('Eingangsbestätigung') && m[3].includes('Uhrzeit ihres Eingangs'))
const hinweis = '„Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail)'
const absatz = `${m[1]} ${hinweis} ${m[2]} ${m[3]}`
assert.ok(text.includes(absatz), 'Absatz zur Widerrufsfunktion fehlt oder weicht vom Satzrahmen der Anlage ab')
assert.ok(text.includes('Vertrag widerrufen') && text.includes('Termin verwalten'))
assert.ok(!/https?:\/\/|www\./.test(text), 'keine Internetadresse, kein Parameter')
assert.ok(!text.includes('über die Schaltfläche'), 'kein umformulierter Satzrahmen')
})
test('Belehrung: Wertersatz für Dienstleistungen, Unzutreffendes (Wasser/Gas/Strom/Fernwärme) gestrichen', () => {
const text = belehrung()
const m = /Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser\/Gas\/Strom\/Fernwärme \[Unzutreffendes streichen\] (während[^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(m, 'Wertersatz-Absatz fehlt in anlage-1.txt')
assert.ok(text.includes(`Haben Sie verlangt, dass die Dienstleistungen ${m[1]}`), 'Wertersatz-Absatz fehlt oder weicht ab')
assert.ok(!/Wasser|Strom|Fernwärme|\[Unzutreffendes/.test(text))
})
test('Belehrung: Muster-Widerrufsformular der Anlage 2 steht wörtlich darin, Betrieb in der „An“-Zeile', () => {
const text = belehrung()
const z = zeilen(ANLAGE_2)
const start = z.indexOf('Muster-Widerrufsformular')
assert.ok(start >= 0, 'Aufbau von anlage-2.txt hat sich geaendert')
const inhalt = z.slice(start).filter((s) => s && s !== '–')
assert.ok(inhalt.length >= 9)
let anZeile = 0
for (const zeile of inhalt) {
if (zeile.startsWith('An [hier ist')) {
anZeile += 1
assert.ok(text.includes('An Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, info@salon-mueller.de:'), '„An“-Zeile mit Betrieb fehlt')
} else {
assert.ok(text.includes(zeile), `Formularzeile fehlt oder ist verändert: ${zeile}`)
}
}
assert.equal(anZeile, 1)
// die (*)-Alternativen bleiben unverändert stehen
assert.ok(text.includes('Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)'))
assert.ok(text.includes('(*) Unzutreffendes streichen.'))
})
test('Belehrung: Betriebsdaten, kein offener Platzhalter, nichts zu Waren-Rücksendung oder Webformular', () => {
const text = belehrung()
for (const wert of Object.values(BETRIEB)) assert.ok(text.includes(wert), `Betriebsangabe fehlt: ${wert}`)
assert.ok(text.includes('Vertrag widerrufen'))
assert.ok(text.includes('Termin verwalten'))
assert.ok(text.includes('vierzehn Tagen'))
assert.ok(!/\{\{|\}\}|\[|\]|…|\.\.\.|\(\s*\)/.test(text), 'offener Platzhalter')
assert.ok(!/Rücksendung|zurückzusenden|Besitz genommen|Wir holen die Waren|Sie haben die Waren|elektronisch ausfüllen|Wir können die Rückzahlung verweigern/.test(text))
assert.ok(!/undefined|null|NaN/.test(text))
})
test('Belehrung: reiner Text mit Zeilenumbrüchen, nicht HTML-maskiert (die Mail maskiert beim Einsetzen)', () => {
const text = belehrung()
assert.equal(typeof text, 'string')
assert.ok(text.includes('\n'))
assert.ok(text.startsWith('Widerrufsbelehrung\n'))
assert.ok(text.includes('Salon Müller & Söhne'))
assert.ok(!text.includes('&amp;') && !/<[a-z/]/i.test(text))
assert.ok(!text.includes('\r'))
assert.equal(belehrung(), belehrung(), 'deterministisch')
})
test('Belehrung: Name, Anschrift, Telefon und E-Mail sind Pflicht, eine unvollständige Belehrung entsteht nie (TypeError)', () => {
for (const feld of ['name', 'adresse', 'telefon', 'email']) {
for (const leer of [undefined, null, '', ' ', ' \n\t ', 42, {}]) {
assert.throws(() => widerrufsbelehrung({ ...BETRIEB, [feld]: leer }), TypeError, `${feld}=${JSON.stringify(leer)}`)
}
const ohne = { ...BETRIEB }
delete ohne[feld]
assert.throws(() => widerrufsbelehrung(ohne), TypeError, `${feld} fehlt`)
}
assert.throws(() => widerrufsbelehrung({ name: 'Salon Müller', email: 'info@salon.de' }), TypeError)
assert.throws(() => widerrufsbelehrung(undefined), TypeError)
assert.throws(() => widerrufsbelehrung(null), TypeError)
assert.throws(() => widerrufsbelehrung('Salon'), TypeError)
})
test('Belehrung: Leerraum in den Angaben wird zu einem Leerzeichen, vollständige Angaben stehen in Klammer und „An“-Zeile', () => {
const text = belehrung({ ...BETRIEB, name: ' Salon \n Müller ', adresse: 'Hauptstraße 1,\n67722 Winnweiler' })
assert.ok(text.includes('uns (Salon Müller, Hauptstraße 1, 67722 Winnweiler, Telefon: 06301 1234, E-Mail: info@salon-mueller.de)'))
assert.ok(text.includes('An Salon Müller, Hauptstraße 1, 67722 Winnweiler, info@salon-mueller.de:'))
})
test('Belehrung ist die Quelle von docs/rechtliches/termine-widerrufsbelehrung-muster.md (Platzhalter-Fassung steht dort wörtlich)', () => {
const md = lies('docs/rechtliches/termine-widerrufsbelehrung-muster.md')
assert.ok(md.replace(/\r/g, '').includes(widerrufsbelehrung(PLATZHALTER)), 'Textblock im Muster-Dokument weicht von widerrufsbelehrung() ab')
})
// ---------------------------------------------------------------- Zustimmungen und Hinweise
test('Zustimmungstexte wortgleich zu den Vorgaben (Sie-Form)', () => {
assert.equal(
ZUSTIMMUNG_VORZEITIG('Salon Müller'),
'Ich verlange ausdrücklich, dass Salon Müller mit der Leistung vor Ablauf der Widerrufsfrist beginnt. '
+ 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). '
+ 'Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).',
)
assert.equal(ZUSTIMMUNG_STORNO, 'Ich habe die Stornobedingungen gelesen.')
assert.equal(ZUSTIMMUNG_ERINNERUNG(24), 'Ich möchte 24 Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.')
assert.equal(ZUSTIMMUNG_ERINNERUNG(2), 'Ich möchte 2 Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.')
assert.equal(GESUNDHEIT_HINWEIS, 'Bitte keine Gesundheitsangaben.')
})
test('Zustimmung zum vorzeitigen Beginn setzt den Betriebsnamen unverändert ein (kein Ersetzungsmuster)', () => {
const text = ZUSTIMMUNG_VORZEITIG("Frisör $& $1 'Anna'")
assert.ok(text.startsWith("Ich verlange ausdrücklich, dass Frisör $& $1 'Anna' mit der Leistung vor Ablauf"))
})
const EINSTELLUNG = { anrede: 'sie', aufbewahrungTage: 90, betrieb: BETRIEB }
test('Formular-Hinweis (Sie): Zweck, Auftragsverarbeiter, Löschfristen, keine Cookies, Gesundheit, Verantwortlicher', () => {
const text = formularHinweis(EINSTELLUNG)
assert.ok(text.startsWith('Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO).'))
assert.ok(text.includes('in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland)'))
assert.ok(text.includes('Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist.'))
assert.ok(text.includes('Es werden keine Cookies gesetzt.'))
assert.ok(text.includes('Bitte machen Sie keine Angaben zu Ihrer Gesundheit.'))
assert.ok(text.includes('Mehr erfahren Sie in unserer Datenschutzerklärung.'))
assert.ok(text.endsWith('Verantwortlich: Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler.'))
assert.ok(!/\{\{|\}\}|\[|\]|undefined|null/.test(text))
assert.ok(!text.includes('info@salon-mueller.de'), 'Betriebs-E-Mail gehört nicht in die öffentliche Konfiguration')
assert.ok(!/\bdein|\bdu\b/i.test(text))
})
test('Formular-Hinweis (Du): gleiche Aussagen in Du-Form', () => {
const text = formularHinweis({ ...EINSTELLUNG, anrede: 'du', aufbewahrungTage: 30 })
assert.ok(text.startsWith('Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO).'))
assert.ok(text.includes('Wir speichern deine Daten bis 30 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist.'))
assert.ok(text.includes('Bitte mach keine Angaben zu deiner Gesundheit.'))
assert.ok(text.includes('Mehr erfährst du in unserer Datenschutzerklärung.'))
assert.ok(text.includes('Es werden keine Cookies gesetzt.'))
assert.ok(!/\bSie\b|\bIhre|\bIhren/.test(text))
})
test('Formular-Hinweis: Sie ist die Vorgabe, Aufbewahrung 90 Tage ohne gültigen Wert, Adresse optional', () => {
const text = formularHinweis({ betrieb: { name: 'Salon Müller', email: 'info@salon.de' } })
assert.ok(text.includes('Wir speichern Ihre Daten bis 90 Tage nach dem Termin'))
assert.ok(text.endsWith('Verantwortlich: Salon Müller.'))
assert.ok(formularHinweis({ ...EINSTELLUNG, aufbewahrungTage: 'viel' }).includes('bis 90 Tage'))
assert.ok(formularHinweis({ ...EINSTELLUNG, aufbewahrungTage: 365 }).includes('bis 365 Tage'))
assert.ok(formularHinweis({ ...EINSTELLUNG, anrede: 'unbekannt' }).includes('Ihre Angaben'))
assert.throws(() => formularHinweis({}), TypeError)
assert.throws(() => formularHinweis(undefined), TypeError)
})
// ---------------------------------------------------------------- Datenschutz-Absatz „Online-Terminbuchung“ (Quelle der Einbauanleitung und der Doku)
test('Rechtstexte sind Entwurf, bis Block F die ENTWURF-Köpfe und diese Konstante zusammen entfernt', () => {
assert.equal(RECHTSTEXTE_ENTWURF, true)
})
const DS_SCHALTER = ['telefonFeld', 'freitext', 'erinnerung', 'widerruf', 'telefon']
// Bedingungstext der Quelle ([[wenn …: …]]) -> Schalter; eine unbekannte Bedingung in der Quelle lässt den Test scheitern
const DS_BEDINGUNGEN = [
[/^Telefon-Feld aktiv$/, 'telefonFeld'],
[/^Nachricht oder Zusatzfelder aktiv$/, 'freitext'],
[/^Erinnerung angeboten$/, 'erinnerung'],
[/^Gäste Verbraucher sein können$/, 'widerruf'],
[/^Telefon hinterlegt$/, 'telefon'],
]
// Satz bzw. Satzteil, den jeder Schalter einschaltet (Sie- und Du-Fassung)
const DS_MARKER = {
telefonFeld: /Telefonnummer/,
freitext: /im Formular mitteil/,
erinnerung: /Erinnerung/,
widerruf: /Vertrag widerrufen/,
telefon: /telefonisch erreichen/,
}
const DS_ALLE_AN = Object.fromEntries(DS_SCHALTER.map((k) => [k, true]))
const DS_ALLE_AUS = Object.fromEntries(DS_SCHALTER.map((k) => [k, false]))
/** Echte, normalisierte Einstellung; jeder Schalter steuert genau die Einstellung, aus der die Funktion ihn ableitet. */
function dsEinstellung(zustand, extra = {}) {
return {
anrede: 'sie',
aufbewahrungTage: 90,
betrieb: { ...BETRIEB, telefon: zustand.telefon ? BETRIEB.telefon : '' },
gastFelder: { telefon: zustand.telefonFeld ? 'optional' : 'aus', nachricht: zustand.freitext ? 'optional' : 'aus', zusatz: [] },
erinnerung: { anbieten: zustand.erinnerung, stundenVorher: 24 },
recht: { gaesteSind: zustand.widerruf ? 'verbraucher' : 'unternehmer', freizeit: false },
ressourcen: [{ id: 'r1', aktiv: true }],
leistungen: [{ id: 'l1', aktiv: true, ressourcen: ['r1'] }],
...extra,
}
}
/** Absätze durch Leerzeile, innerhalb eines Absatzes jeder Leerraum ein Leerzeichen. */
const normalisiere = (text) => text.split(/\n[ \t]*\n/).map((a) => a.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n\n')
/**
* Datenschutz-Absatz aus der Markdown-Quelle: Blockquote der Fassung („Sie“ | „Du“), `> `, `**` und `###` entfernt,
* [[wenn …: X]] durch X (Schalter an) oder nichts (Schalter aus) ersetzt, Platzhalter mit den Testwerten gefüllt.
* Ein Satzteil, der mit einem Wort beginnt („und die Angaben …“), steht in der Quelle ohne Leerzeichen am Vordertext
* („Platz[[…: und die Angaben …]]“); er wird hier mit Leerzeichen angefügt (Satzteile mit Komma bleiben unmittelbar dran).
*/
function datenschutzQuelle(fassung, zustand, { tage = 90 } = {}) {
const md = lies('docs/rechtliches/termine-datenschutz-absatz.md').replace(/\r/g, '')
const teil = md.split(/^## Fassung /m).slice(1).find((t) => t.startsWith(fassung))
assert.ok(teil, `Fassung ${fassung} fehlt in der Quelle`)
const zitat = teil.split('\n').filter((z) => z.startsWith('>')).map((z) => z.replace(/^>\s?/, '').replace(/^### /, ''))
assert.ok(zitat.length > 10, 'Blockquote der Quelle nicht gefunden')
const roh = zitat.join('\n').replace(/\*\*/g, '')
const bedingt = roh.replace(/\[\[wenn ([^:]+): ([\s\S]*?)\]\]/g, (_, bedingung, satz, offset, ganz) => {
const eintrag = DS_BEDINGUNGEN.find(([muster]) => muster.test(bedingung))
assert.ok(eintrag, `unbekannte Bedingung in der Quelle: ${bedingung}`)
if (!zustand[eintrag[1]]) return ''
return /^[A-Za-zÄÖÜäöüß]/.test(satz) && offset > 0 && !/\s/.test(ganz[offset - 1]) ? ` ${satz}` : satz
})
const werte = { Firma: BETRIEB.name, Anschrift: BETRIEB.adresse, 'E-Mail': BETRIEB.email, Telefon: BETRIEB.telefon, Tage: String(tage) }
const gefuellt = bedingt.replace(/\{\{([^}]+)\}\}/g, (_, name) => {
assert.ok(Object.hasOwn(werte, name), `unbekannter Platzhalter in der Quelle: ${name}`)
return werte[name]
})
assert.ok(!/\[\[|\]\]|\{\{|\}\}/.test(gefuellt), 'offene Klammer nach dem Ersetzen')
return normalisiere(gefuellt)
}
const alleZustaende = () =>
Array.from({ length: 2 ** DS_SCHALTER.length }, (_, maske) => Object.fromEntries(DS_SCHALTER.map((k, i) => [k, Boolean(maske & (1 << i))])))
test('Datenschutz-Absatz: Wortlaut entspricht der Markdown-Quelle, beide Anreden, alle 32 Bedingungskombinationen', () => {
for (const [fassung, anrede] of [['Sie', 'sie'], ['Du', 'du']]) {
for (const zustand of alleZustaende()) {
assert.equal(
normalisiere(datenschutzAbsatz(dsEinstellung(zustand), anrede)),
datenschutzQuelle(fassung, zustand),
`${fassung} ${JSON.stringify(zustand)}`,
)
}
}
})
test('Datenschutz-Absatz: Ausgabe ist schon normalisiert (Absätze durch Leerzeile, keine doppelten Leerzeichen, kein Platzhalterrest)', () => {
for (const anrede of ['sie', 'du']) {
for (const zustand of alleZustaende()) {
const text = datenschutzAbsatz(dsEinstellung(zustand), anrede)
assert.equal(text, normalisiere(text), `${anrede} ${JSON.stringify(zustand)}`)
assert.ok(!/ {2}| [,.;:]|\[\[|\]\]|\{\{|\}\}|undefined|null|NaN/.test(text), `${anrede} ${JSON.stringify(zustand)}`)
assert.ok(text.startsWith('Online-Terminbuchung\n\n'))
assert.equal(text.split('\n\n')[0], 'Online-Terminbuchung')
}
}
})
test('Datenschutz-Absatz: gleiche Quelle auch mit anderer Aufbewahrung (Pin mit 365 Tagen)', () => {
const einstellung = dsEinstellung(DS_ALLE_AN, { aufbewahrungTage: 365 })
assert.equal(normalisiere(datenschutzAbsatz(einstellung, 'sie')), datenschutzQuelle('Sie', DS_ALLE_AN, { tage: 365 }))
assert.equal(normalisiere(datenschutzAbsatz(einstellung, 'du')), datenschutzQuelle('Du', DS_ALLE_AN, { tage: 365 }))
})
test('Datenschutz-Absatz: jede Bedingung schaltet genau ihren Satz', () => {
for (const anrede of ['sie', 'du']) {
for (const schalter of DS_SCHALTER) {
const aus = datenschutzAbsatz(dsEinstellung({ ...DS_ALLE_AN, [schalter]: false }), anrede)
const an = datenschutzAbsatz(dsEinstellung({ ...DS_ALLE_AUS, [schalter]: true }), anrede)
for (const [name, muster] of Object.entries(DS_MARKER)) {
assert.equal(muster.test(aus), name !== schalter, `${anrede}: ${schalter} aus, Marker ${name}`)
assert.equal(muster.test(an), name === schalter, `${anrede}: nur ${schalter} an, Marker ${name}`)
}
}
}
})
test('Datenschutz-Absatz: die Bedingungen lesen die Einstellung wie das Widget', () => {
const alleAn = dsEinstellung(DS_ALLE_AN)
const text = (e, anrede = 'sie') => datenschutzAbsatz(e, anrede)
// Telefon-Feld: nur „aus“ blendet aus, optional und Pflicht zählen
assert.ok(DS_MARKER.telefonFeld.test(text({ ...alleAn, gastFelder: { telefon: 'pflicht', nachricht: 'aus', zusatz: [] } })))
assert.ok(!DS_MARKER.telefonFeld.test(text({ ...alleAn, gastFelder: { telefon: 'aus', nachricht: 'aus', zusatz: [] } })))
// Nachricht oder Zusatzfelder: eines von beiden genügt
const aus = { telefon: 'aus', nachricht: 'aus' }
assert.ok(!DS_MARKER.freitext.test(text({ ...alleAn, gastFelder: { ...aus, zusatz: [] } })))
assert.ok(DS_MARKER.freitext.test(text({ ...alleAn, gastFelder: { ...aus, nachricht: 'pflicht', zusatz: [] } })))
assert.ok(DS_MARKER.freitext.test(text({ ...alleAn, gastFelder: { ...aus, zusatz: [{ key: 'wunsch', label: 'Wunsch', typ: 'text', optionen: [], pflicht: false }] } })))
// Widerruf: genau die Bedingung des Widerrufsknopfs (Verbraucher-Gäste und mindestens eine sichtbare Leistung)
assert.ok(DS_MARKER.widerruf.test(text({ ...alleAn, recht: { gaesteSind: 'gemischt', freizeit: false } })))
assert.ok(!DS_MARKER.widerruf.test(text({ ...alleAn, recht: { gaesteSind: 'unternehmer', freizeit: false } })))
assert.ok(!DS_MARKER.widerruf.test(text({ ...alleAn, recht: { gaesteSind: 'verbraucher', freizeit: true } })))
assert.ok(!DS_MARKER.widerruf.test(text({ ...alleAn, leistungen: [{ id: 'l1', aktiv: false, ressourcen: ['r1'] }] })))
assert.ok(!DS_MARKER.widerruf.test(text({ ...alleAn, ressourcen: [{ id: 'r1', aktiv: false }] })))
// Telefon des Betriebs: Leerraum zählt als nicht hinterlegt
assert.ok(!DS_MARKER.telefon.test(text({ ...alleAn, betrieb: { ...BETRIEB, telefon: ' ' } })))
assert.ok(text({ ...alleAn, betrieb: { ...BETRIEB, telefon: ' 06301\n1234 ' } }).includes('telefonisch erreichen: 06301 1234.'))
})
test('Datenschutz-Absatz: Aufbewahrung nach dem Termin, ungültiger Wert wird zu 90 Tagen (wie der Formular-Hinweis)', () => {
for (const anrede of ['sie', 'du']) {
const mit = (aufbewahrungTage) => datenschutzAbsatz(dsEinstellung(DS_ALLE_AN, { aufbewahrungTage }), anrede)
assert.ok(mit(365).includes('Bestätigte Termine löschen wir 365 Tage nach dem Termin,'))
assert.ok(mit(14).includes('löschen wir 14 Tage nach dem Termin,'))
for (const ungueltig of ['viel', undefined, null, 14.5, NaN]) {
assert.ok(mit(ungueltig).includes('löschen wir 90 Tage nach dem Termin,'), `${anrede}: ${String(ungueltig)}`)
}
}
})
test('Datenschutz-Absatz: Sie-Fassung ohne Du-Wörter, Du-Fassung ohne Sie-Wörter', () => {
for (const zustand of [DS_ALLE_AN, DS_ALLE_AUS]) {
const sie = datenschutzAbsatz(dsEinstellung(zustand), 'sie')
const du = datenschutzAbsatz(dsEinstellung(zustand), 'du')
assert.ok(!DU_WOERTER.test(sie), 'Du-Form in der Sie-Fassung')
assert.ok(!SIE_WOERTER.test(du), 'Sie-Form in der Du-Fassung')
assert.ok(/Sie|Ihr/.test(sie) && /\bdu\b|\bdein/.test(du), 'Fassungen sind nicht zu unterscheiden')
assert.ok(sie.includes('Auf unserer Website können Sie Termine online buchen.'))
assert.ok(du.includes('Auf unserer Website kannst du Termine online buchen.'))
}
})
test('Datenschutz-Absatz: die Anrede kommt aus dem Parameter, alles außer „du“ ist die Sie-Fassung', () => {
const einstellung = dsEinstellung(DS_ALLE_AN, { anrede: 'du' })
assert.ok(datenschutzAbsatz(einstellung, 'sie').includes('können Sie Termine online buchen'))
assert.ok(datenschutzAbsatz(einstellung, 'du').includes('kannst du Termine online buchen'))
for (const anrede of [undefined, 'unbekannt', 'Du', null]) {
assert.equal(datenschutzAbsatz(einstellung, anrede), datenschutzAbsatz(einstellung, 'sie'), String(anrede))
}
})
test('Datenschutz-Absatz: Name und E-Mail sind Pflicht (TypeError), eine leere Anschrift lässt kein leeres Komma stehen', () => {
const ohne = (feld) => ({ ...dsEinstellung(DS_ALLE_AN), betrieb: { ...BETRIEB, [feld]: undefined } })
assert.throws(() => datenschutzAbsatz(ohne('name'), 'sie'), TypeError)
assert.throws(() => datenschutzAbsatz(ohne('email'), 'du'), TypeError)
assert.throws(() => datenschutzAbsatz({ ...dsEinstellung(DS_ALLE_AN), betrieb: { ...BETRIEB, name: ' \n ' } }, 'sie'), TypeError)
assert.throws(() => datenschutzAbsatz({ ...dsEinstellung(DS_ALLE_AN), betrieb: { ...BETRIEB, email: '' } }, 'sie'), TypeError)
assert.throws(() => datenschutzAbsatz({ ...dsEinstellung(DS_ALLE_AN), betrieb: undefined }, 'sie'), TypeError)
assert.throws(() => datenschutzAbsatz({}, 'sie'), TypeError)
assert.throws(() => datenschutzAbsatz(undefined, 'sie'), TypeError)
for (const adresse of ['', ' ', undefined]) {
for (const anrede of ['sie', 'du']) {
const text = datenschutzAbsatz({ ...dsEinstellung(DS_ALLE_AN), betrieb: { ...BETRIEB, adresse } }, anrede)
assert.ok(text.endsWith('Verantwortlich ist Salon Müller & Söhne, E-Mail info@salon-mueller.de.'), `${anrede}: ${String(adresse)}`)
assert.ok(!/, ,|,,|, E-Mail , /.test(text))
}
}
const voll = datenschutzAbsatz(dsEinstellung(DS_ALLE_AN), 'sie')
assert.ok(voll.endsWith('Verantwortlich ist Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, E-Mail info@salon-mueller.de.'))
})
test('Datenschutz-Absatz: reiner Text, nicht HTML-maskiert, Betriebsangaben unverändert eingesetzt (kein Ersetzungsmuster)', () => {
const betrieb = { name: "Frisör $& $1 <b>'Anna'</b> & Co", adresse: 'Weg 1,\n67722 Winnweiler', telefon: '06301 $` 1234', email: 'a&b@salon.de' }
const text = datenschutzAbsatz({ ...dsEinstellung(DS_ALLE_AN), betrieb }, 'sie')
assert.ok(text.includes("Absender ist Frisör $& $1 <b>'Anna'</b> & Co, Ihre Antworten"))
assert.ok(text.includes("Verantwortlich ist Frisör $& $1 <b>'Anna'</b> & Co, Weg 1, 67722 Winnweiler, E-Mail a&b@salon.de."))
assert.ok(text.includes('telefonisch erreichen: 06301 $` 1234.'))
assert.ok(!text.includes('&amp;') && !text.includes('&lt;'))
})
// ---------------------------------------------------------------- Dokumente
const DOKUMENTE = [
'docs/rechtliches/termine-widerrufsbelehrung-muster.md',
'docs/rechtliches/termine-datenschutz-absatz.md',
'docs/rechtliches/termine-hinweise.md',
'docs/rechtliches/termine-datenschutz-6c-entwurf.md',
]
test('Jedes Rechtstext-Dokument beginnt mit dem Entwurfs-Kopf', () => {
for (const rel of DOKUMENTE) {
assert.equal(lies(rel).split('\n')[0].trim(), KOPF, rel)
}
})
test('Hinweise-Dokument: Formular-Hinweis (Sie/Du) und alle Zustimmungstexte stehen wörtlich darin', () => {
const md = lies('docs/rechtliches/termine-hinweise.md').replace(/\r/g, '')
const platzhalter = { betrieb: { name: '{{Firma}}', adresse: '{{Anschrift}}', email: '{{E-Mail}}' }, aufbewahrungTage: 90 }
assert.ok(md.includes(formularHinweis({ ...platzhalter, anrede: 'sie' })), 'Formular-Hinweis (Sie) weicht ab')
assert.ok(md.includes(formularHinweis({ ...platzhalter, anrede: 'du' })), 'Formular-Hinweis (Du) weicht ab')
assert.ok(md.includes(ZUSTIMMUNG_VORZEITIG('{{Firma}}')))
assert.ok(md.includes(ZUSTIMMUNG_STORNO))
assert.ok(md.includes(ZUSTIMMUNG_ERINNERUNG('{{n}}')))
assert.ok(md.includes(GESUNDHEIT_HINWEIS))
for (const knopf of ['Zahlungspflichtig buchen', 'Termin buchen', 'Termin anfragen', 'Vertrag widerrufen', 'Widerruf bestätigen']) {
assert.ok(md.includes(knopf), `Knopftext fehlt: ${knopf}`)
}
})
test('Datenschutz-Absatz: Löschfristen der Vorgaben, Sie- und Du-Fassung, Auftragsverarbeiter und Gesundheitshinweis', () => {
const md = lies('docs/rechtliches/termine-datenschutz-absatz.md').replace(/\r/g, '')
assert.ok(/Fassung Sie/i.test(md) && /Fassung Du/i.test(md))
assert.ok(md.includes('Online-Terminbuchung'))
// jede Fassung nennt die vier Fristen: nicht bestätigt 48 Stunden, abgesagt 30 Tage, vergangen {{Tage}} Tage, Abschaltung 30 Tage
const fassungen = md.split(/^## Fassung /m).slice(1)
assert.equal(fassungen.length, 2)
for (const f of fassungen) {
assert.ok(f.includes('48 Stunden'), 'Frist nicht bestätigter Buchungen fehlt')
assert.ok(f.includes('{{Tage}} Tage nach dem Termin'), 'Aufbewahrung vergangener Termine fehlt')
assert.ok(/abgesagte[^.]*30 Tage/.test(f), 'Frist abgesagter Termine fehlt')
assert.ok(/abgeschaltet[^.]*30 Tage|30 Tage nach dem Abschalten/.test(f), 'Frist nach Abschaltung fehlt')
assert.ok(f.includes('WEBklar GbR') && f.includes('Art. 28 DSGVO') && f.includes('Hetzner'))
assert.ok(f.includes('Gesundheit'))
assert.ok(f.includes('Art. 6 Abs. 1 lit. b DSGVO') && f.includes('Art. 6 Abs. 1 lit. a DSGVO'))
}
assert.ok(fassungen[0].startsWith('Sie') && fassungen[1].startsWith('Du'))
assert.ok(!DU_WOERTER.test(fassungen[0]), 'Du-Form in der Sie-Fassung')
assert.ok(!SIE_WOERTER.test(fassungen[1]), 'Sie-Form in der Du-Fassung')
})
test('6c-Entwurf: nur als Entwurfsdatei, public/datenschutz.html bleibt unverändert (ohne Abschnitt 6c)', () => {
const md = lies('docs/rechtliches/termine-datenschutz-6c-entwurf.md')
assert.ok(md.includes('6c. Online-Terminbuchung'))
assert.ok(md.includes('Auftragsverarbeiter'))
assert.ok(/nicht (in|auf) .*(einbauen|veröffentlich)|nicht eingebaut|nicht veröffentlicht/i.test(md))
// TP2 baut 6c ein und entfernt diese Absicherung
const html = lies('public/datenschutz.html')
assert.ok(!/6c\./.test(html) && !html.includes('id="termine"'), '6c darf vor TP2 nicht in public/datenschutz.html stehen')
})