Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:23:21 +00:00
1 parent 4553bc8e92
commit b25689a015
21 files changed
+1456 -11

No files matched your search

+94 -4
View File
@@ -16,7 +16,7 @@ useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1'
const { config } = await import('../server/config.js')
const { startTestApp } = await import('./support/testApp.js')
const { legalPaths, legalPdfFiles, legalPdfAttachments, legalRouter } = await import('../server/services/legalDocs.js')
const { render, parse, lauf, termineDokumente, shopDokumente, MarkdownFehler } = await import('../scripts/build-legal-html.mjs')
const { render, parse, lauf, termineDokumente, shopDokumente, unterkunftDokumente, MarkdownFehler } = await import('../scripts/build-legal-html.mjs')
const V = config.termine.avvVersion
const ROOT = fileURLToPath(new URL('..', import.meta.url))
@@ -24,6 +24,8 @@ const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8')
const DOKUMENTE = termineDokumente(V)
const S = config.shop.fassung
const SHOP = shopDokumente(S)
const U = config.unterkunft.fassung
const UK = unterkunftDokumente(U)
const META = { fassung: '2026-10-05', titel: 'Prüfdokument', seitenkopf: 'Prüfdokument', nav: [{ href: '/dashboard.html', text: 'Portal' }] }
const KOPF = '# Titel\n\n**Fassung 2026-10-05**\n\n'
@@ -359,7 +361,7 @@ test('script_modus: build-legal-pdfs.sh kennt termine und datenschutz, lässt di
// bash versteht das Skript, und ein unbekannter Modus liefert die Usage mit allen Modi
const syntax = spawnSync('bash', ['-n', 'scripts/build-legal-pdfs.sh'], { cwd: ROOT, encoding: 'utf8' })
assert.equal(syntax.status, 0, syntax.stderr)
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|datenschutz]'))
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|unterkunft|datenschutz]'))
})
// ---------------------------------------------------------------------------------------------------- Webshop (Task J1)
@@ -438,7 +440,7 @@ test('shop_lauf: --check meldet fehlende oder abweichende Seiten mit Exit 1, sch
assert.equal(ruf('analyse'), 2)
assert.equal(ruf('shop', '--schreiben'), 2)
assert.equal(ruf(), 2)
assert.match(zeilen[0], /Aufruf: node scripts\/build-legal-html\.mjs termine\|shop \[--check\]/)
assert.match(zeilen[0], /Aufruf: node scripts\/build-legal-html\.mjs termine\|shop\|unterkunft \[--check\]/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
@@ -480,7 +482,95 @@ test('shop_script_modus: build-legal-pdfs.sh kennt den Modus shop (Slugs der Reg
assert.ok(shop.includes('sha256sum "$quelle"') && shop.includes('echo "$summe" > "$ziel.sha256"') && shop.includes('(unveraendert)'))
assert.match(sh, /^shop\)\n {2}shop_pdfs\n {2};;$/m)
assert.ok(sh.slice(sh.indexOf('\nall)'), sh.indexOf('\n*)')).includes(' shop_pdfs\n'), 'all druckt auch die Webshop-PDFs')
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|datenschutz]'))
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|unterkunft|datenschutz]'))
const syntax = spawnSync('bash', ['-n', 'scripts/build-legal-pdfs.sh'], { cwd: ROOT, encoding: 'utf8' })
assert.equal(syntax.status, 0, syntax.stderr)
})
// ---------------------------------------------------------------------------------------------------- Buchungskalender (Task 21)
test('unterkunftDokumente: lb, avv und widerruf mit Quelle, Ziel und Navigation auf die Fassung des Buchungskalenders', () => {
assert.deepEqual(UK.map((d) => d.doc), ['lb', 'avv', 'widerruf'])
assert.deepEqual(
UK.map((d) => [d.md, d.html]),
[
['docs/rechtliches/leistungsbeschreibung-unterkunft.md', `public/rechtliches/leistungsbeschreibung-unterkunft-${U}.html`],
['docs/rechtliches/avv-unterkunft.md', `public/rechtliches/avv-unterkunft-${U}.html`],
['docs/rechtliches/widerrufsbelehrung-funktionen.md', `public/rechtliches/widerrufsbelehrung-funktionen-${U}.html`],
],
)
assert.deepEqual(Object.values(legalPaths('unterkunft', U)), UK.map((d) => d.html.replace(/^public/, '')))
assert.deepEqual(unterkunftDokumente(), UK)
// nur die Widerrufsbelehrung trägt eine fremde Fassung in der Quelle (die des Terminplaners)
assert.deepEqual(UK.map((d) => d.stempel), [undefined, undefined, config.termine.avvVersion])
})
test('unterkunft_html_gleich_generator: eingecheckte Seiten sind genau render(Markdown), Entwurfskennzeichen in LB und AVV, --check Exit 0', () => {
for (const d of UK) {
const md = d.stempel ? lies(d.md).replace(`**Fassung ${d.stempel}**`, `**Fassung ${U}**`) : lies(d.md)
const html = render(md, d.meta)
assert.equal(lies(d.html), html, `${d.html} weicht vom Generator ab (node scripts/build-legal-html.mjs unterkunft)`)
assert.equal(render(md, d.meta), html, 'deterministisch')
assert.ok(html.includes(`<strong>Fassung ${U}</strong>`))
assert.ok(html.includes(`<meta name="webklar-fassung" content="${U}">`))
assert.doesNotMatch(html.replaceAll(U, ''), /\b\d{4}-\d{2}-\d{2}\b/, `${d.html}: Datum außer der Fassung`)
assert.doesNotMatch(html, /<!--|<script|<iframe|<img|<form|<link|\sstyle=|\ssrc=|\{\{|\}\}|undefined|NaN/, `${d.html}: verbotener Inhalt`)
if (d.doc === 'lb' || d.doc === 'avv') assert.ok(html.includes('Entwurf – Prüfung durch Kanzlei offen'), `${d.doc}: Kennzeichnung fehlt`)
else assert.ok(!html.includes('Entwurf'), 'die Widerrufsbelehrung ist kein Entwurf')
}
const lauf = spawnSync(process.execPath, ['--max-old-space-size=512', 'scripts/build-legal-html.mjs', 'unterkunft', '--check'], { cwd: ROOT, encoding: 'utf8', timeout: 60000 })
assert.equal(lauf.status, 0, lauf.stdout + lauf.stderr)
assert.equal([...lauf.stdout.matchAll(/^ok /gm)].length, 3)
})
test('unterkunft_lauf: --check meldet fehlende Seiten mit Exit 1, schreibt drei Seiten; die Fassung der Quelle der Widerrufsbelehrung wird ersetzt', () => {
const dir = mkdtempSync(join(tmpdir(), 'legalhtml-uk-'))
try {
mkdirSync(join(dir, 'docs/rechtliches'), { recursive: true })
mkdirSync(join(dir, 'public/rechtliches'), { recursive: true })
for (const d of UK) cpSync(join(ROOT, d.md), join(dir, d.md))
const zeilen = []
const ruf = (...args) => lauf(args, { root: dir, log: (z) => zeilen.push(z), fehler: (z) => zeilen.push(z) })
assert.equal(ruf('unterkunft', '--check'), 1)
assert.equal(zeilen.filter((z) => z.startsWith('ABWEICHT')).length, 3)
assert.equal(existsSync(join(dir, UK[0].html)), false, '--check schreibt nichts')
assert.equal(ruf('unterkunft'), 0)
assert.equal(ruf('unterkunft', '--check'), 0)
for (const d of UK) assert.equal(readFileSync(join(dir, d.html), 'utf8'), lies(d.html), 'dieselben Bytes wie die eingecheckte Seite')
// Fassung der Quelle fehlt: laut abbrechen statt eine Seite mit falscher Fassung zu schreiben
writeFileSync(join(dir, UK[2].md), lies(UK[2].md).replace(`**Fassung ${UK[2].stempel}**`, '**Fassung ohne**'))
assert.throws(() => ruf('unterkunft'), MarkdownFehler)
assert.equal(ruf('shop', '--schreiben'), 2)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('unterkunft_pdf_dateien: je Dokument ein PDF mit passender Prüfsumme der HTML-Seite', () => {
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
const dateien = legalPdfFiles('unterkunft', U)
assert.deepEqual(Object.keys(dateien), ['lb', 'avv', 'widerruf'])
for (const d of UK) {
const f = dateien[d.doc]
assert.equal(f.path, join(ROOT, d.html.replace(/\.html$/, '.pdf')))
const bytes = readFileSync(f.path)
assert.equal(bytes.subarray(0, 5).toString('latin1'), '%PDF-', `${d.doc}: kein PDF`)
assert.ok(bytes.length > 20 * 1024, `${d.doc}: PDF mit ${bytes.length} Bytes ist zu klein`)
assert.ok(bytes.subarray(-1024).toString('latin1').includes('%%EOF'), `${d.doc}: PDF ist abgeschnitten`)
const summe = lies(d.html.replace(/\.html$/, '.pdf.sha256'))
assert.match(summe, /^[0-9a-f]{64}\n$/)
assert.equal(summe.trim(), sha256(readFileSync(join(ROOT, d.html))), `${d.html} wurde geändert, ohne das PDF neu zu drucken (scripts/build-legal-pdfs.sh unterkunft)`)
}
})
test('unterkunft_script_modus: build-legal-pdfs.sh kennt den Modus unterkunft (Slugs der Registry, Fassung aus config.unterkunft.fassung), Usage nennt ihn', () => {
const sh = lies('scripts/build-legal-pdfs.sh')
assert.match(sh, /^unterkunft\)$/m)
assert.ok(sh.includes('config.unterkunft.fassung') && sh.includes('UNTERKUNFT_VERSION'))
const modus = sh.slice(sh.indexOf('\nunterkunft)'), sh.indexOf('\ndatenschutz)'))
for (const d of UK) {
const slug = /\/([a-z-]+)-\d{4}-\d{2}-\d{2}\.html$/.exec(d.html)[1]
assert.ok(modus.includes(slug), `Modus unterkunft baut ${slug}`)
}
assert.ok(modus.includes('sha256sum "$quelle"') && modus.includes('echo "$summe" > "$ziel.sha256"') && modus.includes('(unveraendert)'))
})