Files
Webklar-Kundenbereich/test/termineFragebogen.test.js
T
ClaudeandClaude Sonnet 5.5 ab47fabcb2 Terminplaner TP2: Fragebogen-Attribut, Branchen, Schritte, Normalisierung
termineConfig bekommt das Attribut fragebogen (Text 20000, Inhalt bis
FRAGEBOGEN_MAX 16000). fragebogen.js: SCHRITTE, BRANCHEN (Vorlagen plus
Gesundheitsbereich), leerFragebogen und normalisiereFragebogen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:49:06 +00:00

279 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { VORLAGEN, vorlagenListe } from '../server/services/termine/vorlagen.js'
import {
SCHRITTE, BRANCHEN, FRAGEBOGEN_MAX, leerFragebogen, normalisiereFragebogen,
} from '../server/services/termine/fragebogen.js'
const norm = normalisiereFragebogen
const ok = (eingabe) => {
const ergebnis = norm(eingabe)
assert.equal(ergebnis.fehler, undefined, `unerwarteter Fehler: ${JSON.stringify(ergebnis.fehler)}`)
return ergebnis.fragebogen
}
// ---------------------------------------------------------------- SCHRITTE und BRANCHEN
test('SCHRITTE: genau die acht Schritte in fester Reihenfolge, unveraenderlich', () => {
assert.deepEqual(SCHRITTE, ['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'])
assert.ok(Object.isFrozen(SCHRITTE))
})
test('BRANCHEN: Reihenfolge von vorlagenListe(), danach genau der Eintrag Gesundheitsbereich', () => {
const liste = vorlagenListe()
assert.equal(BRANCHEN.length, liste.length + 1)
BRANCHEN.slice(0, liste.length).forEach((branche, i) => {
assert.deepEqual(branche, { key: liste[i].key, name: liste[i].name, vorlage: liste[i].key, gesundheit: false })
})
assert.deepEqual(BRANCHEN.at(-1), { key: 'gesundheit', name: 'Gesundheitsbereich', vorlage: null, gesundheit: true })
})
test('BRANCHEN: jede Vorlage steht in VORLAGEN, alle neun Vorlagen kommen genau einmal vor', () => {
const mitVorlage = BRANCHEN.filter((b) => b.vorlage !== null)
assert.ok(mitVorlage.every((b) => Object.hasOwn(VORLAGEN, b.vorlage)))
assert.equal(Object.keys(VORLAGEN).length, 9)
assert.deepEqual(mitVorlage.map((b) => b.vorlage).sort(), Object.keys(VORLAGEN).sort())
assert.equal(new Set(mitVorlage.map((b) => b.vorlage)).size, 9)
assert.equal(new Set(BRANCHEN.map((b) => b.key)).size, BRANCHEN.length, 'Schluessel eindeutig')
})
test('BRANCHEN: genau ein Eintrag mit gesundheit true und ohne Vorlage; sonstiges bleibt waehlbar (D6)', () => {
const gesundheit = BRANCHEN.filter((b) => b.gesundheit)
assert.equal(gesundheit.length, 1)
assert.equal(gesundheit[0].vorlage, null)
assert.equal(gesundheit[0].key, 'gesundheit')
assert.equal(gesundheit[0].name, 'Gesundheitsbereich')
assert.ok(BRANCHEN.filter((b) => b.vorlage === null).every((b) => b.gesundheit), 'ohne Vorlage nur der Gesundheitsbereich')
const sonstiges = BRANCHEN.find((b) => b.key === 'sonstiges')
assert.deepEqual(sonstiges, { key: 'sonstiges', name: VORLAGEN.sonstiges.name, vorlage: 'sonstiges', gesundheit: false })
assert.equal(ok({ branche: 'sonstiges' }).branche, 'sonstiges')
})
test('BRANCHEN: jeder Eintrag hat genau die vier Felder, die Liste ist unveraenderlich', () => {
for (const branche of BRANCHEN) {
assert.deepEqual(Object.keys(branche), ['key', 'name', 'vorlage', 'gesundheit'])
assert.ok(typeof branche.name === 'string' && branche.name.length > 2)
assert.equal(typeof branche.gesundheit, 'boolean')
}
assert.ok(Object.isFrozen(BRANCHEN))
assert.ok(BRANCHEN.every((b) => Object.isFrozen(b)))
})
test('FRAGEBOGEN_MAX ist 16000', () => {
assert.equal(FRAGEBOGEN_MAX, 16000)
})
// ---------------------------------------------------------------- leerFragebogen
test('leerFragebogen: Grundform, bei jedem Aufruf ein neues Objekt', () => {
assert.deepEqual(leerFragebogen(), {
schema: 1,
branche: null,
bestaetigt: [],
bundeslandGewaehlt: false,
preiseGeprueft: false,
einbau: { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: '' },
})
const a = leerFragebogen()
a.bestaetigt.push('wer')
a.einbau.stelle = 'x'
assert.deepEqual(leerFragebogen().bestaetigt, [])
assert.equal(leerFragebogen().einbau.stelle, '')
})
// ---------------------------------------------------------------- normalisiereFragebogen
test('normalisiere: {} ergibt leerFragebogen()', () => {
assert.deepEqual(norm({}), { fragebogen: leerFragebogen() })
})
test('normalisiere: Nicht-Objekt ergibt fehler._ und kein fragebogen', () => {
for (const eingabe of [null, undefined, 'text', 42, true, [], [{}]]) {
const ergebnis = norm(eingabe)
assert.equal(typeof ergebnis.fehler._, 'string', `Eingabe ${JSON.stringify(eingabe)}`)
assert.deepEqual(Object.keys(ergebnis.fehler), ['_'])
assert.equal('fragebogen' in ergebnis, false)
}
})
test('normalisiere: unbekannte Schluessel und __proto__ fliegen raus, ohne den Prototyp anzufassen', () => {
const eingabe = JSON.parse(
'{"__proto__":{"vergiftet":1},"fremd":"x","constructor":{"prototype":{"vergiftet":2}},'
+ '"einbau":{"__proto__":{"vergiftet":3},"fremd":"y","stelle":"Startseite"}}',
)
const fragebogen = ok(eingabe)
assert.deepEqual(fragebogen, { ...leerFragebogen(), einbau: { ...leerFragebogen().einbau, stelle: 'Startseite' } })
assert.deepEqual(Object.keys(fragebogen), ['schema', 'branche', 'bestaetigt', 'bundeslandGewaehlt', 'preiseGeprueft', 'einbau'])
assert.deepEqual(Object.keys(fragebogen.einbau), ['modus', 'knopfText', 'stelle', 'gestaltung', 'bemerkung'])
assert.equal(Object.hasOwn(fragebogen, '__proto__'), false)
assert.equal(Object.getPrototypeOf(fragebogen), Object.prototype)
assert.equal({}.vergiftet, undefined)
assert.equal(fragebogen.vergiftet, undefined)
})
test('normalisiere: schema ist immer 1, egal was kommt', () => {
assert.equal(ok({ schema: 7 }).schema, 1)
assert.equal(ok({ schema: 'x' }).schema, 1)
})
test('normalisiere: bestaetigt in Reihenfolge der Schritte, ohne Doppelte und Fremde', () => {
assert.deepEqual(ok({ bestaetigt: ['website', 'branche', 'branche', 'quatsch'] }).bestaetigt, ['branche', 'website'])
assert.deepEqual(ok({ bestaetigt: [...SCHRITTE].reverse() }).bestaetigt, SCHRITTE)
assert.deepEqual(ok({ bestaetigt: [1, null, {}, ['wer'], 'wer'] }).bestaetigt, ['wer'])
assert.deepEqual(ok({ bestaetigt: [] }).bestaetigt, [])
assert.deepEqual(ok({ bestaetigt: null }).bestaetigt, [])
assert.deepEqual(ok({ bestaetigt: ['__proto__', 'constructor', 'toString'] }).bestaetigt, [])
})
test('normalisiere: bestaetigt, das keine Liste ist, ergibt fehler.bestaetigt', () => {
for (const wert of ['branche', 5, true, { 0: 'branche' }]) {
const ergebnis = norm({ bestaetigt: wert })
assert.deepEqual(Object.keys(ergebnis.fehler), ['bestaetigt'], JSON.stringify(wert))
}
})
test('normalisiere: branche nur aus BRANCHEN oder null', () => {
for (const branche of BRANCHEN) assert.equal(ok({ branche: branche.key }).branche, branche.key)
assert.equal(ok({ branche: 'gesundheit' }).branche, 'gesundheit')
assert.equal(ok({ branche: null }).branche, null)
assert.equal(ok({}).branche, null)
for (const wert of ['zahnarzt', '', 'Salon', 'SALON', 'constructor', '__proto__', 7, ['salon'], {}]) {
assert.deepEqual(Object.keys(norm({ branche: wert }).fehler ?? {}), ['branche'], JSON.stringify(wert))
}
})
test('normalisiere: knopfText mit 41 Zeichen wird auf 40 gekuerzt, einzeilig', () => {
assert.equal(ok({ einbau: { knopfText: 'x'.repeat(41) } }).einbau.knopfText, 'x'.repeat(40))
assert.equal(ok({ einbau: { knopfText: 'x'.repeat(40) } }).einbau.knopfText, 'x'.repeat(40))
assert.equal(ok({ einbau: { knopfText: '😀'.repeat(41) } }).einbau.knopfText, '😀'.repeat(40), 'Codepoints, nicht UTF-16-Einheiten')
assert.equal(ok({ einbau: { knopfText: ' Jetzt\nTermin\tbuchen ' } }).einbau.knopfText, 'Jetzt Termin buchen')
})
test('normalisiere: Textlaengen stelle 300, gestaltung 1000, bemerkung 1000', () => {
const einbau = ok({ einbau: { stelle: 'a'.repeat(301), gestaltung: 'b'.repeat(1001), bemerkung: 'c'.repeat(1001) } }).einbau
assert.equal(einbau.stelle, 'a'.repeat(300))
assert.equal(einbau.gestaltung, 'b'.repeat(1000))
assert.equal(einbau.bemerkung, 'c'.repeat(1000))
})
test('normalisiere: Steuerzeichen verschwinden, Absaetze bleiben (stelle, gestaltung, bemerkung)', () => {
const roh = 'Farbe\u0000 dunkel‮, rund\r\n\r\nZweiter Absatz​\n\n\n\nDritter'
const erwartet = 'Farbe dunkel, rund\n\nZweiter Absatz\n\nDritter'
const einbau = ok({ einbau: { stelle: roh, gestaltung: roh, bemerkung: roh } }).einbau
assert.equal(einbau.gestaltung, erwartet)
assert.equal(einbau.stelle, erwartet)
assert.equal(einbau.bemerkung, erwartet)
assert.ok(!/[\u0000‮​\r]/.test(einbau.gestaltung))
})
test('normalisiere: Texte werden getrimmt, ein Text nur aus Steuerzeichen wird leer', () => {
const einbau = ok({ einbau: { knopfText: '\u0000‮', stelle: ' ', gestaltung: '\n\n', bemerkung: ' x ' } }).einbau
assert.deepEqual(einbau, { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: 'x' })
})
test('normalisiere: Textfelder, die keine Zeichenkette sind, ergeben einen Fehler am Pfad', () => {
for (const feld of ['knopfText', 'stelle', 'gestaltung', 'bemerkung']) {
for (const wert of [5, true, ['a'], {}]) {
assert.deepEqual(Object.keys(norm({ einbau: { [feld]: wert } }).fehler ?? {}), [`einbau.${feld}`], `${feld}=${JSON.stringify(wert)}`)
}
assert.equal(ok({ einbau: { [feld]: null } }).einbau[feld], '')
}
})
test('normalisiere: einbau.modus nur inline, knopf oder null; popup ergibt fehler[einbau.modus]', () => {
assert.equal(ok({ einbau: { modus: 'inline' } }).einbau.modus, 'inline')
assert.equal(ok({ einbau: { modus: 'knopf' } }).einbau.modus, 'knopf')
assert.equal(ok({ einbau: { modus: null } }).einbau.modus, null)
assert.equal(ok({ einbau: {} }).einbau.modus, null)
const ergebnis = norm({ einbau: { modus: 'popup' } })
assert.deepEqual(Object.keys(ergebnis.fehler), ['einbau.modus'])
assert.equal(typeof ergebnis.fehler['einbau.modus'], 'string')
assert.equal('fragebogen' in ergebnis, false)
for (const wert of ['', 'INLINE', 1, ['inline'], {}]) {
assert.deepEqual(Object.keys(norm({ einbau: { modus: wert } }).fehler ?? {}), ['einbau.modus'], JSON.stringify(wert))
}
})
test('normalisiere: einbau, das kein Objekt ist, ergibt fehler.einbau', () => {
for (const wert of ['inline', 5, [], true]) {
assert.deepEqual(Object.keys(norm({ einbau: wert }).fehler ?? {}), ['einbau'], JSON.stringify(wert))
}
assert.deepEqual(ok({ einbau: null }).einbau, leerFragebogen().einbau)
})
test('normalisiere: bundeslandGewaehlt und preiseGeprueft nur als Wahrheitswert; ja ergibt Fehler', () => {
assert.equal(ok({ bundeslandGewaehlt: true }).bundeslandGewaehlt, true)
assert.equal(ok({ preiseGeprueft: true }).preiseGeprueft, true)
assert.equal(ok({ bundeslandGewaehlt: false, preiseGeprueft: false }).bundeslandGewaehlt, false)
assert.equal(ok({ bundeslandGewaehlt: null }).bundeslandGewaehlt, false)
assert.deepEqual(Object.keys(norm({ bundeslandGewaehlt: 'ja' }).fehler), ['bundeslandGewaehlt'])
assert.deepEqual(Object.keys(norm({ preiseGeprueft: 1 }).fehler), ['preiseGeprueft'])
for (const wert of ['true', 'ja', 0, 1, [], {}]) {
assert.deepEqual(Object.keys(norm({ bundeslandGewaehlt: wert }).fehler ?? {}), ['bundeslandGewaehlt'], JSON.stringify(wert))
}
})
test('normalisiere: mehrere Fehler zugleich, jeder am eigenen Pfad', () => {
const ergebnis = norm({ branche: 'x', bestaetigt: 3, bundeslandGewaehlt: 'ja', preiseGeprueft: 'nein', einbau: { modus: 'popup', stelle: 4 } })
assert.deepEqual(Object.keys(ergebnis.fehler).sort(), ['bestaetigt', 'branche', 'bundeslandGewaehlt', 'einbau.modus', 'einbau.stelle', 'preiseGeprueft'])
assert.ok(Object.values(ergebnis.fehler).every((text) => typeof text === 'string' && text.length > 5))
})
test('normalisiere: das Ergebnis teilt nichts mit der Eingabe', () => {
const eingabe = { bestaetigt: ['wer'], einbau: { stelle: 'oben' } }
const fragebogen = ok(eingabe)
eingabe.bestaetigt.push('zeiten')
eingabe.einbau.stelle = 'unten'
assert.deepEqual(fragebogen.bestaetigt, ['wer'])
assert.equal(fragebogen.einbau.stelle, 'oben')
assert.notEqual(fragebogen.einbau, eingabe.einbau)
})
test('normalisiere: voll ausgefuellter Fragebogen bleibt unveraendert', () => {
const voll = {
schema: 1,
branche: 'salon',
bestaetigt: ['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'],
bundeslandGewaehlt: true,
preiseGeprueft: true,
einbau: { modus: 'knopf', knopfText: 'Termin buchen', stelle: 'Unterseite Kontakt', gestaltung: 'Lila Knopf\n\nRund', bemerkung: 'Bitte vor 10 Uhr' },
}
assert.deepEqual(norm(voll), { fragebogen: voll })
})
test('normalisiere: idempotent', () => {
const eingaben = [
{},
{ branche: 'gesundheit', bestaetigt: ['website', 'wer', 'wer', 'x'], einbau: { modus: 'inline', knopfText: 'y'.repeat(60), stelle: ' a\u0000\n\n\n\nb ' } },
{ schema: 3, fremd: 1, bundeslandGewaehlt: true, einbau: { gestaltung: '😀'.repeat(1200), bemerkung: '\r\nzwei\r\n\r\nAbsätze‮' } },
JSON.parse('{"__proto__":{"a":1},"branche":"werkstatt"}'),
]
for (const eingabe of eingaben) {
const einmal = ok(eingabe)
assert.deepEqual(norm(einmal), { fragebogen: einmal })
}
})
test('normalisiere: groesste gueltige Eingabe bleibt unter FRAGEBOGEN_MAX (auch mit Zeichen, die JSON verdoppelt)', () => {
// Anfuehrungszeichen werden als \" gespeichert, Absatzumbrueche als \n\n: ungünstigster Fall fuer die Zeichenzahl
const gross = (zeichen, max) => zeichen.repeat(max)
for (const zeichen of ['"', '\\', '😀', 'ä', 'a\n\n']) {
const fragebogen = ok({
branche: 'werkstatt',
bestaetigt: SCHRITTE,
bundeslandGewaehlt: true,
preiseGeprueft: true,
einbau: {
modus: 'knopf',
knopfText: gross(zeichen, 40),
stelle: gross(zeichen, 300),
gestaltung: gross(zeichen, 1000),
bemerkung: gross(zeichen, 1000),
},
})
const json = JSON.stringify(fragebogen)
assert.ok(json.length < FRAGEBOGEN_MAX, `${JSON.stringify(zeichen)}: ${json.length} Zeichen`)
assert.ok(Buffer.byteLength(json, 'utf8') < FRAGEBOGEN_MAX, `${JSON.stringify(zeichen)}: ${Buffer.byteLength(json, 'utf8')} Bytes`)
}
})