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>
This commit is contained in:
1 parent
36b36bcc4d
commit
ab47fabcb2
4 files changed
+416
-1
No files matched your search
@@ -1,4 +1,113 @@
|
||||
// STUB (Block B) – ersetzt Task C2 (bestellpruefung); Task C1 legt in dieser Datei den Rest des Fragebogen-Dienstes an
|
||||
import { text } from './einstellungen.js'
|
||||
import { vorlagenListe } from './vorlagen.js'
|
||||
|
||||
/**
|
||||
* Bestell-Fragebogen des Terminplaners (TP2, Spezifikation 2026-10-03-terminplaner-tp2-verkauf-design.md): Der Kunde
|
||||
* richtet den Terminplaner vor der Bestellung ein; der Entwurf steht in termineConfig.einstellung, der Fragebogen
|
||||
* (Branche, bestaetigte Schritte, Wuensche zum Einbau) im Attribut termineConfig.fragebogen als JSON-Text.
|
||||
* Dieser Teil: Schritte, Branchen, Grundform und Normalisierung. Reine Funktionen ohne Appwrite und ohne Uhr.
|
||||
* Fehler sind { [pfad]: text } wie bei normalisiereEinstellung (Pfad '_' = das Ganze); Texte in Du-Form.
|
||||
*/
|
||||
|
||||
/** Die acht Schritte des Fragebogens in fester Reihenfolge; `bestaetigt` nennt die vom Kunden abgehakten. */
|
||||
export const SCHRITTE = Object.freeze(['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'])
|
||||
|
||||
/**
|
||||
* Branchen zur Auswahl: alle Vorlagen in der Reihenfolge von vorlagenListe(), danach der Gesundheitsbereich
|
||||
* (ohne Vorlage; er fuehrt in den Hinweis- und Pruefweg fuer Gesundheitsangaben). Schluessel und Name der letzten
|
||||
* Branche gelten woertlich fuer Bestellpruefung, Oberflaeche, Einbauanleitung und Ticket.
|
||||
*/
|
||||
export const BRANCHEN = Object.freeze([
|
||||
...vorlagenListe().map(({ key, name }) => Object.freeze({ key, name, vorlage: key, gesundheit: false })),
|
||||
Object.freeze({ key: 'gesundheit', name: 'Gesundheitsbereich', vorlage: null, gesundheit: true }),
|
||||
])
|
||||
|
||||
/** Obergrenze fuer den JSON-Text des Fragebogens (Zeichen); die Spalte in Appwrite ist mit 20000 groesser. */
|
||||
export const FRAGEBOGEN_MAX = 16000
|
||||
|
||||
const EINBAU_MODI = ['inline', 'knopf']
|
||||
const TEXTGRENZEN = { knopfText: 40, stelle: 300, gestaltung: 1000, bemerkung: 1000 }
|
||||
const MEHRZEILIG = new Set(['stelle', 'gestaltung', 'bemerkung'])
|
||||
|
||||
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
|
||||
|
||||
/** Ein leerer Fragebogen; bei jedem Aufruf ein neues Objekt. */
|
||||
export function leerFragebogen() {
|
||||
return {
|
||||
schema: 1,
|
||||
branche: null,
|
||||
bestaetigt: [],
|
||||
bundeslandGewaehlt: false,
|
||||
preiseGeprueft: false,
|
||||
einbau: { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: '' },
|
||||
}
|
||||
}
|
||||
|
||||
function wahrheit(fehl, pfad, wert) {
|
||||
if (wert === undefined || wert === null) return false
|
||||
if (typeof wert !== 'boolean') {
|
||||
fehl(pfad, 'Bitte „ja“ oder „nein“ wählen.')
|
||||
return false
|
||||
}
|
||||
return wert
|
||||
}
|
||||
|
||||
function einbauFelder(fehl, eingabe) {
|
||||
const quelle = eingabe === undefined || eingabe === null ? {} : eingabe
|
||||
if (!istObjekt(quelle)) {
|
||||
fehl('einbau', 'Die Angaben zum Einbau sind ungültig.')
|
||||
return leerFragebogen().einbau
|
||||
}
|
||||
const einbau = { modus: null }
|
||||
if (quelle.modus !== undefined && quelle.modus !== null) {
|
||||
if (EINBAU_MODI.includes(quelle.modus)) einbau.modus = quelle.modus
|
||||
else fehl('einbau.modus', 'Bitte wähle, ob der Terminplaner im Text oder als Knopf erscheint.')
|
||||
}
|
||||
for (const [feld, max] of Object.entries(TEXTGRENZEN)) {
|
||||
const wert = quelle[feld]
|
||||
if (wert === undefined || wert === null) einbau[feld] = ''
|
||||
else if (typeof wert !== 'string') {
|
||||
fehl(`einbau.${feld}`, 'Der Text ist ungültig.')
|
||||
einbau[feld] = ''
|
||||
} else einbau[feld] = text(wert, max, { mehrzeilig: MEHRZEILIG.has(feld) })
|
||||
}
|
||||
return einbau
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft und bereinigt einen Fragebogen aus Kundeneingabe oder gespeichertem JSON.
|
||||
* Baut ein neues Objekt Feld fuer Feld (unbekannte Schluessel und __proto__ fallen weg), kuerzt Texte, entfernt
|
||||
* Steuerzeichen und ordnet `bestaetigt` nach SCHRITTE ohne Doppelte und Fremde. Fehlende Felder sind leer/false.
|
||||
* Ergebnis: { fragebogen } oder { fehler: { [pfad]: text } }; idempotent.
|
||||
*/
|
||||
export function normalisiereFragebogen(eingabe) {
|
||||
if (!istObjekt(eingabe)) return { fehler: { _: 'Der Fragebogen ist ungültig.' } }
|
||||
const fehler = {}
|
||||
const fehl = (pfad, meldung) => { if (!Object.hasOwn(fehler, pfad)) fehler[pfad] = meldung }
|
||||
|
||||
let branche = null
|
||||
if (eingabe.branche !== undefined && eingabe.branche !== null) {
|
||||
if (BRANCHEN.some((b) => b.key === eingabe.branche)) branche = eingabe.branche
|
||||
else fehl('branche', 'Bitte wähle eine Branche aus der Liste.')
|
||||
}
|
||||
|
||||
let bestaetigt = []
|
||||
if (eingabe.bestaetigt !== undefined && eingabe.bestaetigt !== null) {
|
||||
if (Array.isArray(eingabe.bestaetigt)) bestaetigt = SCHRITTE.filter((schritt) => eingabe.bestaetigt.includes(schritt))
|
||||
else fehl('bestaetigt', 'Die bestätigten Schritte sind ungültig.')
|
||||
}
|
||||
|
||||
const fragebogen = {
|
||||
schema: 1,
|
||||
branche,
|
||||
bestaetigt,
|
||||
bundeslandGewaehlt: wahrheit(fehl, 'bundeslandGewaehlt', eingabe.bundeslandGewaehlt),
|
||||
preiseGeprueft: wahrheit(fehl, 'preiseGeprueft', eingabe.preiseGeprueft),
|
||||
einbau: einbauFelder(fehl, eingabe.einbau),
|
||||
}
|
||||
return Object.keys(fehler).length ? { fehler } : { fragebogen }
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestellpruefung des Terminplaners: Liste der offenen Punkte `[{ schritt, text }]`, leer = bestellbar.
|
||||
|
||||
@@ -26,6 +26,9 @@ export const TERMINE_COLLECTIONS = {
|
||||
int('version'),
|
||||
dt('updatedAt'),
|
||||
str('updatedBy', 100),
|
||||
// TP2: Bestell-Fragebogen als JSON-Text. Der Inhalt ist auf 16000 Zeichen begrenzt (FRAGEBOGEN_MAX in
|
||||
// termine/fragebogen.js); die groessere Spalte wird in Appwrite eine Textspalte und zaehlt nicht zur Zeilenbreite.
|
||||
str('fragebogen', 20000),
|
||||
],
|
||||
indexes: [],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
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`)
|
||||
}
|
||||
})
|
||||
@@ -75,7 +75,7 @@ const expectedIndexes = NAMES.flatMap((name) => TERMINE_COLLECTIONS[name].indexe
|
||||
|
||||
test('Schema: Attribute je Collection wie in der Spezifikation', () => {
|
||||
assert.deepEqual(Object.keys(TERMINE_COLLECTIONS), NAMES)
|
||||
assert.deepEqual(keysOf('termineConfig'), ['customerId', 'projectId', 'einstellung', 'version', 'updatedAt', 'updatedBy'])
|
||||
assert.deepEqual(keysOf('termineConfig'), ['customerId', 'projectId', 'einstellung', 'version', 'updatedAt', 'updatedBy', 'fragebogen'])
|
||||
assert.deepEqual(keysOf('termineBookings'), [
|
||||
'featureId', 'customerId', 'projectId', 'leistungId', 'leistungName', 'ressourceId', 'ressourceName',
|
||||
'startsAt', 'endsAt', 'blockEndsAt', 'status', 'quelle',
|
||||
@@ -101,6 +101,8 @@ test('Schema: Typen und Groessen', () => {
|
||||
assert.equal(settings.updatedAt.type, 'datetime')
|
||||
assert.equal(settings.updatedBy.size, 100)
|
||||
assert.equal(settings.projectId.size, 64)
|
||||
// TP2: Fragebogen als JSON-Text; der Inhalt ist auf 16000 Zeichen begrenzt (FRAGEBOGEN_MAX), die Spalte ist groesser
|
||||
assert.deepEqual(settings.fragebogen, { key: 'fragebogen', type: 'string', size: 20000, required: false })
|
||||
|
||||
const bookings = byKey('termineBookings')
|
||||
const sizes = {
|
||||
@@ -313,6 +315,29 @@ test('main: legt idx_booking der Mail-Warteschlange an; eine vorhandene termineO
|
||||
assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith('termineOutbox.')), [])
|
||||
})
|
||||
|
||||
test('main --check meldet in einer vorhandenen termineConfig ohne fragebogen genau dieses Attribut; der Lauf ohne --check legt es an, der zweite nichts', async () => {
|
||||
const ohneFragebogen = keysOf('termineConfig').filter((key) => key !== 'fragebogen')
|
||||
const vorhanden = () => fakeClient({ termineConfig: { attrs: ohneFragebogen } })
|
||||
|
||||
const lines = []
|
||||
const pruefung = vorhanden()
|
||||
const offen = await main({ check: true, client: pruefung, log: (line) => lines.push(line) })
|
||||
assert.ok(lines.some((line) => line.includes('termineConfig.fragebogen FEHLT')), lines.join('\n'))
|
||||
assert.deepEqual(offen.missing.filter((a) => a.collection === 'termineConfig').map((a) => a.key), ['fragebogen'])
|
||||
assert.deepEqual(offen.missingCollections.includes('termineConfig'), false)
|
||||
assert.deepEqual(pruefung.calls.attrs, [], '--check legt nichts an')
|
||||
|
||||
const client = vorhanden()
|
||||
await main({ client, log: quiet })
|
||||
assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith('termineConfig.')), ['termineConfig.fragebogen'])
|
||||
assert.deepEqual(client.state.get('termineConfig').attrs.get('fragebogen'), { key: 'fragebogen', type: 'string', size: 20000, required: false })
|
||||
assert.ok(!client.calls.collections.some((c) => c.id === 'termineConfig'), 'vorhandene Collection wird nicht neu angelegt')
|
||||
|
||||
client.calls.attrs.length = 0
|
||||
await main({ client, log: quiet })
|
||||
assert.deepEqual(client.calls.attrs, [], 'der zweite Lauf legt nichts an')
|
||||
})
|
||||
|
||||
test('main --check legt nichts an und meldet Fehlendes', async () => {
|
||||
const client = fakeClient()
|
||||
const lines = []
|
||||
|
||||
Reference in new issue
Block a user