Files
Webklar-Kundenbereich/test/legalTexts.test.js
T

109 lines
5.6 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
const { config } = await import('../server/config.js')
// = `git show 18c3cdf:public/abo-bedingungen.html | sha256sum` (Fassung 3, Stand 25.09.2026).
// Fest eingetragen, weil die Tests auch ohne git laufen muessen (node:20-alpine).
const V3_SHA256 = '1c9e4b002dcd5d7896bbda24c573e56544b7123e3a88d154e5db77bd2855bc47'
const file = (rel) => new URL(`../${rel}`, import.meta.url)
const read = (rel) => readFileSync(file(rel), 'utf8')
const sha256 = (rel) => createHash('sha256').update(readFileSync(file(rel))).digest('hex')
const strip = (html) => html.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
// HTML-Ausschnitt von einem Anker bis zum naechsten, danach ohne Tags. Die Inhaltsuebersicht wiederholt
// die Ueberschriften, deshalb wird nicht nach dem ersten Text-Treffer, sondern nach id="..." geschnitten.
const section = (html, fromId, toId) => {
const a = html.indexOf(`id="${fromId}"`)
assert.ok(a >= 0, `Anker #${fromId} fehlt`)
const b = html.indexOf(`id="${toId}"`, a + 1)
assert.ok(b > a, `Anker #${toId} fehlt nach #${fromId}`)
return strip(html.slice(a, b))
}
const between = (s, a, b) => {
const i = s.indexOf(a)
assert.ok(i >= 0, `Text fehlt: ${a}`)
const j = s.indexOf(b, i)
assert.ok(j > i, `Text fehlt: ${b}`)
return s.slice(i, j)
}
const V3 = 'public/vertragsunterlagen/v3/abo-bedingungen.html'
const V4 = 'public/abo-bedingungen.html'
test('Fassung 3 unverändert archiviert (HTML und PDF)', () => {
assert.equal(sha256(V3), V3_SHA256)
// Das bisherige PDF bleibt die Fassung 3 und liegt identisch im Archiv
assert.equal(
sha256('public/vertragsunterlagen/v3/WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'),
sha256('public/WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')
)
assert.ok(read(V3).includes('Version 3 · Stand 25.09.2026'))
})
test('Widerrufsbelehrung und Muster-Widerrufsformular wortgleich mit Fassung 3', () => {
const v3 = read(V3), v4 = read(V4)
const muster = (belehrung) => between(belehrung, 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung')
const muster3 = muster(section(v3, 'widerruf', 'formular'))
assert.ok(muster3.length > 1500)
assert.equal(muster(section(v4, 'widerruf', 'formular')), muster3)
// Muster-Widerrufsformular (Anlage 2 zu Art. 246a EGBGB) ebenfalls unveraendert
const formular = (s) => between(s, 'Muster-Widerrufsformular', '(*) Unzutreffendes streichen.')
assert.equal(formular(section(v4, 'formular', 'widerruf-hinweise')), formular(section(v3, 'formular', 'avv')))
})
test('Fassung 4: beide Abos, Postfächer, Zusatzspeicher, Wechsel', () => {
const v4html = read(V4), v4 = strip(v4html)
for (const s of ['15,00 €', '50,00 €', 'beliebig viele Postfächer', 'zusammen 5 GB', '1,00 €', 'Zusatzspeicher',
'12 Monate', 'statische', 'Hetzner', '§ 19 UStG', 'Leistungsbeschreibung A', 'Leistungsbeschreibung B',
'„Zahlungspflichtig bestellen“', '„Verträge hier kündigen“', '„Nur den E-Mail-Zusatzspeicher“', '„Speichern“',
'Vertragsbedingungen für die WEBklar-Abos']) assert.ok(v4.includes(s), s)
assert.ok(!v4.includes('Version 3'))
const hosting = section(v4html, 'hosting', 'wartung')
assert.ok(hosting.includes('1 E-Mail-Postfach'))
assert.ok(hosting.includes('statische'))
const wartung = section(v4html, 'wartung', 'widerruf')
assert.ok(wartung.includes('beliebig viele Postfächer'))
assert.ok(wartung.includes('zusammen 5 GB'))
assert.ok(!wartung.includes('1 E-Mail-Postfach'))
// Hinweise zum Widerruf bei Wechsel und Zusatzspeicher stehen ausserhalb der Musterbelehrung
const hinweise = section(v4html, 'widerruf-hinweise', 'avv')
assert.ok(hinweise.includes('Wechsel'))
assert.ok(hinweise.includes('Zusatzspeicher'))
assert.ok(hinweise.includes('Wertersatz'))
})
test('Anker der Fassung 4', () => {
const v4 = read(V4)
for (const id of ['widerruf', 'hosting', 'wartung', 'zusatzspeicher', 'wechsel', 'avv', 'widerruf-hinweise', 'formular', 'tom', 'sub']) {
assert.equal(v4.split(`id="${id}"`).length, 2, `Anker #${id} genau einmal`)
}
// Reihenfolge: allgemeiner Teil, Leistungsbeschreibung A, B, Widerruf, Hinweise, Anlagen 1-3
const order = ['id="wechsel"', 'id="zusatzspeicher"', 'id="hosting"', 'id="wartung"', 'id="widerruf"', 'id="formular"',
'id="widerruf-hinweise"', 'id="avv"', 'id="tom"', 'id="sub"'].map((m) => v4.indexOf(m))
assert.deepEqual([...order].sort((a, b) => a - b), order)
})
test('Version und Stand passen zu termsVersion', () => {
const m = /^(\d{4})-(\d{2})-(\d{2})-v4$/.exec(config.abo.termsVersion)
assert.ok(m, `termsVersion ${config.abo.termsVersion}`)
const [, y, mo, d] = m
assert.ok(strip(read(V4)).includes(`Version 4 · Stand ${d}.${mo}.${y}`))
assert.ok(read(V4).includes(`content: "Version 4 · Stand ${d}.${mo}.${y}"`), 'Kopfzeile im Druck')
})
test('PDF der Fassung 4 verlinkt, erzeugt und im Build-Skript', () => {
const v4 = read(V4)
assert.ok(v4.includes('href="/WEBklar-Abo-Vertragsunterlagen.pdf"'))
assert.ok(!v4.includes('WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'))
const pdf = readFileSync(file('public/WEBklar-Abo-Vertragsunterlagen.pdf'))
assert.ok(pdf.length > 50_000 && pdf.subarray(0, 5).toString() === '%PDF-')
const build = read('scripts/build-legal-pdfs.sh')
assert.ok(build.includes('pdf public/abo-bedingungen.html public/WEBklar-Abo-Vertragsunterlagen.pdf'))
const erzeugt = build.split('\n').filter((l) => l.startsWith('pdf '))
assert.ok(!erzeugt.some((l) => l.includes('WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')), 'Fassung 3 wird nicht neu erzeugt')
})