Rechtstexte: Dokument-Registry und Fassungskonstante des Terminplaners
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
938b7099dd
commit
b20d0464c4
6 files changed
+414
-53
No files matched your search
+37
-2
@@ -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,
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
@@ -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'] })
|
||||
@@ -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 (<slug>-<Fassung>.html|pdf),
|
||||
* pdf = Dateiname des Mail-Anhangs (<pdf>-<Fassung>.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/<slug>-<Fassung>.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/<slug>.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
|
||||
}
|
||||
@@ -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) => `<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)
|
||||
})
|
||||
})
|
||||
|
||||
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))'))
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user