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`) } })