Terminplaner TP2: Bestell-Gate und Gesundheits-Stichwortpruefung

fragebogen.js ersetzt den Stub aus Block B: bestellpruefung (Kriterien (a) bis (h),
je Punkt ein Eintrag, nach Schrittfolge sortiert, Abo-Punkt nur bei mitAbo !== false),
SCHRITT_TITEL, gesundheitsWarnungen (Stichworte nur am Wortanfang, hoechstens 10
Meldungen) und bestellungAnsicht fuer die Bestellseite. einstellungen.js exportiert
sichtbareLeistungen und MELDUNG_PFLICHT. Testhelfer bestellbarerStand und entwurfDoc;
der Stub-Test entfaellt, die zwei B-Tests mit dem Stub laufen gegen das echte Gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 19:00:12 +00:00
1 parent ab47fabcb2
commit 008ad4fb1e
7 files changed
+869 -47

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ beforeEach(async () => {
mails.versandFehler = null
config.termine.mode = 'pilot'
config.termine.pilotEmails = ['*']
// Die Fragebogen-Pruefung gehoert zu Block C: hier ist jedes Projekt bestellbar
// Die Bestellpruefung (Fragebogen-Gate, termine/fragebogen.js) ist hier nicht Gegenstand: jedes Projekt ist bestellbar
T.bestellpruefung = async () => []
})
+73 -8
View File
@@ -27,6 +27,7 @@ const { config } = await import('../server/config.js')
const { startTestApp, post } = await import('./support/testApp.js')
const { customerCookie, kundenansichtCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const { bestellbarerStand, entwurfDoc } = await import('./support/fragebogenStand.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const reg = await import('../server/services/featureRegistry.js')
const order = await import('../server/services/featureOrder.js')
@@ -349,9 +350,10 @@ test('Bestellpruefung: kaputter Haken (keine Liste) lehnt ab, 500 ohne Rohtext (
test('Standard-Haken liest den Entwurf nur, wenn es ein Dokument gibt, und ruft die Bestellpruefung ohne Abo', async () => {
const haken = reg.featureHaken('termine')
const arg = { customerId: 'c1', projectId: 'p1', feature: T }
// ohne Dokument: keine Abfrage von termineConfig, der Platzhalter meldet nichts offen
// ohne Dokument: keine Abfrage von termineConfig, das echte Gate meldet nur die Branche offen (kein Abo-Punkt)
aw.failNext({ collection: 'termineConfig', status: 500 })
assert.deepEqual(await haken.bestellpruefung(arg), [])
const ohneDoc = await haken.bestellpruefung(arg)
assert.deepEqual(ohneDoc.map((p) => p.schritt), ['branche'])
// mit Dokument: der Entwurf wird geladen (der eingeplante Lesefehler trifft)
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
const log = stumm()
@@ -360,10 +362,21 @@ test('Standard-Haken liest den Entwurf nur, wenn es ein Dokument gibt, und ruft
} finally {
log.mock.restore()
}
assert.deepEqual(await haken.bestellpruefung(arg), [], 'danach wieder normal')
// Dokument ohne gespeicherten Entwurf: wie ohne Dokument
assert.deepEqual((await haken.bestellpruefung(arg)).map((p) => p.schritt), ['branche'], 'danach wieder normal')
// bestellbarer Entwurf: leer; offener Fragebogen: Punkte der Schritte, nie der Abo-Punkt (mitAbo: false)
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1' }))
assert.deepEqual(await haken.bestellpruefung(arg), [])
const offen = bestellbarerStand()
offen.fragebogen.bestaetigt = []
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand: offen }))
const punkte = await haken.bestellpruefung(arg)
assert.equal(punkte.length, 8)
assert.ok(punkte.every((p) => p.schritt !== 'bestellen'))
})
// ------------------------------------------------------------------------------------------ 7 Abo ---
const SCHRITTE_OHNE_BRANCHE = ['wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website']
const ABO_TEXT = {
kein_abo: 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.',
gekuendigt: 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.',
@@ -443,15 +456,67 @@ test('Abo-Lesefehler: 500 ohne Rohtext, fail-closed ohne Spur', async () => {
}
})
test('Abo-Stufe mit leerer Pruefung (Stub und Standard-Gate): ohne Abo 409 kein_abo, nie bestellung_offen', async () => {
test('Abo-Stufe mit dem echten Gate: ohne Abo und bestellbarem Entwurf 409 kein_abo, mit offenem Fragebogen 409 bestellung_offen (ohne bestellen)', async () => {
const c3 = customerCookie('c3')
// Standard-Haken (Platzhalter von Block B meldet nichts offen)
aboAbgelehnt(await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3), 'kein_abo')
// ueberschriebener Haken mit leerer Liste
const entwurf = (stand) => {
aw.seed('portalFeatures', { $id: 'f_p3_termine', customerId: 'c3', projectId: 'p3', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
aw.seed('termineConfig', entwurfDoc('f_p3_termine', { customerId: 'c3', projectId: 'p3', stand }))
}
// Standard-Haken, Kunde ohne Abo, bestellbarer Entwurf: das Gate ist leer, die Abo-Stufe lehnt ab
entwurf(bestellbarerStand())
const featuresVorher = aw.docs('portalFeatures').size
const abgelehnt = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
assert.equal(abgelehnt.status, 409)
assert.equal(abgelehnt.body.code, 'kein_abo')
assert.notEqual(abgelehnt.body.code, 'bestellung_offen')
assert.equal(abgelehnt.body.error, ABO_TEXT.kein_abo)
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: featuresVorher }, 'keine Sitzung, kein Nachweis, kein neues Dokument')
// Kunde ohne Abo, Fragebogen offen: bestellung_offen, und die Liste enthaelt keinen Abo-Punkt (mitAbo: false)
const offen = bestellbarerStand()
offen.fragebogen.bestaetigt = ['branche']
entwurf(offen)
const res = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
assert.equal(res.status, 409)
assert.equal(res.body.code, 'bestellung_offen')
assert.equal(res.body.offen.length, 7)
assert.ok(res.body.offen.every((p) => p.schritt !== 'bestellen'), 'kein Eintrag bestellen')
assert.deepEqual(res.body.offen.map((p) => p.schritt), SCHRITTE_OHNE_BRANCHE)
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: featuresVorher })
// ohne Entwurf (gar kein Dokument): ebenfalls bestellung_offen mit der Branche, nie ein Abo-Punkt
for (const col of ['portalFeatures', 'termineConfig']) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
const leer = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
assert.equal(leer.status, 409)
assert.equal(leer.body.code, 'bestellung_offen')
assert.deepEqual(leer.body.offen.map((p) => p.schritt), ['branche'])
// ueberschriebener Haken mit leerer Liste: weiter die Abo-Stufe
T.bestellpruefung = async () => []
aboAbgelehnt(await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3), 'kein_abo')
})
test('Echtes Gate bis zur Stripe-Sitzung: Kunde mit Wartungs-Abo und bestellbarem Entwurf kommt durch, mit offenem Punkt nicht', async () => {
const entwurf = (stand) => {
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand }))
}
// ein offener Punkt (Impressum fehlt): 409 bestellung_offen, keine Sitzung, kein Nachweis
const luecke = bestellbarerStand()
luecke.einstellung.betrieb.impressumUrl = null
entwurf(luecke)
const offen = await checkout('termine', bodyT())
assert.equal(offen.status, 409)
assert.equal(offen.body.code, 'bestellung_offen')
assert.deepEqual(offen.body.offen.map((p) => p.schritt), ['website'])
assert.deepEqual({ sessions: sitzungen().length, consents: nachweise().length }, { sessions: 0, consents: 0 })
// bestellbarer Entwurf: Gate leer, Abo laeuft, die Stripe-Sitzung entsteht
entwurf(bestellbarerStand())
const ok = await checkout('termine', bodyT())
assert.equal(ok.status, 200, JSON.stringify(ok.body))
assert.match(ok.body.url, /^https?:\/\//)
assert.equal(sitzungen().length, 1)
assert.equal(nachweise().length, 1)
assert.equal(termineDoc().state, 'ordered')
})
test('aboMeldung: Wortlaute aus dem Register, Akkusativ aus mails.formen.akk, unbekannter Grund wie unbekannt', () => {
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv']) assert.equal(order.aboMeldung(T, grund), ABO_TEXT[grund], grund)
assert.equal(order.aboMeldung(T, 'unbekannt'), 'Das Abo konnte gerade nicht geprüft werden.')
@@ -544,7 +609,7 @@ test('Abo-Memo: je Anfrage ein aboMemo(), News fragt das Abo nie', async () => {
})
// ------------------------------------------------------------------------------------------ Sperre, Zustand, Nachweis (B7) ---
// Ab hier der Rest der Route: die Vorpruefungen sind bestanden (Fragebogen-Pruefung per Haken uebersprungen, Block C fuellt sie).
// Ab hier der Rest der Route: die Vorpruefungen sind bestanden (Fragebogen-Pruefung per Haken uebersprungen; das echte Gate pruefen die Tests der Abschnitte 6 und 7).
const bereit = () => {
T.bestellpruefung = async () => []
}
+59
View File
@@ -0,0 +1,59 @@
import { ausVorlage } from '../../server/services/termine/vorlagen.js'
import { SCHRITTE, leerFragebogen } from '../../server/services/termine/fragebogen.js'
/**
* Testhelfer fuer die Bestellpruefung des Terminplaners (TP2, Task C2): ein Entwurf, der alle Punkte des Gates erfuellt,
* und das termineConfig-Dokument dazu. Erst nach useTestEnv() dynamisch importieren, wenn der Test config.js mit
* Testwerten braucht (die Vorlagen ziehen config.js mit).
*/
const BETRIEB = {
name: 'Salon Muster',
email: 'salon@example.invalid',
telefon: '06301 12345',
adresse: 'Hauptstraße 1, 67655 Kaiserslautern',
impressumUrl: 'https://salon.example.invalid/impressum',
datenschutzUrl: 'https://salon.example.invalid/datenschutz',
bundesland: 'RP',
}
/**
* Ein bestellbarer Entwurf: Vorlage `salon` mit Telefon, Anschrift, Impressum und Datenschutz, alle Schritte bestaetigt,
* Bundesland gewaehlt, Preise geprueft, Einbau `inline`, Branche `salon`. Bei jedem Aufruf neue Objekte (neue IDs).
* Mit `abo: { ok: true }` ergibt bestellpruefung() []; Tests aendern einzelne Felder, um genau einen Punkt zu oeffnen.
* @param {{ now?: Date }} [optionen]
* @returns {{ einstellung: object, fragebogen: object }}
*/
export function bestellbarerStand({ now = new Date() } = {}) {
const einstellung = ausVorlage('salon', BETRIEB, now)
const leer = leerFragebogen()
const fragebogen = {
...leer,
branche: 'salon',
bestaetigt: [...SCHRITTE],
bundeslandGewaehlt: true,
preiseGeprueft: true,
einbau: { ...leer.einbau, modus: 'inline' },
}
return { einstellung, fragebogen }
}
/**
* termineConfig-Dokument (Dokument-ID = ID des portalFeatures-Dokuments) fuer aw.seed('termineConfig', …): Einstellung und
* Fragebogen als JSON-Text wie in Produktion. Ohne `stand` der bestellbare Stand; ein Feld, das null/undefined ist, fehlt im Dokument.
* @param {string} featureId
* @param {{ customerId: string, projectId: string, stand?: { einstellung?: object|null, fragebogen?: object|null } }} optionen
*/
export function entwurfDoc(featureId, { customerId, projectId, stand = bestellbarerStand() }) {
const doc = {
$id: featureId,
customerId,
projectId,
einstellung: JSON.stringify(stand.einstellung ?? {}),
version: 1,
updatedAt: '2026-10-04T10:00:00.000Z',
updatedBy: 'test',
}
if (stand.fragebogen !== null && stand.fragebogen !== undefined) doc.fragebogen = JSON.stringify(stand.fragebogen)
return doc
}
+532 -2
View File
@@ -1,9 +1,15 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { VORLAGEN, vorlagenListe } from '../server/services/termine/vorlagen.js'
import { VORLAGEN, vorlagenListe, ausVorlage } from '../server/services/termine/vorlagen.js'
import {
SCHRITTE, BRANCHEN, FRAGEBOGEN_MAX, leerFragebogen, normalisiereFragebogen,
SCHRITTE, SCHRITT_TITEL, BRANCHEN, FRAGEBOGEN_MAX, leerFragebogen, normalisiereFragebogen,
bestellpruefung, gesundheitsWarnungen, bestellungAnsicht,
} from '../server/services/termine/fragebogen.js'
import { MELDUNG_PFLICHT, sichtbareLeistungen } from '../server/services/termine/einstellungen.js'
import { aboMeldung } from '../server/services/featureOrder.js'
import { oeffentlicheAnsicht, featureHaken, BELEHRUNG_PLATZHALTER } from '../server/services/featureRegistry.js'
import { config } from '../server/config.js'
import { bestellbarerStand, entwurfDoc } from './support/fragebogenStand.js'
const norm = normalisiereFragebogen
const ok = (eingabe) => {
@@ -276,3 +282,527 @@ test('normalisiere: groesste gueltige Eingabe bleibt unter FRAGEBOGEN_MAX (auch
assert.ok(Buffer.byteLength(json, 'utf8') < FRAGEBOGEN_MAX, `${JSON.stringify(zeichen)}: ${Buffer.byteLength(json, 'utf8')} Bytes`)
}
})
// ---------------------------------------------------------------- Bestell-Gate (bestellpruefung)
const T = config.features.termine
const ABO_OK = { ok: true }
const pruefe = (stand, extra = {}) => bestellpruefung({ ...stand, feature: T, abo: ABO_OK, ...extra })
const stand = (aendere) => {
const s = bestellbarerStand({ now: new Date('2026-10-04T10:00:00.000Z') })
aendere?.(s)
return s
}
const SIE_FORM = /\b(Sie|Ihnen|Ihr|Ihre|Ihren|Ihrem|Ihrer|Ihres)\b/
const ohnePflichttexte = (texte) => texte.filter((t) => !Object.values(MELDUNG_PFLICHT).includes(t))
test('SCHRITT_TITEL: ein Titel je Schritt und fuer bestellen, unveraenderlich', () => {
assert.deepEqual(Object.keys(SCHRITT_TITEL), [...SCHRITTE, 'bestellen'])
assert.ok(Object.values(SCHRITT_TITEL).every((t) => typeof t === 'string' && t.length > 2))
assert.ok(Object.isFrozen(SCHRITT_TITEL))
assert.equal(new Set(Object.values(SCHRITT_TITEL)).size, SCHRITTE.length + 1, 'Titel eindeutig')
})
test('bestellbarerStand: Vorlage salon, alle Angaben da, bei jedem Aufruf neue Objekte', () => {
const a = bestellbarerStand({ now: new Date('2026-10-04T10:00:00.000Z') })
const b = bestellbarerStand()
assert.equal(a.einstellung.vorlage, 'salon')
assert.equal(a.einstellung.betrieb.telefon, '06301 12345')
assert.ok(a.einstellung.betrieb.adresse && a.einstellung.betrieb.impressumUrl && a.einstellung.betrieb.datenschutzUrl)
assert.deepEqual(a.fragebogen.bestaetigt, SCHRITTE)
assert.equal(a.fragebogen.bundeslandGewaehlt, true)
assert.equal(a.fragebogen.preiseGeprueft, true)
assert.equal(a.fragebogen.einbau.modus, 'inline')
assert.equal(a.fragebogen.branche, 'salon')
assert.notEqual(a.einstellung.ressourcen[0].id, b.einstellung.ressourcen[0].id)
assert.notEqual(a.fragebogen.bestaetigt, b.fragebogen.bestaetigt)
assert.deepEqual(norm(a.fragebogen), { fragebogen: a.fragebogen }, 'der Fragebogen ist normalisiert')
})
test('entwurfDoc: termineConfig-Dokument mit Einstellung und Fragebogen als JSON-Text', () => {
const s = stand()
const doc = entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand: s })
assert.equal(doc.$id, 'f_p1_termine')
assert.equal(doc.customerId, 'c1')
assert.equal(doc.projectId, 'p1')
assert.deepEqual(JSON.parse(doc.einstellung), JSON.parse(JSON.stringify(s.einstellung)))
assert.deepEqual(JSON.parse(doc.fragebogen), s.fragebogen)
assert.ok(Number.isInteger(doc.version))
assert.equal(Object.hasOwn(entwurfDoc('f', { customerId: 'c', projectId: 'p', stand: { einstellung: s.einstellung, fragebogen: null } }), 'fragebogen'), false)
assert.ok(JSON.parse(entwurfDoc('f', { customerId: 'c', projectId: 'p' }).fragebogen).bestaetigt.length === 8, 'ohne stand: der bestellbare')
})
test('bestellpruefung: bestellbarer Stand mit abo { ok: true } ergibt []', () => {
const ergebnis = pruefe(stand())
assert.deepEqual(ergebnis, [])
assert.ok(Array.isArray(ergebnis), 'ein Array, kein Promise')
assert.notEqual(pruefe(stand()), pruefe(stand()), 'bei jedem Aufruf ein neues Array')
})
// Tabelle: Kriterium, Aenderung am bestellbaren Stand, erwarteter Schritt, (optional) erwarteter Text
const GATE_FAELLE = [
['(a) Telefon leer', (s) => { s.einstellung.betrieb.telefon = '' }, 'website', MELDUNG_PFLICHT['betrieb.telefon']],
['(a) Telefon fehlt (null)', (s) => { s.einstellung.betrieb.telefon = null }, 'website', MELDUNG_PFLICHT['betrieb.telefon']],
['(a) Anschrift leer', (s) => { s.einstellung.betrieb.adresse = '' }, 'branche', MELDUNG_PFLICHT['betrieb.adresse']],
['(a) Anschrift nur Leerzeichen', (s) => { s.einstellung.betrieb.adresse = ' ' }, 'branche', MELDUNG_PFLICHT['betrieb.adresse']],
['(b) alle Leistungen inaktiv', (s) => { s.einstellung.leistungen.forEach((l) => { l.aktiv = false }) }, 'leistungen'],
['(b) keine aktive Person', (s) => { s.einstellung.ressourcen.forEach((r) => { r.aktiv = false }) }, 'leistungen'],
['(b) keine Leistung', (s) => { s.einstellung.leistungen = [] }, 'leistungen'],
['(c) Schritt branche nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'branche') }, 'branche', 'Bitte bestätige den Schritt „Branche und Betrieb“.'],
['(c) Schritt wer nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'wer') }, 'wer', 'Bitte bestätige den Schritt „Wer wird gebucht“.'],
['(c) Schritt website nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'website') }, 'website', 'Bitte bestätige den Schritt „Website“.'],
['(c) Preise nicht geprueft', (s) => { s.fragebogen.preiseGeprueft = false }, 'leistungen'],
['(d) Bundesland nicht gewaehlt', (s) => { s.fragebogen.bundeslandGewaehlt = false }, 'zeiten'],
['(d) bundeslandGewaehlt fehlt', (s) => { delete s.fragebogen.bundeslandGewaehlt }, 'zeiten'],
['(d) Wahrheitswert als Text zaehlt nicht', (s) => { s.fragebogen.bundeslandGewaehlt = 'true' }, 'zeiten'],
['(e) einbau.modus fehlt', (s) => { s.fragebogen.einbau.modus = null }, 'website'],
['(e) einbau fehlt ganz', (s) => { delete s.fragebogen.einbau }, 'website'],
['(e) Modus knopf ohne Knopftext', (s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' }, 'website'],
['(e) Modus knopf mit Leerzeichen als Knopftext', (s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = ' ' }, 'website'],
['(f) Branche fehlt', (s) => { s.fragebogen.branche = null }, 'branche'],
['(f) Branche unbekannt', (s) => { s.fragebogen.branche = 'raumschiff' }, 'branche'],
['(f) Branche Gesundheit', (s) => { s.fragebogen.branche = 'gesundheit' }, 'branche'],
['(g) Impressum fehlt', (s) => { s.einstellung.betrieb.impressumUrl = null }, 'website'],
['(g) Datenschutz fehlt', (s) => { s.einstellung.betrieb.datenschutzUrl = null }, 'website'],
['(g) Datenschutz leer', (s) => { s.einstellung.betrieb.datenschutzUrl = '' }, 'website'],
]
for (const [name, aendere, schritt, erwartetText] of GATE_FAELLE) {
test(`Gate ${name}: genau ein Punkt im Schritt ${schritt}`, () => {
const ergebnis = pruefe(stand(aendere))
assert.equal(ergebnis.length, 1, JSON.stringify(ergebnis))
assert.equal(ergebnis[0].schritt, schritt)
assert.ok(typeof ergebnis[0].text === 'string' && ergebnis[0].text.length > 10)
assert.deepEqual(Object.keys(ergebnis[0]), ['schritt', 'text'])
if (erwartetText) assert.equal(ergebnis[0].text, erwartetText)
})
}
test('Gate: Telefon leer ergibt { schritt: website }, Anschrift leer { schritt: branche } (Text aus MELDUNG_PFLICHT)', () => {
assert.deepEqual(pruefe(stand((s) => { s.einstellung.betrieb.telefon = '' })), [{ schritt: 'website', text: MELDUNG_PFLICHT['betrieb.telefon'] }])
assert.deepEqual(pruefe(stand((s) => { s.einstellung.betrieb.adresse = '' })), [{ schritt: 'branche', text: MELDUNG_PFLICHT['betrieb.adresse'] }])
})
test('Gate (a): ohne sichtbare Widerrufsbelehrung braucht es die Anschrift nicht (Unternehmer-Gaeste)', () => {
const ergebnis = pruefe(stand((s) => { s.einstellung.recht.gaesteSind = 'unternehmer'; s.einstellung.betrieb.adresse = '' }))
assert.deepEqual(ergebnis, [])
})
test('Gate (b): sichtbareLeistungen ist dieselbe Funktion wie in der oeffentlichen Sicht (aktiv und mit aktiver Ressource)', () => {
const s = stand()
assert.ok(sichtbareLeistungen(s.einstellung).length >= 1)
s.einstellung.leistungen.forEach((l) => { l.ressourcen = ['r_gibtesnicht'] })
assert.equal(sichtbareLeistungen(s.einstellung).length, 0)
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['leistungen'])
})
test('Gate (c): bestaetigt leer ergibt acht Punkte in Schrittfolge, je Schritt einer', () => {
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = [] }))
assert.equal(ergebnis.length, 8)
assert.deepEqual(ergebnis.map((p) => p.schritt), SCHRITTE)
ergebnis.forEach((p, i) => assert.equal(p.text, `Bitte bestätige den Schritt „${SCHRITT_TITEL[SCHRITTE[i]]}“.`))
})
test('Gate (c): fehlendes bestaetigt (kein Array) zaehlt wie leer', () => {
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = 'alle' }))
assert.deepEqual(ergebnis.map((p) => p.schritt), SCHRITTE)
})
test('Gate (c): Preis-Punkt nur bei sichtbarer Leistung mit Preis; alle Preise keiner ergibt keinen Preis-Punkt', () => {
const s = stand((x) => { x.fragebogen.preiseGeprueft = false })
assert.ok(s.einstellung.leistungen.some((l) => l.preisArt !== 'keiner' && l.preisCent > 0))
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['leistungen'])
const keiner = stand((x) => {
x.fragebogen.preiseGeprueft = false
x.einstellung.leistungen.forEach((l) => { l.preisArt = 'keiner'; l.preisCent = 0 })
})
assert.deepEqual(pruefe(keiner), [])
// Preis nur an einer unsichtbaren (inaktiven) Leistung ⇒ ebenfalls kein Preis-Punkt
const verdeckt = stand((x) => {
x.fragebogen.preiseGeprueft = false
x.einstellung.leistungen.forEach((l, i) => { if (i > 0) { l.preisArt = 'keiner'; l.preisCent = 0 } })
x.einstellung.leistungen[0].aktiv = false
})
assert.deepEqual(pruefe(verdeckt), [])
})
test('Gate (c): unbestaetigte Leistungen und ungepruefte Preise sind zwei eigene Punkte, beide im Schritt leistungen', () => {
const ergebnis = pruefe(stand((s) => {
s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'leistungen')
s.fragebogen.preiseGeprueft = false
}))
assert.equal(ergebnis.length, 2)
assert.deepEqual(ergebnis.map((p) => p.schritt), ['leistungen', 'leistungen'])
assert.notEqual(ergebnis[0].text, ergebnis[1].text)
assert.equal(ergebnis[0].text, 'Bitte bestätige den Schritt „Leistungen“.')
})
test('Gate (d): bundeslandGewaehlt false bei einstellung.bundesland RP ist trotzdem ein Punkt (der Serverstandard zaehlt nicht)', () => {
const s = stand((x) => { x.fragebogen.bundeslandGewaehlt = false })
assert.equal(s.einstellung.bundesland, 'RP')
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['zeiten'])
s.fragebogen.bundeslandGewaehlt = true
assert.deepEqual(pruefe(s), [])
})
test('Gate (e): beide Modi sind in Ordnung, knopf mit Knopftext auch; ein unbekannter Modus nicht', () => {
assert.deepEqual(pruefe(stand((s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = 'Termin buchen' })), [])
assert.deepEqual(pruefe(stand((s) => { s.fragebogen.einbau.modus = 'popup' })).map((p) => p.schritt), ['website'])
})
test('Gate (e): zwei unterschiedliche Texte fuer fehlenden Modus und fehlenden Knopftext', () => {
const ohneModus = pruefe(stand((s) => { s.fragebogen.einbau.modus = null }))[0].text
const ohneText = pruefe(stand((s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' }))[0].text
assert.notEqual(ohneModus, ohneText)
})
test('Gate (f): Gesundheitstext nennt die Funktion aus dem Register (label.name), nicht die Branche "gesundheit" als Schluessel', () => {
const [punkt] = pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }))
assert.equal(punkt.schritt, 'branche')
assert.ok(punkt.text.includes(T.label.name))
assert.ok(punkt.text.includes('Gesundheitsbereich'))
const [anders] = pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }), { feature: { key: 'x', label: { name: 'Zeitplaner' } } })
assert.ok(anders.text.includes('Zeitplaner'))
assert.ok(!anders.text.includes(T.label.name))
// ohne Register-Eintrag faellt der Name auf "Terminplaner" zurueck
assert.ok(pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }), { feature: undefined })[0].text.includes('Terminplaner'))
})
test('Gate (g): beide URLs fehlen ⇒ zwei Punkte (Impressum, Datenschutz) im Schritt website, Impressum zuerst', () => {
const ergebnis = pruefe(stand((s) => { s.einstellung.betrieb.impressumUrl = null; s.einstellung.betrieb.datenschutzUrl = null }))
assert.equal(ergebnis.length, 2)
assert.deepEqual(ergebnis.map((p) => p.schritt), ['website', 'website'])
assert.match(ergebnis[0].text, /Impressum/)
assert.match(ergebnis[1].text, /Datenschutz/)
})
test('Gate (h): abo { ok: false, grund } ergibt einen Punkt in bestellen mit dem Text aus aboMeldung', () => {
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv']) {
const ergebnis = pruefe(stand(), { abo: { ok: false, grund } })
assert.deepEqual(ergebnis, [{ schritt: 'bestellen', text: aboMeldung(T, grund) }], grund)
}
})
test('Gate (h): abo fehlt (Lesefehler) ergibt aboMeldung(feature, "unbekannt"); abo ohne gueltigen Grund ebenso', () => {
const erwartet = [{ schritt: 'bestellen', text: aboMeldung(T, 'unbekannt') }]
assert.deepEqual(pruefe(stand(), { abo: undefined }), erwartet)
assert.deepEqual(pruefe(stand(), { abo: null }), erwartet)
assert.deepEqual(pruefe(stand(), { abo: { ok: false } }), erwartet)
assert.deepEqual(pruefe(stand(), { abo: { ok: false, grund: 'was_anderes' } }), erwartet)
assert.deepEqual(bestellpruefung({ ...stand(), feature: T }), erwartet, 'Standard mitAbo true, abo nicht uebergeben')
})
test('Gate (h): nur abo.ok === true gibt frei (truthy genuegt nicht)', () => {
assert.deepEqual(pruefe(stand(), { abo: { ok: 'ja' } }).map((p) => p.schritt), ['bestellen'])
assert.deepEqual(pruefe(stand(), { abo: { ok: 1 } }).map((p) => p.schritt), ['bestellen'])
})
test('Gate (h): mitAbo false ergibt keinen Abo-Punkt, auch ohne abo und mit abo { ok: false, grund: kein_abo }', () => {
assert.deepEqual(pruefe(stand(), { mitAbo: false, abo: undefined }), [])
assert.deepEqual(pruefe(stand(), { mitAbo: false, abo: { ok: false, grund: 'kein_abo' } }), [])
assert.deepEqual(bestellpruefung({ ...stand(), feature: T, mitAbo: false }), [])
// mitAbo true/undefined fragt weiter; nur ein ausdrueckliches false schaltet ab
assert.equal(pruefe(stand(), { mitAbo: true, abo: { ok: false, grund: 'kein_abo' } }).length, 1)
assert.equal(pruefe(stand(), { mitAbo: undefined, abo: { ok: false, grund: 'kein_abo' } }).length, 1)
})
test('Gate (h): der Abo-Punkt steht immer zuletzt, nach den Punkten der Schritte', () => {
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = [] }), { abo: { ok: false, grund: 'gekuendigt' } })
assert.equal(ergebnis.length, 9)
assert.deepEqual(ergebnis.map((p) => p.schritt), [...SCHRITTE, 'bestellen'])
})
test('Gate: einstellung null ergibt genau einen Punkt branche (auch mit abo, fragebogen null)', () => {
const ergebnis = bestellpruefung({ einstellung: null, fragebogen: null, feature: T, abo: { ok: false, grund: 'kein_abo' } })
assert.equal(ergebnis.length, 1)
assert.equal(ergebnis[0].schritt, 'branche')
assert.equal(typeof ergebnis[0].text, 'string')
assert.deepEqual(bestellpruefung({ einstellung: null, fragebogen: null, feature: T, mitAbo: false }).map((p) => p.schritt), ['branche'])
assert.deepEqual(bestellpruefung({}).map((p) => p.schritt), ['branche'], 'ohne Argumente')
assert.deepEqual(bestellpruefung().map((p) => p.schritt), ['branche'], 'ganz ohne Argument')
assert.deepEqual(bestellpruefung({ einstellung: 'kaputt' }).map((p) => p.schritt), ['branche'], 'kein Objekt')
})
test('Gate: fragebogen null bei vorhandener Einstellung ist ein leerer Fragebogen', () => {
const s = stand()
const ergebnis = pruefe({ einstellung: s.einstellung, fragebogen: null })
const schritte = ergebnis.map((p) => p.schritt)
assert.ok(schritte.filter((x) => SCHRITTE.includes(x)).length >= 8)
assert.deepEqual(ergebnis, pruefe({ einstellung: s.einstellung, fragebogen: leerFragebogen() }))
assert.deepEqual(ergebnis, pruefe({ einstellung: s.einstellung, fragebogen: undefined }))
})
test('Gate: Reihenfolge stabil – nach Schrittfolge, innerhalb eines Schritts in der Reihenfolge (a), (e), (g)', () => {
const kaputt = stand((s) => {
s.einstellung.betrieb.impressumUrl = null
s.einstellung.betrieb.telefon = ''
s.fragebogen.einbau.modus = null
s.einstellung.betrieb.adresse = ''
s.fragebogen.branche = null
s.fragebogen.bundeslandGewaehlt = false
s.fragebogen.preiseGeprueft = false
})
const ergebnis = pruefe(kaputt, { abo: { ok: false, grund: 'nicht_aktiv' } })
const reihenfolge = ergebnis.map((p) => p.schritt)
assert.deepEqual(reihenfolge, ['branche', 'branche', 'leistungen', 'zeiten', 'website', 'website', 'website', 'bestellen'])
const website = ergebnis.filter((p) => p.schritt === 'website').map((p) => p.text)
assert.equal(website[0], MELDUNG_PFLICHT['betrieb.telefon'], '(a) vor (e) vor (g)')
assert.match(website[2], /Impressum/)
assert.equal(ergebnis[0].text, MELDUNG_PFLICHT['betrieb.adresse'], '(a) vor (f) im Schritt branche')
// zweiter Aufruf mit anders aufgebauten Eingaben: dasselbe Ergebnis
assert.deepEqual(pruefe(structuredClone(kaputt), { abo: { ok: false, grund: 'nicht_aktiv' } }), ergebnis)
assert.deepEqual(pruefe(kaputt, { abo: { ok: false, grund: 'nicht_aktiv' } }), ergebnis)
})
test('Gate: reine Funktion – veraendert Eingaben nicht, ohne Gaestedaten und ohne Nebenwirkung', () => {
const s = stand((x) => { x.fragebogen.bestaetigt = ['wer']; x.einstellung.betrieb.telefon = '' })
const vorher = structuredClone(s)
const abo = { ok: false, grund: 'kein_abo' }
pruefe(s, { abo })
assert.deepEqual(s, vorher)
assert.deepEqual(abo, { ok: false, grund: 'kein_abo' })
})
test('Gate: alle Texte in Du-Form (kein Sie/Ihr), ausser den unveraenderten Pflichtangaben-Meldungen aus einstellungen.js', () => {
const alles = new Set()
for (const [, aendere] of GATE_FAELLE) pruefe(stand(aendere)).forEach((p) => alles.add(p.text))
pruefe(stand((s) => { s.fragebogen.bestaetigt = [] })).forEach((p) => alles.add(p.text))
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv', undefined]) pruefe(stand(), { abo: { ok: false, grund } }).forEach((p) => alles.add(p.text))
pruefe({ einstellung: null, fragebogen: null }).forEach((p) => alles.add(p.text))
pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit'; s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' })).forEach((p) => alles.add(p.text))
assert.ok(alles.size > 15, `nur ${alles.size} verschiedene Texte`)
for (const text of ohnePflichttexte([...alles])) {
assert.doesNotMatch(text, SIE_FORM, text)
assert.ok(text.length <= 300, `zu lang: ${text}`)
assert.doesNotMatch(text, /[<>]/, 'kein Markup')
}
// Die zwei Pflichtangaben-Meldungen bleiben wie in einstellungen.js (Gäste sehen "sie" = die Nummer/Anschrift, keine Anrede)
assert.ok(Object.values(MELDUNG_PFLICHT).every((m) => /\bdeines? Betriebs\b/.test(m)))
})
// ---------------------------------------------------------------- Gesundheits-Stichwortpruefung
const mitName = (name) => ({ betrieb: { name: 'Studio' }, leistungen: [{ name, beschreibung: '', aktiv: true, ressourcen: [] }], gastFelder: { zusatz: [] } })
const mitBeschreibung = (beschreibung) => ({ betrieb: { name: 'Studio' }, leistungen: [{ name: 'Termin', beschreibung }], gastFelder: { zusatz: [] } })
const mitLabel = (label) => ({ betrieb: { name: 'Studio' }, leistungen: [], gastFelder: { zusatz: [{ key: 'x', label, typ: 'text', optionen: [] }] } })
const mitOption = (option) => ({ betrieb: { name: 'Studio' }, leistungen: [], gastFelder: { zusatz: [{ key: 'x', label: 'Anliegen', typ: 'auswahl', optionen: ['Sonstiges', option] }] } })
const mitBetrieb = (name) => ({ betrieb: { name }, leistungen: [], gastFelder: { zusatz: [] } })
// Stichwort ⇒ ein Beispielwort, das den Stamm enthaelt (je Stamm der Liste ein Fall)
const STICHWORTE = [
['physio', 'Physiotherapie'], ['therap', 'Therapeutin'], ['arzt', 'Arztbesuch'], ['ärzt', 'Ärztin'],
['zahnarzt', 'Zahnarzt'], ['zahnärzt', 'Zahnärztin'], ['heilpraktik', 'Heilpraktiker'],
['krankengymnast', 'Krankengymnastik'], ['osteopath', 'Osteopathie'], ['diagnos', 'Diagnose'],
['rezept', 'Rezept'], ['beschwerde', 'Beschwerden'], ['behandlung', 'Behandlung'],
]
test('Stichwortpruefung: jedes der 13 Stichworte trifft in Leistungsname, Beschreibung, Zusatzfeld-Beschriftung, Auswahl und Betriebsname', () => {
assert.equal(STICHWORTE.length, 13)
for (const [stamm, wort] of STICHWORTE) {
for (const [ort, einstellung] of [['Name', mitName(wort)], ['Beschreibung', mitBeschreibung(`Termin wegen ${wort} möglich`)], ['Beschriftung', mitLabel(wort)], ['Auswahl', mitOption(wort)], ['Betrieb', mitBetrieb(`${wort} Müller`)]]) {
const treffer = gesundheitsWarnungen(einstellung)
assert.equal(treffer.length, 1, `${stamm} (${wort}) in ${ort}: ${JSON.stringify(treffer)}`)
assert.ok(treffer[0].includes(wort), `${wort} steht in der Meldung: ${treffer[0]}`)
}
}
})
test('Stichwortpruefung: Gross-/Kleinschreibung und Umlaute (auch zerlegtes ä)', () => {
for (const wort of ['PHYSIO', 'physio', 'PhysioTherapie', 'ÄRZTIN', 'ärztin', 'Ärztin', 'ZAHNÄRZTIN', 'zahnärztin', 'HEILPRAKTIKERIN', 'REZEPT', 'BEHANDLUNGEN']) {
assert.equal(gesundheitsWarnungen(mitName(wort)).length, 1, wort)
}
assert.equal(gesundheitsWarnungen(mitName('Arztpraxis')).length, 1)
})
test('Regeltest: Treffer nur am Wortanfang; Gruppe 2 nur mit hoechstens einer Beugungsendung', () => {
const treffer = ['Rezept', 'Rezepte', 'Behandlungen', 'Beschwerden', 'Physiotherapie', 'Therapiestunde', 'Zahnärztin', 'Krankengymnastik', 'Osteopathie']
const keine = ['Rezeption', 'Rezeptur', 'Gesichtsbehandlung', 'Beratung', 'Besichtigung']
for (const wort of treffer) assert.equal(gesundheitsWarnungen(mitName(wort)).length, 1, `Treffer erwartet: ${wort}`)
for (const wort of keine) assert.deepEqual(gesundheitsWarnungen(mitName(wort)), [], `kein Treffer erwartet: ${wort}`)
// weitere Randfaelle der Regel
for (const wort of ['Rezepts', 'Rezeptes', 'Behandlung', 'Behandlungs', 'Beschwerde', 'Beschwerdes', 'Beschwerdenn']) {
assert.equal(gesundheitsWarnungen(mitName(wort)).length, wort === 'Beschwerdenn' ? 0 : 1, wort)
}
for (const wort of ['Behandlungsraum', 'Rezepturen', 'Beschwerdemanagement', 'Hausarzt', 'Kinderphysio', 'Lagerdiagnos', 'Wasserrezeption']) {
// "Hausarzt", "Kinderphysio", "Lagerdiagnos": kein Buchstabe vor dem Treffer erlaubt – Treffer nur am Wortanfang
assert.deepEqual(gesundheitsWarnungen(mitName(wort)), [], wort)
}
})
test('Regeltest: Wortanfang auch nach Bindestrich, Leerzeichen, Ziffer und Satzzeichen (kein Buchstabe davor)', () => {
for (const text of ['Kinder-Physiotherapie', 'Termin (Rezept abholen)', '3x Therapie', 'Beratung/Behandlung', 'a, Rezepte', 'Ärztliche Beratung']) {
assert.equal(gesundheitsWarnungen(mitName(text)).length, 1, text)
}
assert.deepEqual(gesundheitsWarnungen(mitName('Gesichtsbehandlung und Beratung, Besichtigung')), [])
})
test('Stichwortpruefung: alle neun Vorlagen ergeben [] (schuetzt Gesichtsbehandlung, Beratung, Besichtigung)', () => {
const keys = Object.keys(VORLAGEN)
assert.equal(keys.length, 9)
for (const key of keys) {
const einstellung = ausVorlage(key, { name: 'Muster GmbH', email: 'a@example.invalid', telefon: '1', adresse: 'Hauptstr. 1' }, new Date('2026-10-04T10:00:00.000Z'))
assert.deepEqual(gesundheitsWarnungen(einstellung), [], key)
}
})
test('Stichwortpruefung: null und leere Einstellung ergeben []', () => {
assert.deepEqual(gesundheitsWarnungen(null), [])
assert.deepEqual(gesundheitsWarnungen(undefined), [])
assert.deepEqual(gesundheitsWarnungen({}), [])
assert.deepEqual(gesundheitsWarnungen('Physiotherapie'), [])
assert.deepEqual(gesundheitsWarnungen({ betrieb: null, leistungen: 'x', gastFelder: { zusatz: [null, 5, { label: 7, optionen: 'a' }] } }), [])
})
test('Stichwortpruefung: nur Betriebsname, Leistungen und Zusatzfelder werden gelesen – nie E-Mail, Telefon, Anschrift, Storno, Gaeste', () => {
const e = {
betrieb: { name: 'Studio Nord', email: 'physio@example.invalid', telefon: 'Arzt-Notruf', adresse: 'Therapiestraße 1', impressumUrl: 'https://rezept.example.invalid' },
storno: { text: 'Behandlung nur nach Diagnose' },
leistungen: [{ name: 'Haarschnitt', beschreibung: 'Kurz und gut' }],
gastFelder: { telefon: 'pflicht', nachricht: 'optional', zusatz: [] },
}
assert.deepEqual(gesundheitsWarnungen(e), [])
})
test('Stichwortpruefung: ohne Doppelte (gleiche Meldung nur einmal) und hoechstens 10 Meldungen', () => {
const doppelt = {
betrieb: { name: 'Studio' },
leistungen: [{ name: 'Physiotherapie' }, { name: 'Physiotherapie' }, { name: 'physiotherapie' }],
gastFelder: { zusatz: [] },
}
const treffer = gesundheitsWarnungen(doppelt)
assert.equal(new Set(treffer).size, treffer.length, 'keine Doppelten')
assert.ok(treffer.length >= 1 && treffer.length <= 2, JSON.stringify(treffer))
const viele = { betrieb: { name: 'Studio' }, leistungen: Array.from({ length: 25 }, (_, i) => ({ name: `Therapie${String.fromCharCode(97 + i)}` })), gastFelder: { zusatz: [] } }
const alle = gesundheitsWarnungen(viele)
assert.equal(alle.length, 10)
assert.equal(new Set(alle).size, 10)
// zusammen mit Zusatzfeldern ebenfalls gedeckelt
const gemischt = { ...viele, gastFelder: { zusatz: Array.from({ length: 5 }, (_, i) => ({ label: `Rezept${i}`, optionen: ['Diagnose', 'Arzt'] })) } }
assert.ok(gesundheitsWarnungen(gemischt).length <= 10)
})
test('Stichwortpruefung: Meldungen sind Klartext (Strings), nennen den Fundort und das Wort, kein Markup, begrenzte Laenge', () => {
const langesWort = `Physio${'x'.repeat(200)}`
const treffer = gesundheitsWarnungen({ betrieb: { name: '<b>Praxis</b> Therapie' }, leistungen: [{ name: langesWort, beschreibung: 'Rezept <script>' }], gastFelder: { zusatz: [{ label: 'Diagnose?', optionen: ['Beschwerden', 'Nein'] }] } })
assert.ok(treffer.length >= 4)
for (const t of treffer) {
assert.equal(typeof t, 'string')
assert.ok(t.length <= 200, `${t.length}: ${t}`)
assert.doesNotMatch(t, /[<>]/, 'Markup bleibt aussen vor')
}
assert.ok(treffer.some((t) => /Betriebsname/.test(t)))
assert.ok(treffer.some((t) => /Leistung/.test(t)))
assert.ok(treffer.some((t) => /Zusatzfeld/.test(t)))
})
test('Stichwortpruefung: reine Funktion – Eingabe unveraendert, bei jedem Aufruf ein neues Array', () => {
const e = mitName('Physiotherapie')
const vorher = structuredClone(e)
const a = gesundheitsWarnungen(e)
assert.deepEqual(e, vorher)
assert.notEqual(a, gesundheitsWarnungen(e))
assert.deepEqual(a, gesundheitsWarnungen(e))
})
// ---------------------------------------------------------------- bestellungAnsicht
test('bestellungAnsicht: Abbildung von oeffentlicheAnsicht – Preis, Hinweis, Einrichtung, Zustimmungen unveraendert', async () => {
const ansicht = await bestellungAnsicht(T)
const reg = oeffentlicheAnsicht('termine')
assert.deepEqual(Object.keys(ansicht).sort(), ['belehrung', 'consent', 'dokumente', 'einrichtung', 'preis', 'preisHinweis'])
assert.equal(ansicht.preis, reg.priceLabel)
assert.equal(ansicht.preis, '149 €')
assert.equal(ansicht.preisHinweis, config.ustHinweis)
assert.equal(ansicht.einrichtung, reg.setupEstimate)
assert.deepEqual(ansicht.consent, reg.consent)
assert.equal(ansicht.consent.knopf, T.consent.knopf, 'der Knopf wörtlich aus dem Register')
assert.deepEqual(ansicht.consent.items, T.consent.items.map((i) => ({ id: i.id, text: i.text, haken: i.haken === true })))
assert.ok(ansicht.consent.items.every((i) => typeof i.haken === 'boolean'), 'haken-Flag je Eintrag')
assert.equal(ansicht.consent.version, T.consent.version)
})
test('bestellungAnsicht: belehrung ist das Ergebnis des Hakens (derselbe Text, den der Nachweis speichert)', async () => {
const ansicht = await bestellungAnsicht(T)
assert.equal(ansicht.belehrung, await featureHaken('termine').belehrung())
assert.equal(ansicht.belehrung, BELEHRUNG_PLATZHALTER)
// ein Test-Haken im Register ueberschreibt den Standard
T.belehrung = async () => 'Belehrung Test'
try {
assert.equal((await bestellungAnsicht(T)).belehrung, 'Belehrung Test')
} finally {
delete T.belehrung
}
})
test('bestellungAnsicht: dokumente nur mit gesetzter url, mit id, titel, version und url', async () => {
const merke = { lb: T.leistungsbeschreibung, avv: T.avv, widerruf: T.widerruf }
try {
T.leistungsbeschreibung = { version: '2026-10-05', url: '/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html' }
T.avv = { version: '2026-10-05', url: '' }
T.widerruf = null
let ansicht = await bestellungAnsicht(T)
assert.deepEqual(ansicht.dokumente, [{ id: 'leistungsbeschreibung', titel: 'Leistungsbeschreibung', version: '2026-10-05', url: '/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html' }])
T.avv = { version: '2026-10-05', url: '/rechtliches/avv-terminplaner-2026-10-05.html' }
T.widerruf = { version: '2026-10-05', url: '/rechtliches/widerruf-terminplaner-2026-10-05.html' }
ansicht = await bestellungAnsicht(T)
assert.deepEqual(ansicht.dokumente.map((d) => d.id), ['leistungsbeschreibung', 'avv', 'widerruf'])
for (const d of ansicht.dokumente) {
assert.deepEqual(Object.keys(d), ['id', 'titel', 'version', 'url'])
assert.ok(d.titel.length > 3 && d.url.startsWith('/rechtliches/') && d.version === '2026-10-05')
}
assert.ok(!('pflichtinfo' in ansicht) && !('widerruf' in ansicht), 'keine Felder pflichtinfo/widerruf')
} finally {
T.leistungsbeschreibung = merke.lb
T.avv = merke.avv
T.widerruf = merke.widerruf
}
})
test('bestellungAnsicht: Register ohne Rechtsunterlagen (Entwurf, url leer) ergibt eine leere Liste', async () => {
const merke = { lb: T.leistungsbeschreibung, avv: T.avv, widerruf: T.widerruf }
try {
T.leistungsbeschreibung = { version: 'ENTWURF', url: '' }
T.avv = { version: 'ENTWURF' }
T.widerruf = null
assert.deepEqual((await bestellungAnsicht(T)).dokumente, [])
} finally {
T.leistungsbeschreibung = merke.lb
T.avv = merke.avv
T.widerruf = merke.widerruf
}
})
test('bestellungAnsicht: unbekannte Funktion ergibt null, ebenso fehlender Eintrag', async () => {
assert.equal(await bestellungAnsicht({ key: 'analyse' }), null)
assert.equal(await bestellungAnsicht({ key: '__proto__' }), null)
assert.equal(await bestellungAnsicht({ key: 'nope', label: { name: 'x' } }), null)
assert.equal(await bestellungAnsicht(null), null)
assert.equal(await bestellungAnsicht(undefined), null)
assert.equal(await bestellungAnsicht({}), null)
})
test('bestellungAnsicht: enthaelt nie interne Felder (ticket, mails, Funktionen) und ist JSON-sicher', async () => {
const ansicht = await bestellungAnsicht(T)
const json = JSON.stringify(ansicht)
for (const verboten of ['ticket', 'mails', 'topic', 'details', 'formen', 'returnHash', 'product', 'requiresWartungsAbo', 'disabledMessage']) {
assert.ok(!json.includes(`"${verboten}"`), `kein Feld ${verboten}`)
}
assert.deepEqual(JSON.parse(json), ansicht)
// eine Kopie: Aenderungen an der Ansicht aendern das Register nicht
ansicht.consent.items[0].text = 'geaendert'
ansicht.consent.knopf = 'geaendert'
assert.notEqual(T.consent.items[0].text, 'geaendert')
assert.notEqual(T.consent.knopf, 'geaendert')
})
test('bestellungAnsicht: News (ohne belehrung-Haken) ergibt belehrung null und den News-Preis', async () => {
const ansicht = await bestellungAnsicht(config.features.news)
assert.equal(ansicht.belehrung, null)
assert.equal(ansicht.preis, config.features.news.priceLabel)
})
test('Der Stub ist weg: keine Marke STUB im Kopf von fragebogen.js', async () => {
const { readFile } = await import('node:fs/promises')
const quelle = await readFile(new URL('../server/services/termine/fragebogen.js', import.meta.url), 'utf8')
assert.doesNotMatch(quelle, /STUB/)
})
-26
View File
@@ -1,26 +0,0 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
// Platzhalter der Bestellpruefung (server/services/termine/fragebogen.js, Block B): bestellbar ist alles, bis
// Block C die echte Pruefung liefert. Block C loescht diese Datei zusammen mit dem Stub (Task C2).
const { bestellpruefung } = await import('../server/services/termine/fragebogen.js')
test('Stub: bestellpruefung liefert [] fuer jede Eingabe, mit und ohne Abo-Punkt', () => {
assert.deepEqual(bestellpruefung({ einstellung: null, fragebogen: null, feature: { key: 'termine' } }), [])
assert.deepEqual(bestellpruefung({ einstellung: {}, fragebogen: {}, feature: {}, abo: { ok: false, grund: 'kein_abo' } }), [])
assert.deepEqual(bestellpruefung({ einstellung: {}, fragebogen: {}, feature: {}, mitAbo: false }), [])
assert.deepEqual(bestellpruefung({}), [])
assert.ok(Array.isArray(bestellpruefung({})), 'ein Array, kein Promise')
})
test('Stub: jeder Aufruf liefert ein neues Array (Aufrufer duerfen es veraendern)', () => {
const eins = bestellpruefung({})
eins.push({ schritt: 'x', text: 'y' })
assert.deepEqual(bestellpruefung({}), [])
})
test('Stub traegt die Marke im Kopfkommentar (Block C ersetzt die Datei, Abschlusspruefung Block B zaehlt sie)', async () => {
const quelle = await readFile(new URL('../server/services/termine/fragebogen.js', import.meta.url), 'utf8')
assert.match(quelle, /^\/\/ STUB \(Block B\) – ersetzt Task C/)
})