import { test, after } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Router } from 'express' import { useTestEnv } from './support/testEnv.js' 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, featureLegalAttachments, featureLegalReady, legalRouter } = await import( '../server/services/legalDocs.js' ) const analyse = await import('../server/services/analyseLegal.js') const { featureConfig } = await import('../server/services/featureRegistry.js') const V = config.termine.avvVersion const ALT = '2026-10-02' // eingefrorene Altfassung der Gaestetexte und der Analyse-Rechtstexte const root = (rel) => new URL(`../${rel}`, import.meta.url) // Router mit eigenem Verzeichnis: die Adressen ohne Fassung liefern die Datei der Fassung aus config const dir = mkdtempSync(join(tmpdir(), 'legaldocs-')) const inhalt = (name, version) => `

${name} ${version}

` const DATEIEN = ['leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'widerrufsbelehrung-funktionen'] for (const slug of DATEIEN) writeFileSync(join(dir, `${slug}-${V}.html`), inhalt(slug, V)) writeFileSync(join(dir, `avv-website-analyse-${ALT}.html`), inhalt('avv-website-analyse', ALT)) const rechtliches = Router() rechtliches.use(legalRouter({ dir })) const app = await startTestApp({ '/rechtliches': rechtliches }) after(() => { app.close() rmSync(dir, { recursive: true, force: true }) }) const get = async (path) => { const r = await fetch(app.url + path) return { status: r.status, text: await r.text() } } /** Konstante und Register fuer einen Test setzen; stellt sie danach wieder her. */ async function mitFassung(fassung, fn) { const alt = config.termine.avvVersion config.termine.avvVersion = fassung try { return await fn() } finally { config.termine.avvVersion = alt } } test('legalPaths("termine", V) nennt drei versionierte Adressen', () => { assert.deepEqual(legalPaths('termine', V), { lb: `/rechtliches/leistungsbeschreibung-terminplaner-${V}.html`, avv: `/rechtliches/avv-terminplaner-${V}.html`, widerruf: `/rechtliches/widerrufsbelehrung-funktionen-${V}.html`, }) assert.deepEqual(Object.keys(legalPaths('termine', V)), ['lb', 'avv', 'widerruf']) assert.deepEqual(legalPaths('analyse', ALT), { avv: `/rechtliches/avv-website-analyse-${ALT}.html`, lb: `/rechtliches/leistungsbeschreibung-website-analyse-${ALT}.html`, }) }) test('legalPaths: ungültige Fassung und unbekannter Satz werfen', () => { for (const satz of ['termine', 'analyse']) { for (const v of ['../x', '2026-10-5', '2026-10-05x', 'ENTWURF', '', undefined, null, '2026-10-05/../x']) { assert.throws(() => legalPaths(satz, v), /Fassung/, `${satz}: ${String(v)}`) assert.throws(() => legalPdfFiles(satz, v), /Fassung/) } } for (const satz of ['news', 'unbekannt', '__proto__', 'toString', '', undefined]) { assert.throws(() => legalPaths(satz, V), /Satz/, String(satz)) assert.throws(() => legalPdfFiles(satz, V), /Satz/) } }) test('legalPdfFiles: Dateinamen und Pfade je Satz und Fassung', () => { const t = legalPdfFiles('termine', V) assert.deepEqual(Object.keys(t), ['lb', 'avv', 'widerruf']) assert.equal(t.lb.filename, `Leistungsbeschreibung-Terminplaner-${V}.pdf`) assert.equal(t.avv.filename, `AVV-Terminplaner-${V}.pdf`) assert.equal(t.widerruf.filename, `Widerrufsbelehrung-Muster-Widerrufsformular-${V}.pdf`) assert.equal(t.lb.path, new URL(`../public/rechtliches/leistungsbeschreibung-terminplaner-${V}.pdf`, import.meta.url).pathname) assert.equal(t.avv.path, new URL(`../public/rechtliches/avv-terminplaner-${V}.pdf`, import.meta.url).pathname) assert.equal(t.widerruf.path, new URL(`../public/rechtliches/widerrufsbelehrung-funktionen-${V}.pdf`, import.meta.url).pathname) const a = legalPdfFiles('analyse', ALT) assert.deepEqual(Object.keys(a), ['avv', 'lb']) assert.equal(a.avv.filename, `AVV-Website-Analyse-${ALT}.pdf`) assert.equal(a.lb.filename, `Leistungsbeschreibung-Website-Analyse-${ALT}.pdf`) }) test('legalPdfAttachments: fehlende Datei wirft, Analyse liefert avv und lb, ohne lässt Dokumente weg', async () => { await assert.rejects(legalPdfAttachments('termine', '1999-01-01')) await assert.rejects(legalPdfAttachments('analyse', '1999-01-01')) await assert.rejects(legalPdfAttachments('unbekannt', V), /Satz/) const alle = await legalPdfAttachments('analyse', ALT) assert.deepEqual(alle.map((x) => x.filename), [`AVV-Website-Analyse-${ALT}.pdf`, `Leistungsbeschreibung-Website-Analyse-${ALT}.pdf`]) for (const x of alle) { assert.equal(x.contentType, 'application/pdf') assert.equal(x.content.subarray(0, 4).toString(), '%PDF') } const ohneAvv = await legalPdfAttachments('analyse', ALT, { ohne: ['avv'] }) assert.deepEqual(ohneAvv.map((x) => x.filename), [`Leistungsbeschreibung-Website-Analyse-${ALT}.pdf`]) // eine ausgelassene Datei muss nicht vorhanden sein assert.deepEqual(await legalPdfAttachments('termine', '1999-01-01', { ohne: ['lb', 'avv', 'widerruf'] }), []) await assert.rejects(legalPdfAttachments('analyse', ALT, { ohne: ['gibtsnicht'] }), /Dokument/) }) test('featureLegalAttachments / featureLegalReady: news ohne Dokumente, unbekannte Funktion nie bereit', async () => { assert.deepEqual(await featureLegalAttachments('news'), []) assert.deepEqual(await featureLegalAttachments('news', { ohne: ['lb'] }), []) assert.equal(await featureLegalReady('news'), true) await assert.rejects(featureLegalAttachments('gibtsnicht'), /Funktion/) await assert.rejects(featureLegalAttachments('analyse'), /Funktion/) await assert.rejects(featureLegalAttachments('__proto__'), /Funktion/) assert.equal(await featureLegalReady('gibtsnicht'), false) assert.equal(await featureLegalReady('analyse'), false) }) test('featureLegalAttachments("termine") liest die Fassung aus config.termine.avvVersion; fehlt eine Datei, ist die Funktion nicht bereit', async () => { await mitFassung('1999-01-01', async () => { await assert.rejects(featureLegalAttachments('termine')) assert.equal(await featureLegalReady('termine'), false) }) }) test('config.termine.avvVersion: Datumsmuster, nach der Altfassung der Gästetexte, echter Tag', () => { assert.match(config.termine.avvVersion, /^\d{4}-\d{2}-\d{2}$/) assert.ok(config.termine.avvVersion > ALT, `${config.termine.avvVersion} liegt nicht nach ${ALT}`) assert.ok(config.termine.avvVersion >= '2026-10-03') assert.ok(!Number.isNaN(Date.parse(`${config.termine.avvVersion}T00:00:00Z`))) assert.equal(new Date(`${config.termine.avvVersion}T00:00:00Z`).toISOString().slice(0, 10), config.termine.avvVersion) }) test('config.ustHinweis bleibt wörtlich', () => { assert.equal(config.ustHinweis, 'ohne Umsatzsteuer gemäß § 19 UStG') }) test('Register: leistungsbeschreibung und avv des Terminplaners folgen der Konstante (Getter, Drift-Wächter)', async () => { const t = featureConfig('termine') assert.deepEqual(t.leistungsbeschreibung, { version: V, url: legalPaths('termine', V).lb }) assert.deepEqual(t.avv, { version: V, url: legalPaths('termine', V).avv }) assert.doesNotMatch(JSON.stringify([t.leistungsbeschreibung, t.avv]), /ENTWURF/) // zur Laufzeit geaendert: der Getter folgt await mitFassung('2099-01-02', async () => { assert.deepEqual(t.leistungsbeschreibung, { version: '2099-01-02', url: legalPaths('termine', '2099-01-02').lb }) assert.deepEqual(t.avv, { version: '2099-01-02', url: legalPaths('termine', '2099-01-02').avv }) }) assert.deepEqual(t.leistungsbeschreibung, { version: V, url: legalPaths('termine', V).lb }) // ein Test darf den Eintrag ueberschreiben (siehe termineFragebogen.test.js) const merke = t.leistungsbeschreibung t.leistungsbeschreibung = { version: 'T1', url: '/x' } assert.deepEqual(t.leistungsbeschreibung, { version: 'T1', url: '/x' }) t.leistungsbeschreibung = merke assert.deepEqual(t.leistungsbeschreibung, merke) }) test('News: Leistungsbeschreibung folgt der Fassung (Adresse unverändert, kein AVV, kein Getter am Eintrag)', () => { const n = featureConfig('news') assert.deepEqual(n.leistungsbeschreibung, { version: config.termine.avvVersion, url: '/vertragsunterlagen/funktionen-leistungsbeschreibung.html' }) assert.equal(n.avv, null) assert.deepEqual(analyse.legalPaths(ALT), legalPaths('analyse', ALT)) assert.equal(Object.getOwnPropertyDescriptor(n, 'leistungsbeschreibung').get, undefined) }) test('config.js importiert legalDocs.js nicht (Adresse aus Slug und Version gebaut)', () => { const src = readFileSync(root('server/config.js'), 'utf8') assert.doesNotMatch(src, /^\s*(import\b[^\n]*|.*\bimport\()['"`][^'"`]*legalDocs/m) assert.doesNotMatch(src, /^import .* from '\.\/services\//m) }) test('analyseLegal.js ist ein dünner Wrapper um die Registry', async () => { assert.deepEqual(analyse.legalPaths(ALT), legalPaths('analyse', ALT)) assert.deepEqual(analyse.legalPdfFiles(ALT), legalPdfFiles('analyse', ALT)) assert.deepEqual(await analyse.legalPdfAttachments(ALT), await legalPdfAttachments('analyse', ALT)) assert.throws(() => analyse.legalPaths('../x'), /Fassung/) await assert.rejects(analyse.legalPdfAttachments('1999-01-01')) assert.equal(typeof analyse.analyseLegalRouter, 'function') }) test('Router: Adressen ohne Fassung liefern die Fassung aus config (Terminplaner)', async () => { for (const slug of DATEIEN) { const r = await get(`/rechtliches/${slug}.html`) assert.equal(r.status, 200, slug) assert.equal(r.text, inhalt(slug, V)) } // die Fassung folgt der Konstante zur Laufzeit: fuer eine andere Fassung gibt es keine Datei => 404 await mitFassung('2099-01-02', async () => { assert.equal((await get('/rechtliches/avv-terminplaner.html')).status, 404) }) assert.equal((await get('/rechtliches/avv-terminplaner.html')).status, 200) }) test('Router: Adressen ohne Fassung der Analyse liefern config.analyse.avvVersion', async () => { assert.equal(config.analyse.avvVersion, ALT) const r = await get('/rechtliches/avv-website-analyse.html') assert.equal(r.status, 200) assert.equal(r.text, inhalt('avv-website-analyse', ALT)) // die zweite Analyse-Datei fehlt im Testverzeichnis: 404, kein 500 assert.equal((await get('/rechtliches/leistungsbeschreibung-website-analyse.html')).status, 404) }) test('Router: nur die festen Adressen, versionierte und fremde Pfade fallen durch', async () => { for (const p of [ `/rechtliches/avv-terminplaner-${V}.html`, // versionierte Adressen liefert express.static in index.js '/rechtliches/avv-terminplaner.pdf', '/rechtliches/../config.js', '/rechtliches/gibtsnicht.html', '/rechtliches/', ]) { assert.equal((await get(p)).status, 404, p) } const post = await fetch(`${app.url}/rechtliches/avv-terminplaner.html`, { method: 'POST' }) assert.equal(post.status, 404) }) test('Router: legalRouter() ohne Verzeichnis liefert die Analyse-Adressen aus public/rechtliches', async () => { const echt = Router() echt.use(legalRouter()) const a = await startTestApp({ '/rechtliches': echt }) try { const r = await fetch(`${a.url}/rechtliches/avv-website-analyse.html`) assert.equal(r.status, 200) assert.deepEqual(Buffer.from(await r.arrayBuffer()), readFileSync(root(`public/rechtliches/avv-website-analyse-${ALT}.html`))) } finally { a.close() } }) test('server/index.js hängt den gemeinsamen Router vor express.static ein', () => { const index = readFileSync(root('server/index.js'), 'utf8') assert.match(index, /import \{ legalRouter \} from '\.\/services\/legalDocs\.js'/) assert.ok(index.includes("app.use('/rechtliches', legalRouter())")) assert.ok(!index.includes('analyseLegalRouter')) assert.ok(index.indexOf("app.use('/rechtliches', legalRouter())") < index.indexOf('app.use(express.static(publicDir))')) })