332 lines
17 KiB
JavaScript
332 lines
17 KiB
JavaScript
import { test, after, mock } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync, writeSync } from 'node:fs'
|
|
import fsp from 'node:fs/promises'
|
|
import { syncBuiltinESMExports } from 'node:module'
|
|
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, featureLegalPaths, legalRouter, setLegalDirForTests } = 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) => `<p>${name} ${version}</p>`
|
|
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)
|
|
})
|
|
})
|
|
|
|
// R114 (Task F8): featureLegalReady läuft bei jeder Bestellung (Stufe 5b des Checkouts) und prüft je PDF nur stat (Größe > 0) und die
|
|
// ersten fünf Bytes "%PDF-", nie den ganzen Inhalt. Die Dateien liegen für den Test in einem Verzeichnis mit Kopien.
|
|
test('featureLegalReady("termine"): je PDF stat und "%PDF-", nie den ganzen Inhalt lesen; jeder Mangel und jeder Fehler ergibt false, nie einen Wurf', async () => {
|
|
const kopien = mkdtempSync(join(tmpdir(), 'legal-ready-'))
|
|
mkdirSync(join(kopien, 'rechtliches'))
|
|
// Quellen: die ausgelieferten Dateien (vor dem Tausch des Verzeichnisses ermittelt)
|
|
const echteHtml = legalPaths('termine', V)
|
|
const echtePdf = Object.fromEntries(Object.entries(legalPdfFiles('termine', V)).map(([doc, f]) => [doc, f.path]))
|
|
const bereit = () => {
|
|
for (const adresse of Object.values(echteHtml)) copyFileSync(root(`public${adresse}`), join(kopien, adresse))
|
|
for (const pfad of Object.values(echtePdf)) copyFileSync(pfad, join(kopien, 'rechtliches', pfad.split('/').at(-1)))
|
|
}
|
|
const pdfPfad = (doc) => join(kopien, 'rechtliches', echtePdf[doc].split('/').at(-1))
|
|
const htmlPfad = (doc) => join(kopien, echteHtml[doc])
|
|
const lesen = mock.method(fsp, 'readFile')
|
|
syncBuiltinESMExports()
|
|
try {
|
|
setLegalDirForTests(kopien)
|
|
bereit()
|
|
assert.equal(await featureLegalReady('termine'), true)
|
|
assert.equal(lesen.mock.callCount(), 0, 'readFile wird nicht aufgerufen')
|
|
|
|
// ein riesiges PDF mit gültigem Kopf (nur Loch im Dateisystem): bereit, ohne es zu lesen
|
|
const gross = openSync(pdfPfad('lb'), 'w')
|
|
writeSync(gross, '%PDF-1.4\n')
|
|
closeSync(gross)
|
|
await fsp.truncate(pdfPfad('lb'), 512 * 1024 * 1024)
|
|
assert.equal(await featureLegalReady('termine'), true)
|
|
assert.equal(lesen.mock.callCount(), 0, 'auch ein 512-MB-PDF wird nicht gelesen')
|
|
bereit()
|
|
|
|
// Mängel je Datei: fehlend, leer, falscher Kopf, zu kurz, Verzeichnis statt Datei
|
|
for (const doc of ['lb', 'avv', 'widerruf']) {
|
|
const pfad = pdfPfad(doc)
|
|
rmSync(pfad)
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: PDF fehlt`)
|
|
writeFileSync(pfad, '')
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: PDF leer`)
|
|
writeFileSync(pfad, '<html>kein PDF</html>')
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: kein %PDF-`)
|
|
writeFileSync(pfad, '%PDF')
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: zu kurz`)
|
|
rmSync(pfad)
|
|
mkdirSync(pfad)
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: Verzeichnis statt Datei`)
|
|
rmSync(pfad, { recursive: true })
|
|
bereit()
|
|
assert.equal(await featureLegalReady('termine'), true, `${doc}: wieder bereit`)
|
|
// die HTML-Seite fehlt
|
|
rmSync(htmlPfad(doc))
|
|
assert.equal(await featureLegalReady('termine'), false, `${doc}: HTML fehlt`)
|
|
bereit()
|
|
}
|
|
assert.equal(lesen.mock.callCount(), 0, 'nie readFile')
|
|
// ohne Verzeichnis, mit unbekannter Funktion und mit unbekannter Fassung: false statt Wurf
|
|
await mitFassung('1999-01-01', async () => assert.equal(await featureLegalReady('termine'), false))
|
|
assert.equal(await featureLegalReady('gibtsnicht'), false)
|
|
setLegalDirForTests(join(kopien, 'gibtsnicht'))
|
|
assert.equal(await featureLegalReady('termine'), false)
|
|
// die Anhänge der Mail lesen dagegen die ganzen Dateien (legalPdfAttachments)
|
|
setLegalDirForTests(kopien)
|
|
bereit()
|
|
assert.equal((await featureLegalAttachments('termine')).length, 3)
|
|
assert.ok(lesen.mock.callCount() >= 3)
|
|
} finally {
|
|
setLegalDirForTests(null)
|
|
mock.restoreAll()
|
|
syncBuiltinESMExports()
|
|
rmSync(kopien, { recursive: true, force: true })
|
|
}
|
|
assert.equal(await featureLegalReady('termine'), true, 'mit den ausgelieferten Dateien bereit')
|
|
})
|
|
|
|
// Fassung wählbar (Task F8, Fix-Runde 1): K15 schickt die Unterlagen der Fassung, der der Kunde zugestimmt hat
|
|
test('featureLegalAttachments / featureLegalReady / featureLegalPaths mit opts.fassung', async () => {
|
|
assert.deepEqual(featureLegalPaths('termine'), legalPaths('termine', V))
|
|
assert.deepEqual(featureLegalPaths('termine', { fassung: '2026-10-01' }), legalPaths('termine', '2026-10-01'))
|
|
assert.equal(featureLegalPaths('news'), null)
|
|
assert.throws(() => featureLegalPaths('gibtsnicht'), /Funktion/)
|
|
assert.throws(() => featureLegalPaths('termine', { fassung: '2026-10-5' }), /Fassung/)
|
|
// ausdrücklich die aktuelle Fassung = ohne Angabe
|
|
assert.deepEqual((await featureLegalAttachments('termine', { fassung: V })).map((a) => a.filename), (await featureLegalAttachments('termine')).map((a) => a.filename))
|
|
assert.equal(await featureLegalReady('termine', { fassung: V }), true)
|
|
// andere Fassung ohne Dateien: Wurf bei den Anhängen, false bei der Prüfung, news weiter []
|
|
await assert.rejects(featureLegalAttachments('termine', { fassung: '1999-01-01' }))
|
|
assert.equal(await featureLegalReady('termine', { fassung: '1999-01-01' }), false)
|
|
assert.equal(await featureLegalReady('termine', { fassung: 'kaputt' }), false)
|
|
assert.deepEqual(await featureLegalAttachments('news', { fassung: '1999-01-01' }), [])
|
|
// opts.ohne wirkt weiter neben fassung
|
|
assert.deepEqual((await featureLegalAttachments('termine', { fassung: V, ohne: ['avv'] })).map((a) => a.filename.split('-')[0]), ['Leistungsbeschreibung', 'Widerrufsbelehrung'])
|
|
})
|
|
|
|
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 <F> (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))'))
|
|
})
|