Files
ClaudeandClaude Sonnet 5.5 bfe3895a50 Kundenansicht neu P7: Einschalten zeigt Linkzeile, Häkchen und Unterlagen der Fassung 6 aus P8
Verdrahtet P5 Task 15 und P7 Task 0 (N33): consentAbo liest Linkzeile und Hinweis aus WORTLAUT_V6, unterlagenFuer die fünf Funktionen aus FUNKTIONEN_UNTERLAGEN_V6 (Analyse bleibt bei den Alt-Unterlagen bis zur Freigabe), config.aboFunktionen.texte.hinweis entfällt, pdfAnhaenge liest PDFs der Fassung 6, sichereAdresse lässt /neu/rechtliches/ zu. Der Analyse-Kundenweg setzt wie der Team-Weg den Abo-Marker (unlockedBy und einrichtungAbrechnung abo).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:56:40 +00:00

96 lines
6.3 KiB
JavaScript

import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { sichereAdresse } from '../public/neu/funktion-einschalten.js'
// Verdrahtung P5 <-> P8 <-> P7 (N33): Einschalten, Katalog und Nachweis zeigen Linkzeile, Haekchen, Hinweis und Unterlagen der
// Fassung 6 aus products.js. Die Gleichheit mit WORTLAUT_V6 und FUNKTIONEN_UNTERLAGEN_V6 ist gepinnt, nicht ein Literal.
const aw = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const reg = await import('../server/services/featureRegistry.js')
const { katalogFuerKunde } = await import('../server/services/aboKatalog.js')
const { unterlagenFuer, analyseUnterlagen, pdfAnhaenge } = await import('../server/services/aboUnterlagen.js')
const { WORTLAUT_V6, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6, mitFassung } = await import('../server/services/products.js')
const KEYS = ['news', 'termine', 'shop', 'unterkunft', 'zeiten']
const unterlage = (d) => (d ? { version: FUNKTIONEN_FASSUNG_V6, url: d.url } : null)
test('config.aboFunktionen trägt die Literale aus P8, config.js importiert keine Dienste', () => {
assert.deepEqual([config.aboFunktionen.fassung, config.aboFunktionen.texte.beginn], [FUNKTIONEN_FASSUNG_V6, WORTLAUT_V6.beginn.einrichtung])
assert.equal(Object.hasOwn(config.aboFunktionen.texte, 'hinweis'), false, 'der Hinweis kommt aus WORTLAUT_V6, nicht aus config')
assert.doesNotMatch(readFileSync(new URL('../server/config.js', import.meta.url), 'utf8'), /from '\.\/services\//)
})
test('consentAbo: Linkzeile mit und ohne AVV aus WORTLAUT_V6, Hinweis und Häkchen aus WORTLAUT_V6, Nachweis gleich', () => {
for (const key of KEYS) {
const mitAvv = FUNKTIONEN_UNTERLAGEN_V6[key].avv !== null
const zeile = mitFassung(WORTLAUT_V6.linkzeile[mitAvv ? 'einschaltenMitAvv' : 'einschaltenOhneAvv'], FUNKTIONEN_FASSUNG_V6)
const c = reg.consentAbo(key)
assert.equal(c.version, FUNKTIONEN_FASSUNG_V6, key)
assert.equal(c.items[0].text, zeile, key)
assert.equal(c.items.at(-1).text, WORTLAUT_V6.beginn.einrichtung, key)
assert.equal(c.items.find((i) => i.id === 'hinweis')?.text, WORTLAUT_V6.hinweis[key], key)
assert.deepEqual(reg.consentAboAktiv(key, { beginn: true }).items.map((i) => i.text), [zeile, ...(WORTLAUT_V6.hinweis[key] ? [WORTLAUT_V6.hinweis[key]] : []), WORTLAUT_V6.beginn.einrichtung], key)
}
assert.equal(reg.consentAbo('analyse'), null)
assert.equal(reg.consentAbo('gibtEsNicht'), null)
})
test('unterlagenFuer und aboweg: die fünf Funktionen aus FUNKTIONEN_UNTERLAGEN_V6, die Analyse bleibt bei den Alt-Unterlagen (N33)', () => {
for (const key of KEYS) {
assert.deepEqual(unterlagenFuer(key), { leistungsbeschreibung: unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].lb), avv: unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].avv) }, key)
const w = reg.aboweg(reg.featureConfig(key))
assert.deepEqual([w.leistungsbeschreibung, w.avv], [unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].lb), unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].avv)], key)
}
const a = analyseUnterlagen()
assert.deepEqual(a, { avvVersion: config.analyse.avvVersion, legal: { lb: `/rechtliches/leistungsbeschreibung-website-analyse-${config.analyse.avvVersion}.html`, avv: `/rechtliches/avv-website-analyse-${config.analyse.avvVersion}.html` } })
assert.equal(reg.consentAboAnalyse().items[0].text, `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${config.analyse.avvVersion}).`)
})
test('Katalog: unterlagen aus FUNKTIONEN_UNTERLAGEN_V6, hinweis aus WORTLAUT_V6, Analyse bleibt bei den Alt-Unterlagen', async () => {
const vorher = config.analyse.enabled
config.analyse.enabled = true
try {
const katalog = await katalogFuerKunde({ email: 'k@example.invalid' })
for (const key of KEYS) {
const e = katalog.find((x) => x.key === key)
assert.deepEqual(e.unterlagen, { leistungsbeschreibung: unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].lb), avv: unterlage(FUNKTIONEN_UNTERLAGEN_V6[key].avv) }, key)
assert.equal(e.hinweis, WORTLAUT_V6.hinweis[key] ?? null, key)
}
const a = katalog.find((x) => x.key === 'analyse')
assert.deepEqual([a.unterlagen.avv.version, a.unterlagen.avv.url, a.hinweis], [config.analyse.avvVersion, `/rechtliches/avv-website-analyse-${config.analyse.avvVersion}.html`, null])
assert.doesNotMatch(JSON.stringify(katalog.filter((e) => e.key !== 'analyse')), /"\/(rechtliches|vertragsunterlagen)\//, 'kein Alt-Dokument mit Preisen')
} finally {
config.analyse.enabled = vorher
}
})
test('sichereAdresse (P3) lässt jede Adresse der Funktionsunterlagen durch, auch unter /neu/rechtliches/', () => {
for (const u of Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((x) => [x.lb, x.avv]).filter(Boolean)) assert.equal(sichereAdresse(u.url), u.url, u.url)
for (const schlecht of ['/neu/rechtliches/../geheim', '/neu/rechtliches/%2e%2e/x', '/neu/rechtliches/a b', '/neu/rechtliches\\x', '/neu/andere/seite.html', '/neu/rechtliches', '//example.org/neu/rechtliches/x']) {
assert.equal(sichereAdresse(schlecht), '', schlecht)
}
})
test('pdfAnhaenge: liest die PDFs; fehlende, leere und Nicht-PDF-Dateien werfen', async () => {
const dir = await mkdtemp(join(process.env.TMPDIR ?? tmpdir(), 'kn-pdf-')) // im eigenen Scratch-Ordner, wird nicht gelöscht
await mkdir(join(dir, 'neu/rechtliches'), { recursive: true })
const dok = (name) => ({ pdfFile: `neu/rechtliches/${name}.pdf`, anhangName: `${name}-2026-10-16.pdf` })
await writeFile(join(dir, dok('ok').pdfFile), '%PDF-1.4 x')
await writeFile(join(dir, dok('leer').pdfFile), '')
await writeFile(join(dir, dok('text').pdfFile), 'hallo')
const ok = await pdfAnhaenge([dok('ok')], { basis: dir })
assert.deepEqual(ok.map((x) => [x.filename, x.contentType]), [['ok-2026-10-16.pdf', 'application/pdf']])
assert.ok(Buffer.isBuffer(ok[0].content) && ok[0].content.length > 0)
for (const name of ['fehlt', 'leer', 'text']) await assert.rejects(() => pdfAnhaenge([dok(name)], { basis: dir }), name)
})