diff --git a/server/config.js b/server/config.js index 3a16081..138887d 100644 --- a/server/config.js +++ b/server/config.js @@ -145,6 +145,25 @@ const ustHinweis = 'ohne Umsatzsteuer gemäß § 19 UStG' const ticketProjektName = (project) => project?.projectName || project?.subdomain || '' +/** + * Rechtsunterlage des Terminplaners mit Fassung und Adresse aus config.termine.avvVersion (bei jedem Zugriff gelesen). + * Tests duerfen sie ueberschreiben (Zuweisung); wird der aktuelle Standardwert zurueckgeschrieben (Wiederherstellen nach + * dem Test) oder null/undefined zugewiesen, gilt wieder der Standard und die Unterlage folgt der Konstante. + */ +function termineRechtsunterlage(slug) { + let ueberschrieben = null + const standard = () => ({ version: config.termine.avvVersion, url: `/rechtliches/${slug}-${config.termine.avvVersion}.html` }) + return { + get: () => ueberschrieben ?? standard(), + set(wert) { + const std = standard() + ueberschrieben = wert == null || (wert.version === std.version && wert.url === std.url) ? null : wert + }, + } +} +const termineLb = termineRechtsunterlage('leistungsbeschreibung-terminplaner') +const termineAvv = termineRechtsunterlage('avv-terminplaner') + /** * Register der kaufbaren Funktionen (News-Pinnwand, Terminplaner): Preis, Zustimmungen, Rechtsunterlagen, * Ticket- und Mail-Texte je Funktion; reine Daten, config.js importiert keine Dienste. Die Helfer (featureConfig, @@ -234,8 +253,20 @@ const features = { }, ], }, - leistungsbeschreibung: { version: 'ENTWURF', url: '' }, - avv: { version: 'ENTWURF', url: '' }, + // Fassung und Adresse folgen config.termine.avvVersion bei jedem Zugriff (Slugs wie in services/legalDocs.js; config.js + // importiert keine Dienste, test/legalDocs.test.js gleicht beide ab) + get leistungsbeschreibung() { + return termineLb.get() + }, + set leistungsbeschreibung(wert) { + termineLb.set(wert) + }, + get avv() { + return termineAvv.get() + }, + set avv(wert) { + termineAvv.set(wert) + }, widerruf: null, returnHash: 'termine', returnProjekt: true, @@ -527,6 +558,10 @@ export const config = { .split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean), + // Fassung der Terminplaner-Bestellunterlagen (Leistungsbeschreibung, AVV, Widerrufsbelehrung, Bestelltexte, News- + // Zustimmung): Berliner Kalendertag des Fassungs-Commits (JJJJ-MM-TT). Eine ausgelieferte Fassung wird nie + // ueberschrieben: nach Uebergabe an die Kanzlei oder Livegang heisst jede Textaenderung neues Datum und neue Dateien. + avvVersion: '2026-10-05', holdMinutes: 30, zelleMin: 5, mailsProStunde: 200, diff --git a/server/index.js b/server/index.js index f1582fc..cdfa158 100644 --- a/server/index.js +++ b/server/index.js @@ -38,7 +38,7 @@ import newsRoutes from './routes/news.js' import publicNewsRoutes from './routes/publicNews.js' import publicTermineRoutes from './routes/publicTermine.js' import adminAnalyseRoutes from './routes/admin/analyse.js' -import { analyseLegalRouter } from './services/analyseLegal.js' +import { legalRouter } from './services/legalDocs.js' import adminFinanzenRoutes from './routes/admin/finanzen.js' import teamRoutes from './routes/team/index.js' import legacyAdminRedirect from './routes/legacyAdminRedirect.js' @@ -110,8 +110,8 @@ app.use('/embed/', (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*') next() }) -// Adressen ohne Fassung: aktuelle geprüfte Fassung der Analyse-Rechtstexte (vor express.static) -app.use('/rechtliches', analyseLegalRouter()) +// Adressen ohne Fassung: aktuelle Fassung der Rechtstexte (Analyse und Terminplaner; vor express.static) +app.use('/rechtliches', legalRouter()) // Abgeschaltetes Admin-Panel und Rechnungsanalyse: Links aus alten Team-Mails ins Ticketsystem app.use('/admin.html', legacyAdminRedirect.admin) app.use('/kalkulation.html', legacyAdminRedirect.kalkulation) diff --git a/server/services/analyseLegal.js b/server/services/analyseLegal.js index ee23143..22fe836 100644 --- a/server/services/analyseLegal.js +++ b/server/services/analyseLegal.js @@ -1,54 +1,13 @@ -// Veroeffentlichte Rechtstexte der Website-Analyse (je Fassung feste Dateien unter public/rechtliches/) -import { readFile } from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { join } from 'node:path' -import { Router } from 'express' -import { config } from '../config.js' +// Veroeffentlichte Rechtstexte der Website-Analyse (je Fassung feste Dateien unter public/rechtliches/): +// duenner Wrapper um die Dokument-Registry legalDocs.js (Satz 'analyse') +import { legalPaths as registryPaths, legalPdfAttachments as registryAttachments, legalPdfFiles as registryFiles, legalRouter } from './legalDocs.js' -const publicDir = fileURLToPath(new URL('../../public/', import.meta.url)) -const VERSION_RE = /^\d{4}-\d{2}-\d{2}$/ +export const legalPaths = (version) => registryPaths('analyse', version) -function checkVersion(version) { - if (!VERSION_RE.test(String(version))) throw new Error('Analyse: ungültige Fassung') - return String(version) -} - -export function legalPaths(version) { - const v = checkVersion(version) - return { - avv: `/rechtliches/avv-website-analyse-${v}.html`, - lb: `/rechtliches/leistungsbeschreibung-website-analyse-${v}.html`, - } -} - -export function legalPdfFiles(version) { - const v = checkVersion(version) - return { - avv: { filename: `AVV-Website-Analyse-${v}.pdf`, path: join(publicDir, 'rechtliches', `avv-website-analyse-${v}.pdf`) }, - lb: { filename: `Leistungsbeschreibung-Website-Analyse-${v}.pdf`, path: join(publicDir, 'rechtliches', `leistungsbeschreibung-website-analyse-${v}.pdf`) }, - } -} +export const legalPdfFiles = (version) => registryFiles('analyse', version) /** Beide PDFs als Mail-Anhaenge; wirft, wenn eine Datei fehlt oder nicht lesbar ist. */ -export async function legalPdfAttachments(version) { - const files = legalPdfFiles(version) - return Promise.all( - [files.avv, files.lb].map(async (f) => ({ - filename: f.filename, - content: await readFile(f.path), - contentType: 'application/pdf', - })) - ) -} +export const legalPdfAttachments = (version) => registryAttachments('analyse', version) /** Adressen ohne Fassung liefern immer die aktuelle geprüfte Fassung (config.analyse.avvVersion). */ -export function analyseLegalRouter() { - const router = Router() - const send = (key) => (_req, res, next) => { - const file = join(publicDir, legalPaths(config.analyse.avvVersion)[key]) - res.sendFile(file, (err) => err && next(err.code === 'ENOENT' ? undefined : err)) - } - router.get('/avv-website-analyse.html', send('avv')) - router.get('/leistungsbeschreibung-website-analyse.html', send('lb')) - return router -} +export const analyseLegalRouter = () => legalRouter({ saetze: ['analyse'] }) diff --git a/server/services/legalDocs.js b/server/services/legalDocs.js new file mode 100644 index 0000000..e7a7642 --- /dev/null +++ b/server/services/legalDocs.js @@ -0,0 +1,127 @@ +// Dokument-Registry der veroeffentlichten Rechtstexte (je Fassung feste Dateien unter public/rechtliches/): +// Saetze 'analyse' (AVV, Leistungsbeschreibung der Website-Analyse) und 'termine' (Leistungsbeschreibung, AVV und +// Widerrufsbelehrung mit Muster-Widerrufsformular des Terminplaners). analyseLegal.js ist ein duenner Wrapper darum. +// Die Fassung ist der Berliner Kalendertag der Texte (JJJJ-MM-TT); eine ausgelieferte Fassung wird nie ueberschrieben. +import { access, readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import { Router } from 'express' +import { config } from '../config.js' + +const publicDir = fileURLToPath(new URL('../../public/', import.meta.url)) +const VERSION_RE = /^\d{4}-\d{2}-\d{2}$/ + +/** + * Dokumente je Satz in Anhangsreihenfolge: slug = Dateiname der HTML-Seite und des PDFs (-.html|pdf), + * pdf = Dateiname des Mail-Anhangs (-.pdf). config.js baut die Adressen des Terminplaners aus denselben + * Slugs (config.js darf diese Datei nicht importieren); test/legalDocs.test.js gleicht beide ab. + */ +const SAETZE = { + analyse: { + avv: { slug: 'avv-website-analyse', pdf: 'AVV-Website-Analyse' }, + lb: { slug: 'leistungsbeschreibung-website-analyse', pdf: 'Leistungsbeschreibung-Website-Analyse' }, + }, + termine: { + lb: { slug: 'leistungsbeschreibung-terminplaner', pdf: 'Leistungsbeschreibung-Terminplaner' }, + avv: { slug: 'avv-terminplaner', pdf: 'AVV-Terminplaner' }, + widerruf: { slug: 'widerrufsbelehrung-funktionen', pdf: 'Widerrufsbelehrung-Muster-Widerrufsformular' }, + }, +} + +/** Satz, dessen Dokumente eine Funktion (Registerschluessel) mit der Bestellung bekommt; News hat keine. */ +const FUNKTION_SATZ = { news: null, termine: 'termine' } + +/** Aktuelle Fassung je Satz, bei jedem Aufruf aus config gelesen (Tests aendern sie zur Laufzeit). */ +const aktuelleFassung = (satz) => (satz === 'analyse' ? config.analyse.avvVersion : config.termine.avvVersion) + +function satzDokumente(satz) { + if (typeof satz !== 'string' || !Object.hasOwn(SAETZE, satz)) throw new Error('Rechtstexte: unbekannter Satz') + return SAETZE[satz] +} + +function pruefeFassung(version) { + if (!VERSION_RE.test(String(version))) throw new Error('Rechtstexte: ungültige Fassung') + return String(version) +} + +/** Adressen der versionierten HTML-Seiten: { [doc]: '/rechtliches/-.html' }. */ +export function legalPaths(satz, version) { + const dokumente = satzDokumente(satz) + const v = pruefeFassung(version) + return Object.fromEntries(Object.entries(dokumente).map(([doc, d]) => [doc, `/rechtliches/${d.slug}-${v}.html`])) +} + +/** PDF je Dokument: { [doc]: { filename (Anhangsname), path (Datei unter public/rechtliches/) } }. */ +export function legalPdfFiles(satz, version) { + const dokumente = satzDokumente(satz) + const v = pruefeFassung(version) + return Object.fromEntries( + Object.entries(dokumente).map(([doc, d]) => [doc, { filename: `${d.pdf}-${v}.pdf`, path: join(publicDir, 'rechtliches', `${d.slug}-${v}.pdf`) }]) + ) +} + +/** + * PDFs als Mail-Anhaenge in der Reihenfolge des Satzes (termine: lb, avv, widerruf; analyse: avv, lb); wirft, wenn eine + * Datei fehlt oder nicht lesbar ist. ohne: Dokumente, die nicht angehaengt werden (sie muessen nicht vorhanden sein). + */ +export async function legalPdfAttachments(satz, version, { ohne = [] } = {}) { + const dateien = legalPdfFiles(satz, version) + for (const doc of ohne) { + if (!Object.hasOwn(dateien, doc)) throw new Error('Rechtstexte: unbekanntes Dokument') + } + return Promise.all( + Object.entries(dateien) + .filter(([doc]) => !ohne.includes(doc)) + .map(async ([, f]) => ({ filename: f.filename, content: await readFile(f.path), contentType: 'application/pdf' })) + ) +} + +function funktionSatz(featureKey) { + if (typeof featureKey !== 'string' || !Object.hasOwn(FUNKTION_SATZ, featureKey)) throw new Error('Rechtstexte: unbekannte Funktion') + return FUNKTION_SATZ[featureKey] +} + +/** + * Anhaenge der Bestellmail einer Funktion in der aktuellen Fassung: termine => Satz 'termine', news => []. Wirft bei + * unbekannter Funktion und bei fehlender Datei (der Aufrufer entscheidet, was dann geschieht). + */ +export async function featureLegalAttachments(featureKey, opts) { + const satz = funktionSatz(featureKey) + return satz ? legalPdfAttachments(satz, aktuelleFassung(satz), opts) : [] +} + +/** + * true, wenn alle Rechtsdokumente der Funktion in der aktuellen Fassung vorliegen: HTML-Seiten (die die Bestellseite + * verlinkt) und PDFs (die Mail anhaengt). Unbekannte Funktion und jeder Lesefehler: false (fail-closed). + */ +export async function featureLegalReady(featureKey) { + try { + const satz = funktionSatz(featureKey) + if (!satz) return true + const v = aktuelleFassung(satz) + await Promise.all(Object.values(legalPaths(satz, v)).map((adresse) => access(join(publicDir, adresse)))) + await legalPdfAttachments(satz, v) + return true + } catch { + return false + } +} + +/** + * Adressen ohne Fassung (/rechtliches/.html) liefern die Datei der aktuellen Fassung aus config. Nur fuer + * Menschen und die WOMS-Karten: keine Mail und kein Zustimmungstext darf sie verlinken, denn sie aendern sich mit der + * Fassung. Gehoert vor express.static; fehlt die Datei, geht die Anfrage weiter (404). saetze: welche Saetze bedient + * werden, dir: Verzeichnis der Dateien (Standard public/rechtliches, Tests uebergeben ein eigenes). + */ +export function legalRouter({ saetze = Object.keys(SAETZE), dir = join(publicDir, 'rechtliches') } = {}) { + const router = Router() + for (const satz of saetze) { + for (const d of Object.values(satzDokumente(satz))) { + router.get(`/${d.slug}.html`, (_req, res, next) => { + const datei = join(dir, `${d.slug}-${pruefeFassung(aktuelleFassung(satz))}.html`) + res.sendFile(datei, (err) => err && next(err.code === 'ENOENT' ? undefined : err)) + }) + } + } + return router +} diff --git a/test/legalDocs.test.js b/test/legalDocs.test.js new file mode 100644 index 0000000..5c5db8f --- /dev/null +++ b/test/legalDocs.test.js @@ -0,0 +1,237 @@ +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-Rechtsdokumente bleiben unberührt', () => { + const n = featureConfig('news') + assert.deepEqual(n.leistungsbeschreibung, { version: '2026-09-30', 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))')) +}) diff --git a/test/termineSchalter.test.js b/test/termineSchalter.test.js index c0d08ee..9f49cee 100644 --- a/test/termineSchalter.test.js +++ b/test/termineSchalter.test.js @@ -41,9 +41,12 @@ async function freshConfig(env = {}) { test('Konfiguration: Vorgaben ohne Umgebungsvariablen', async () => { const fresh = await freshConfig() + // avvVersion: Fassung der Bestellunterlagen (Berliner Tag des Fassungs-Commits), hier nur das Datumsmuster + assert.match(fresh.termine.avvVersion, /^\d{4}-\d{2}-\d{2}$/) assert.deepEqual(fresh.termine, { mode: 'off', pilotEmails: [], + avvVersion: fresh.termine.avvVersion, holdMinutes: 30, zelleMin: 5, mailsProStunde: 200,