127 lines
8.1 KiB
JavaScript
127 lines
8.1 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createHash } from 'node:crypto'
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { PLATZHALTER_BETRIEB, archivPfad, baueArchiv, lauf } from '../scripts/termine-texte-archiv.mjs'
|
|
import {
|
|
GESUNDHEIT_HINWEIS,
|
|
RECHTSTEXTE_VERSION,
|
|
ZUSTIMMUNG_ERINNERUNG,
|
|
ZUSTIMMUNG_STORNO,
|
|
ZUSTIMMUNG_VORZEITIG,
|
|
widerrufsbelehrung,
|
|
} from '../server/services/termine/rechtstexte.js'
|
|
|
|
// Archiv der Gästetexte (AVV Terminplaner Nr. 8 Abs. 5): jede Fassung steht als Datei unter docs/rechtliches/archiv/, erzeugt von
|
|
// scripts/termine-texte-archiv.mjs. Die Datei einer früheren Fassung wird nie neu erzeugt; ihr Hash ist hier festgenagelt.
|
|
|
|
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
|
|
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
|
|
const ARCHIV_DIR = 'docs/rechtliches/archiv'
|
|
|
|
/** Temporäres Projektverzeichnis mit dem Archiv-Ordner; räumt nach dem Test auf. */
|
|
function mitTempRoot(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'termine-archiv-'))
|
|
mkdirSync(join(root, ARCHIV_DIR), { recursive: true })
|
|
t.after(() => rmSync(root, { recursive: true, force: true }))
|
|
return root
|
|
}
|
|
const leise = { log: () => {}, fehler: () => {} }
|
|
|
|
test('archiv_aktuelle_fassung: Skriptausgabe ist die eingecheckte Datei der aktuellen Fassung; Datei und README nennen die Fassung', () => {
|
|
assert.equal(archivPfad(), `${ARCHIV_DIR}/termine-gaestetexte-${RECHTSTEXTE_VERSION}.md`)
|
|
const datei = lies(archivPfad())
|
|
assert.equal(baueArchiv(), datei, 'Archivdatei weicht von rechtstexte.js ab: node scripts/termine-texte-archiv.mjs ausführen (nur bei neuer Fassung)')
|
|
assert.equal(baueArchiv(), baueArchiv(), 'deterministisch')
|
|
assert.ok(datei.startsWith(`# Gästetexte des Terminplaners, Fassung ${RECHTSTEXTE_VERSION}\n`))
|
|
const readme = lies(`${ARCHIV_DIR}/README.md`)
|
|
const zeile = readme.split('\n').find((z) => z.startsWith('|') && z.includes(RECHTSTEXTE_VERSION) && z.includes(`termine-gaestetexte-${RECHTSTEXTE_VERSION}.md`))
|
|
assert.ok(zeile, 'README-Tabelle „Terminplaner-Gästetexte“ nennt die Fassung nicht')
|
|
assert.ok(readme.includes('## Terminplaner-Gästetexte'))
|
|
})
|
|
|
|
test('archiv_aktuelle_fassung: --check vergleicht mit der eingecheckten Datei (Exit 0 / 1 / 2), ohne --check wird geschrieben', (t) => {
|
|
assert.equal(lauf(['--check'], leise), 0, 'die eingecheckte Datei stimmt')
|
|
const root = mitTempRoot(t)
|
|
const ziel = join(root, archivPfad())
|
|
assert.equal(lauf(['--check'], { ...leise, root }), 1, 'fehlende Datei')
|
|
assert.equal(lauf([], { ...leise, root }), 0)
|
|
assert.equal(readFileSync(ziel, 'utf8'), baueArchiv())
|
|
assert.equal(lauf(['--check'], { ...leise, root }), 0)
|
|
writeFileSync(ziel, readFileSync(ziel, 'utf8').replace('Gesundheit', 'Gesundheits'))
|
|
const zeilen = []
|
|
assert.equal(lauf(['--check'], { root, log: (z) => zeilen.push(z), fehler: () => {} }), 1, 'geänderte Datei')
|
|
assert.match(zeilen[0], /^ABWEICHT /)
|
|
assert.equal(readFileSync(ziel, 'utf8').includes('Gesundheits'), true, '--check schreibt nichts')
|
|
const meldungen = []
|
|
assert.equal(lauf(['--prüfen'], { ...leise, root, fehler: (m) => meldungen.push(m) }), 2)
|
|
assert.match(meldungen[0], /Aufruf: node scripts\/termine-texte-archiv\.mjs \[--check\]/)
|
|
})
|
|
|
|
test('archiv_inhalt: alle Gästetexte stehen wörtlich im Archiv, die Betriebsangaben als Platzhalter', () => {
|
|
const text = baueArchiv()
|
|
const zitat = (s) => s.split('\n').map((z) => (z === '' ? '>' : `> ${z}`)).join('\n')
|
|
// Formular-Hinweis, Sie und Du
|
|
assert.ok(text.includes('> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten'))
|
|
assert.ok(text.includes('> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten'))
|
|
assert.ok(text.includes('Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.'))
|
|
// Haken, Gesundheitshinweis, Belehrung
|
|
assert.ok(text.includes(zitat(ZUSTIMMUNG_VORZEITIG('{{Name des Betriebs}}'))))
|
|
assert.ok(text.includes(zitat(ZUSTIMMUNG_STORNO)))
|
|
assert.ok(text.includes(zitat(ZUSTIMMUNG_ERINNERUNG('{{Stunden}}'))))
|
|
assert.ok(text.includes(zitat(GESUNDHEIT_HINWEIS)))
|
|
assert.ok(text.includes(zitat(widerrufsbelehrung(PLATZHALTER_BETRIEB))))
|
|
assert.ok(text.includes('Telefon: {{Telefon}}, E-Mail: {{E-Mail}}'))
|
|
// Knopftexte
|
|
for (const zeile of [
|
|
'| Sofort buchbar, ohne Preis | Termin buchen | Termin bestätigen |',
|
|
'| Sofort buchbar, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |',
|
|
'| Terminanfrage | Termin anfragen | Anfrage absenden |',
|
|
]) {
|
|
assert.ok(text.includes(`${zeile}\n`), `Knopftext-Zeile fehlt: ${zeile}`)
|
|
}
|
|
// Datenschutz-Absatz der Website, mit den bedingten Sätzen
|
|
assert.ok(text.includes('> Online-Terminbuchung'))
|
|
assert.ok(text.includes('Auf unserer Website können Sie Termine online buchen.'))
|
|
assert.ok(text.includes('Auf unserer Website kannst du Termine online buchen.'))
|
|
assert.ok(text.includes('Vertrag widerrufen'))
|
|
assert.ok(text.includes('Wenn Sie eine Erinnerung vor dem Termin wünschen'))
|
|
// Fassung 2026-10-05: Widerrufssatz am Erinnerungshaken und Abo-Ende-Satz im Datenschutz-Absatz (Sie und Du)
|
|
assert.ok(text.includes('> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen.\n'))
|
|
assert.equal(text.split('zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht.').length, 3, 'Abo-Ende-Satz in Sie- und Du-Fassung')
|
|
assert.ok(!text.includes('löschen wir alle Termindaten 30 Tage'))
|
|
// Nachweis-Hinweis (⚖ Q26)
|
|
assert.ok(text.includes('Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).'))
|
|
// kein Rest, keine echten Betriebsdaten
|
|
assert.ok(!/undefined|\bnull\b|NaN|\[object/.test(text))
|
|
assert.ok(!text.includes('\r'))
|
|
assert.ok(text.endsWith('\n') && !text.endsWith('\n\n'))
|
|
})
|
|
|
|
|
|
// SHA-256 der eingefrorenen Datei der Fassung 2026-10-02 (aus den unveränderten Texten erzeugt, vor jeder Textänderung eingecheckt)
|
|
const HASH_2026_10_02 = '920633e2e23c9c98aded0c1509db5cac1465058a025aeddc76a9a1282041a6a9'
|
|
|
|
// 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' }
|
|
|
|
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}`)
|
|
const datei = lies(archivPfad())
|
|
assert.equal(sha256(datei), HASH_AKTUELL[RECHTSTEXTE_VERSION], 'Gästetext geändert, aber die Fassung ist dieselbe: neues Datum, neue Archivdatei und neuer Pin nötig')
|
|
assert.equal(sha256(baueArchiv()), HASH_AKTUELL[RECHTSTEXTE_VERSION])
|
|
assert.notEqual(HASH_AKTUELL[RECHTSTEXTE_VERSION], HASH_2026_10_02, 'die neue Fassung unterscheidet sich von der eingefrorenen')
|
|
})
|
|
|
|
test('archiv_altfassung_eingefroren: Datei 2026-10-02 hat den alten Erinnerungstext ohne den Widerrufssatz; ihr Hash ist festgenagelt', () => {
|
|
const alt = lies(`${ARCHIV_DIR}/termine-gaestetexte-2026-10-02.md`)
|
|
assert.ok(alt.startsWith('# Gästetexte des Terminplaners, Fassung 2026-10-02\n'))
|
|
assert.ok(alt.includes('> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.\n'), 'alter Erinnerungstext')
|
|
assert.ok(!alt.includes('Diese Einwilligung kann ich jederzeit widerrufen.'), 'der Widerrufssatz gehört erst zur nächsten Fassung')
|
|
assert.ok(alt.includes('Wird die Online-Terminbuchung abgeschaltet, löschen wir alle Termindaten 30 Tage nach dem Abschalten.'), 'alter Datenschutz-Absatz')
|
|
assert.equal(sha256(alt), HASH_2026_10_02, 'Die Datei der Fassung 2026-10-02 darf nie verändert werden; Textänderungen bekommen eine neue Fassung')
|
|
})
|