import { test } from 'node:test' import assert from 'node:assert/strict' import { normalisiereZeiten, normalisiereTeam, telefonZiffern, kurzname, } from '../server/services/zeiten/pruefen.js' import { BUNDESLAENDER } from '../server/services/termine/feiertage.js' import { wochenplanHash } from '../server/services/zeiten/ersatz.js' import { hauptplan, gueltigeHinweise, statusZu, tagesbild } from '../server/services/zeiten/zeitlogik.js' const NOW = new Date('2026-10-05T10:00:00.000Z') // Berlin: Montag 2026-10-05 12:00 const P1 = 'p_aaaaaaa1' const P2 = 'p_aaaaaaa2' // Wochentage 1..7 (Mo..So). Wert: ['09:00','18:00'] = ein Fenster, [[..],[..]] = zwei Fenster, 'rund' / 'vereinbarung' = Modus, fehlt = zu. function tage(woche = {}) { const t = {} for (let d = 1; d <= 7; d += 1) { const w = woche[d] if (!w) t[d] = { modus: 'zu', fenster: [] } else if (typeof w === 'string') t[d] = { modus: w, fenster: [] } else t[d] = { modus: 'zeiten', fenster: Array.isArray(w[0]) ? w : [w] } } return t } function plan(extra = {}) { return { id: P1, kurz: 'standard', name: 'Standard', haupt: true, tage: tage({ 1: ['09:00', '18:00'] }), ...extra } } function eingabe(extra = {}) { return { land: 'RP', plaene: [plan()], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null }, ...extra, } } function urlaub(extra = {}) { return { id: 'x_aaaaaaa1', art: 'urlaub', von: '2026-10-12', bis: '2026-10-16', text: 'Betriebsurlaub', modus: 'zu', fenster: [], plan: null, ...extra } } function hinweis(extra = {}) { return { id: 'h_aaaaaaa1', art: 'info', text: 'Wir haben neue Öffnungszeiten.', knopf: null, von: null, bis: null, ...extra } } /** Erwartet { daten } und gibt sie zurück. */ function gut(e, optionen = {}) { const r = normalisiereZeiten(e, { now: NOW, ...optionen }) assert.equal(r.fehler, undefined, `unerwartete Fehler: ${JSON.stringify(r.fehler)}`) assert.ok(r.daten) return r.daten } /** Erwartet { fehler } und gibt sie zurück. */ function schlecht(e, optionen = {}) { const r = normalisiereZeiten(e, { now: NOW, ...optionen }) assert.equal(r.daten, undefined, 'es sollten Fehler kommen') assert.ok(r.fehler && typeof r.fehler === 'object') return r.fehler } const nurPfad = (fehler, pfad) => assert.ok(Object.hasOwn(fehler, pfad), `Pfad ${pfad} fehlt in ${JSON.stringify(fehler)}`) // ------------------------------------------------------------------ Grundform, IDs, Kurznamen test('gültiger Plan bleibt, IDs und Kurznamen bleiben unverändert', () => { const e = eingabe({ plaene: [ plan({ tage: tage({ 1: ['09:00', '12:30'], 2: [['09:00', '12:30'], ['13:30', '18:00']], 3: 'rund', 4: 'vereinbarung' }) }), plan({ id: P2, kurz: 'kueche', name: 'Küche', haupt: false, tage: tage({ 5: ['11:00', '14:00'] }) }), ], }) const daten = gut(e, { bisher: { plaene: [{ id: P1, kurz: 'standard' }, { id: P2, kurz: 'kueche' }] } }) assert.deepEqual(daten, e) assert.deepEqual(Object.keys(daten), ['land', 'plaene', 'feiertage', 'ausnahmen', 'hinweise', 'kontakt']) }) test('Eingabe wird nicht verändert', () => { const e = eingabe({ plaene: [plan({ tage: tage({ 1: [['13:00', '18:00'], ['09:00', '12:00']] }) })] }) const kopie = JSON.parse(JSON.stringify(e)) gut(e) assert.deepEqual(e, kopie) }) test('Nicht-Objekt als Eingabe ist ein Fehler an daten', () => { for (const x of [null, undefined, 'x', 5, [], true]) { const f = schlecht(x) nurPfad(f, 'daten') } }) test('now ist Pflicht', () => { assert.throws(() => normalisiereZeiten(eingabe(), {}), TypeError) assert.throws(() => normalisiereZeiten(eingabe()), TypeError) assert.throws(() => normalisiereZeiten(eingabe(), { now: 'heute' }), TypeError) assert.throws(() => normalisiereZeiten(eingabe(), { now: new Date('x') }), TypeError) }) test('kurz ist unveränderlich: bei bekannter Plan-ID gilt der Kurzname von bisher', () => { const e = eingabe({ plaene: [plan({ kurz: 'ganz-anders', name: 'Umbenannt' })] }) const daten = gut(e, { bisher: { plaene: [{ id: P1, kurz: 'standard', name: 'Standard' }] } }) assert.equal(daten.plaene[0].kurz, 'standard') assert.equal(daten.plaene[0].name, 'Umbenannt') }) test('neuer Plan bekommt Kurznamen aus dem Namen', () => { const e = eingabe({ plaene: [ plan({ id: P1, kurz: 'egal', name: 'Küche', haupt: true }), plan({ id: P2, kurz: 'egal', name: 'Küche', haupt: false }), plan({ id: 'p_aaaaaaa3', kurz: 'egal', name: 'Küche', haupt: false }), ], }) const daten = gut(e) assert.deepEqual(daten.plaene.map((p) => p.kurz), ['kueche', 'kueche-2', 'kueche-3']) }) test('ein neuer Plan nimmt keinen Kurznamen eines Plans, den es gab (auch eines gelöschten)', () => { const e = eingabe({ plaene: [plan({ id: P2, name: 'Standard', kurz: 'x' })] }) const daten = gut(e, { bisher: { plaene: [{ id: P1, kurz: 'standard' }] } }) assert.equal(daten.plaene[0].kurz, 'standard-2') }) test('kurzname: Umlaute, Sonderzeichen, Länge, Dubletten', () => { assert.equal(kurzname('Küche', []), 'kueche') assert.equal(kurzname('Küche', ['kueche']), 'kueche-2') assert.equal(kurzname('Küche', new Set(['kueche', 'kueche-2'])), 'kueche-3') assert.equal(kurzname('Straße & Café Öffnung', []), 'strasse-cafe-oeffnung') assert.equal(kurzname(' --Sommer 2026!! ', []), 'sommer-2026') assert.equal(kurzname('ÄÖÜ', []), 'aeoeue') assert.equal(kurzname('!!!', []), 'plan') assert.equal(kurzname('', ['plan']), 'plan-2') assert.equal(kurzname(undefined, []), 'plan') const lang = kurzname('a'.repeat(30), []) assert.equal(lang.length, 24) assert.equal(lang, 'a'.repeat(24)) const dublette = kurzname('a'.repeat(30), [lang]) assert.equal(dublette.length <= 24, true) assert.match(dublette, /^a+-2$/) assert.match(kurzname('Ein sehr langer Name mit Bindestrich am Ende hier', []), /^[a-z0-9]+(-[a-z0-9]+)*$/) assert.ok(kurzname('Ein sehr langer Name mit Bindestrich am Ende hier', []).length <= 24) }) test('fehlende oder ungültige IDs werden neu erzeugt, doppelte ebenfalls', () => { const e = eingabe({ plaene: [ plan({ id: undefined, name: 'Eins' }), plan({ id: P2, name: 'Zwei', haupt: false }), plan({ id: P2, name: 'Drei', haupt: false }), plan({ id: 'hat.punkt', name: 'Vier', haupt: false }), ], ausnahmen: [urlaub({ id: undefined }), urlaub({ id: '' })], hinweise: [hinweis({ id: 42 })], }) const daten = gut(e) const ids = daten.plaene.map((p) => p.id) assert.match(ids[0], /^p_[a-z0-9]{8}$/) assert.equal(ids[1], P2) assert.match(ids[2], /^p_[a-z0-9]{8}$/) assert.match(ids[3], /^p_[a-z0-9]{8}$/) assert.equal(new Set(ids).size, 4) assert.match(daten.ausnahmen[0].id, /^x_[a-z0-9]{8}$/) assert.match(daten.ausnahmen[1].id, /^x_[a-z0-9]{8}$/) assert.notEqual(daten.ausnahmen[0].id, daten.ausnahmen[1].id) assert.match(daten.hinweise[0].id, /^h_[a-z0-9]{8}$/) }) test('land: Vorgabe RP, nur die 16 Bundesländer', () => { assert.equal(gut(eingabe({ land: undefined })).land, 'RP') assert.equal(gut(eingabe({ land: null })).land, 'RP') for (const land of BUNDESLAENDER) assert.equal(gut(eingabe({ land })).land, land) for (const land of ['XX', 'rp', 'BUND', 5, '']) nurPfad(schlecht(eingabe({ land })), 'land') }) test('Normalisieren ist idempotent', () => { const e = eingabe({ land: 'BY', plaene: [ plan({ tage: tage({ 1: [['13:00', '18:00'], ['08:00', '12:00']], 5: ['20:00', '02:00'], 6: 'rund' }) }), plan({ id: undefined, name: 'Mittagstisch', haupt: false }), ], feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '12:00']] }, '2026-12-25': { modus: 'zu', fenster: [] } }, ausnahmen: [urlaub(), { id: undefined, art: 'sondertag', von: '2026-11-01', bis: '2026-11-01', text: 'Tag der offenen Tür', modus: 'zeiten', fenster: [['10:00', '16:00']], plan: P1 }], hinweise: [hinweis({ knopf: { art: 'link', ziel: 'HTTPS://Beispiel.DE/Angebot', text: 'Mehr' }, von: '2026-10-01', bis: '2026-10-31' }), hinweis({ id: undefined, art: 'aktion', bis: '2026-11-30' })], kontakt: { an: true, telefon: '0171 1234567', adresse: ' Hauptstr. 1 ', whatsapp: { nummer: '+49 171 1234567', starttext: 'Hallo', bestaetigtAt: '2026-10-04T08:00:00Z' } }, }) const einmal = gut(e) const zweimal = gut(einmal, { bisher: einmal }) assert.deepEqual(zweimal, einmal) assert.equal(JSON.stringify(zweimal), JSON.stringify(einmal)) }) test('Das Ergebnis passt zur Zeitlogik: Wochenplan-Hash ist stabil, Reihenfolge der Fenster egal', () => { const a = gut(eingabe({ plaene: [plan({ tage: tage({ 1: [['13:30', '18:00'], ['09:00', '12:30']], 2: 'rund' }) })] })) const b = gut(eingabe({ plaene: [plan({ tage: { 2: { modus: 'rund', fenster: [['01:00', '02:00']] }, 1: { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] } } })] })) assert.equal(wochenplanHash(a), wochenplanHash(b)) assert.deepEqual(a.plaene, b.plaene) }) test('Das Ergebnis lässt sich direkt in der Zeitlogik verwenden', () => { const daten = gut(eingabe({ plaene: [plan({ tage: tage({ 1: ['09:00', '18:00'] }) })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })], hinweise: [hinweis({ von: '2026-10-01', bis: '2026-10-31' })], })) assert.equal(hauptplan(daten).id, P1) assert.equal(tagesbild(daten, hauptplan(daten), '2026-10-05').modus, 'zeiten') assert.equal(tagesbild(daten, hauptplan(daten), '2026-10-13').art, 'urlaub') assert.equal(statusZu(daten, hauptplan(daten), NOW).offen, true) assert.equal(gueltigeHinweise(daten, NOW).length, 1) }) // ------------------------------------------------------------------ Pläne test('genau ein Hauptplan, höchstens 5 Pläne, mindestens einer', () => { nurPfad(schlecht(eingabe({ plaene: [plan({ haupt: false })] })), 'plaene') nurPfad(schlecht(eingabe({ plaene: [plan(), plan({ id: P2, haupt: true })] })), 'plaene') nurPfad(schlecht(eingabe({ plaene: [] })), 'plaene') nurPfad(schlecht(eingabe({ plaene: undefined })), 'plaene') nurPfad(schlecht(eingabe({ plaene: 'x' })), 'plaene') const fuenf = Array.from({ length: 5 }, (_, i) => plan({ id: `p_bbbbbbb${i}`, name: `Plan ${i}`, haupt: i === 0 })) assert.equal(gut(eingabe({ plaene: fuenf })).plaene.length, 5) const sechs = [...fuenf, plan({ id: 'p_bbbbbbb9', name: 'Plan 9', haupt: false })] nurPfad(schlecht(eingabe({ plaene: sechs })), 'plaene') }) test('Planname ist Pflicht und höchstens 60 Zeichen', () => { nurPfad(schlecht(eingabe({ plaene: [plan({ name: ' ' })] })), `plaene.${P1}.name`) nurPfad(schlecht(eingabe({ plaene: [plan({ name: undefined })] })), `plaene.${P1}.name`) nurPfad(schlecht(eingabe({ plaene: [plan({ name: 'x'.repeat(61) })] })), `plaene.${P1}.name`) assert.equal(gut(eingabe({ plaene: [plan({ name: 'x'.repeat(60) })] })).plaene[0].name.length, 60) }) test('Plan ohne Wochentage: alle sieben Tage geschlossen; fehlende Tage werden ergänzt', () => { const daten = gut(eingabe({ plaene: [plan({ tage: undefined })] })) assert.deepEqual(Object.keys(daten.plaene[0].tage), ['1', '2', '3', '4', '5', '6', '7']) for (let d = 1; d <= 7; d += 1) assert.deepEqual(daten.plaene[0].tage[d], { modus: 'zu', fenster: [] }) const teil = gut(eingabe({ plaene: [plan({ tage: { 3: { modus: 'rund' } } })] })) assert.deepEqual(teil.plaene[0].tage[3], { modus: 'rund', fenster: [] }) assert.deepEqual(teil.plaene[0].tage[1], { modus: 'zu', fenster: [] }) }) test('Wochentage: Modus muss gültig sein, unbekannte Schlüssel und Tage fallen weg', () => { nurPfad(schlecht(eingabe({ plaene: [plan({ tage: { 1: { modus: 'offen', fenster: [] } } })] })), `plaene.${P1}.tage.1`) nurPfad(schlecht(eingabe({ plaene: [plan({ tage: { 2: {} } })] })), `plaene.${P1}.tage.2`) nurPfad(schlecht(eingabe({ plaene: [plan({ tage: { 4: 'zu' } })] })), `plaene.${P1}.tage.4`) nurPfad(schlecht(eingabe({ plaene: [plan({ tage: 'x' })] })), `plaene.${P1}.tage`) nurPfad(schlecht(eingabe({ plaene: [plan({ tage: { 3: { modus: 'zeiten', fenster: [] } } })] })), `plaene.${P1}.tage.3`) const daten = gut(eingabe({ plaene: [plan({ tage: { ...tage({ 1: ['09:00', '12:00'] }), 0: { modus: 'zu' }, 8: { modus: 'zu' }, x: 1 } })] })) assert.deepEqual(Object.keys(daten.plaene[0].tage), ['1', '2', '3', '4', '5', '6', '7']) }) test('Kanonische Form: Fenster sortiert, fenster [] außer bei zeiten, unbekannte Schlüssel fallen weg', () => { const e = eingabe({ plaene: [{ id: P1, kurz: 'standard', name: 'Standard', haupt: true, farbe: 'rot', extra: { a: 1 }, tage: { 1: { modus: 'zeiten', fenster: [['13:30', '18:00'], ['09:00', '12:30']], notiz: 'x' }, 2: { modus: 'rund', fenster: [['09:00', '10:00']] }, 3: { modus: 'zu', fenster: 'kaputt' }, 4: { modus: 'vereinbarung', fenster: [['99:99', '100:00']] }, }, }], kontakt: { an: true, telefon: '0171 1234567', adresse: '', whatsapp: null, fax: '1' }, zusatz: 1, }) const daten = gut(e) assert.deepEqual(daten.plaene[0].tage[1], { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] }) assert.deepEqual(daten.plaene[0].tage[2], { modus: 'rund', fenster: [] }) assert.deepEqual(daten.plaene[0].tage[3], { modus: 'zu', fenster: [] }) assert.deepEqual(daten.plaene[0].tage[4], { modus: 'vereinbarung', fenster: [] }) assert.deepEqual(Object.keys(daten.plaene[0]), ['id', 'kurz', 'name', 'haupt', 'tage']) assert.deepEqual(Object.keys(daten.kontakt), ['an', 'telefon', 'adresse', 'whatsapp']) assert.equal(daten.zusatz, undefined) }) // ------------------------------------------------------------------ Fenster function wochenfehler(woche) { return schlecht(eingabe({ plaene: [plan({ tage: tage(woche) })] })) } test('Fensterregeln: jede Regel hat ihren eigenen Fehlerpfad', () => { const f = wochenfehler({ 1: [['09:00', '12:00'], ['11:00', '14:00']], // Überschneidung 2: ['10:00', '10:00'], // bis == von 3: [['22:00', '02:00'], ['23:00', '23:30']], // Mitternacht nicht als letztes 4: ['22:00', '03:00'], // ragt in den Folgetag 5: ['09:03', '12:00'], // kein 5-Minuten-Raster 6: [['08:00', '09:00'], ['10:00', '11:00'], ['12:00', '13:00']], // drei Fenster 7: ['9:00', '12:00'], // kein HH:MM }) // Tag 4 ragt in Tag 5 hinein, nur wenn Tag 5 gültige Fenster hat; hier ist Tag 5 kaputt -> kein Folgefehler for (const t of [1, 2, 3, 5, 6, 7]) nurPfad(f, `plaene.${P1}.tage.${t}`) assert.equal(Object.hasOwn(f, `plaene.${P1}.tage.4`), false) const meldungen = new Set(Object.values(f)) assert.ok(meldungen.size >= 5, 'unterschiedliche Meldungen je Regel') }) test('Fenster über Mitternacht: nur zuletzt und nicht in das erste Fenster des Folgetags hinein', () => { // ragt in den Dienstag hinein (erstes Fenster 01:00) const f = wochenfehler({ 1: ['22:00', '03:00'], 2: ['01:00', '05:00'] }) nurPfad(f, `plaene.${P1}.tage.1`) assert.equal(Object.keys(f).length, 1) // Sonntag -> Montag springt um const w = wochenfehler({ 7: ['20:00', '10:00'], 1: ['09:00', '12:00'] }) nurPfad(w, `plaene.${P1}.tage.7`) // genau bis zum Beginn des Folgefensters ist erlaubt const ok = gut(eingabe({ plaene: [plan({ tage: tage({ 7: ['20:00', '09:00'], 1: ['09:00', '12:00'] }) })] })) assert.deepEqual(ok.plaene[0].tage[7].fenster, [['20:00', '09:00']]) // Folgetag zu, rund oder ohne Fenster: nichts zu prüfen assert.ok(gut(eingabe({ plaene: [plan({ tage: tage({ 1: ['22:00', '03:00'] }) })] }))) assert.ok(gut(eingabe({ plaene: [plan({ tage: tage({ 1: ['22:00', '03:00'], 2: 'rund' }) })] }))) // Nur das zweite Fenster darf über Mitternacht gehen assert.ok(gut(eingabe({ plaene: [plan({ tage: tage({ 1: [['11:00', '14:00'], ['18:00', '01:00']], 2: ['09:00', '12:00'] }) })] }))) // Das erste Fenster eines Folgetags zählt, auch wenn es das zweite eingegebene ist nurPfad(wochenfehler({ 1: ['22:00', '03:00'], 2: [['13:00', '14:00'], ['02:00', '04:00']] }), `plaene.${P1}.tage.1`) }) test('Fensterende 24:00 und 00:00 sind gültig, Beginn 24:00 nicht', () => { const ok = gut(eingabe({ plaene: [plan({ tage: tage({ 1: ['18:00', '24:00'], 2: ['00:00', '24:00'], 3: ['18:00', '00:00'] }) })] })) assert.deepEqual(ok.plaene[0].tage[1].fenster, [['18:00', '24:00']]) assert.deepEqual(ok.plaene[0].tage[2].fenster, [['00:00', '24:00']]) nurPfad(wochenfehler({ 1: ['24:00', '24:00'] }), `plaene.${P1}.tage.1`) nurPfad(wochenfehler({ 1: ['12:00', '24:05'] }), `plaene.${P1}.tage.1`) nurPfad(wochenfehler({ 1: ['12:00', '25:00'] }), `plaene.${P1}.tage.1`) nurPfad(wochenfehler({ 1: ['12:00', '12:60'] }), `plaene.${P1}.tage.1`) }) test('Fenster: Berührung ist keine Überschneidung, Reihenfolge der Eingabe egal', () => { const daten = gut(eingabe({ plaene: [plan({ tage: tage({ 1: [['12:00', '18:00'], ['09:00', '12:00']] }) })] })) assert.deepEqual(daten.plaene[0].tage[1].fenster, [['09:00', '12:00'], ['12:00', '18:00']]) }) test('Fenster: kaputte Formen sind Fehler, keine Abstürze', () => { for (const kaputt of ['x', 5, null, {}, [['09:00']], [['09:00', '12:00', '13:00']], [null], [[9, 12]], [['09:00', 12]]]) { const f = schlecht(eingabe({ plaene: [plan({ tage: { ...tage(), 3: { modus: 'zeiten', fenster: kaputt } } })] })) nurPfad(f, `plaene.${P1}.tage.3`) } }) test('Feiertage: Fenster nach denselben Regeln, Mitternacht nur zuletzt', () => { nurPfad(schlecht(eingabe({ feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '12:00'], ['11:00', '13:00']] } } })), 'feiertage.2026-12-24') nurPfad(schlecht(eingabe({ feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['22:00', '02:00'], ['23:00', '23:30']] } } })), 'feiertage.2026-12-24') nurPfad(schlecht(eingabe({ feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['09:07', '12:00']] } } })), 'feiertage.2026-12-24') nurPfad(schlecht(eingabe({ feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [] } } })), 'feiertage.2026-12-24') assert.ok(gut(eingabe({ feiertage: { '2026-12-31': { modus: 'zeiten', fenster: [['18:00', '03:00']] } } }))) }) test('Feiertage: Einträge ohne gültigen Modus fallen weg, Schlüssel werden geprüft und sortiert', () => { const daten = gut(eingabe({ feiertage: { '2026-12-26': { modus: 'rund', fenster: [['09:00', '10:00']] }, '2026-12-25': {}, '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '12:00']], text: 'x' }, '2026-12-27': { modus: 'kaputt' }, '2026-12-28': null, '2026-12-29': 'zu', '2026-12-30': { modus: 'zu', fenster: [['09:00', '12:00']] }, }, })) assert.deepEqual(daten.feiertage, { '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '12:00']] }, '2026-12-26': { modus: 'rund', fenster: [] }, '2026-12-30': { modus: 'zu', fenster: [] }, }) assert.deepEqual(Object.keys(daten.feiertage), ['2026-12-24', '2026-12-26', '2026-12-30']) nurPfad(schlecht(eingabe({ feiertage: { '2026-02-30': { modus: 'zu' } } })), 'feiertage.2026-02-30') nurPfad(schlecht(eingabe({ feiertage: { morgen: { modus: 'zu' } } })), 'feiertage.morgen') nurPfad(schlecht(eingabe({ feiertage: [] })), 'feiertage') nurPfad(schlecht(eingabe({ feiertage: 'x' })), 'feiertage') assert.deepEqual(gut(eingabe({ feiertage: undefined })).feiertage, {}) }) // ------------------------------------------------------------------ Ausnahmen test('Ausnahmen: höchstens 100, Reihenfolge bleibt erhalten', () => { const liste = Array.from({ length: 100 }, (_, i) => urlaub({ id: `x_ccccc${String(i).padStart(3, '0')}`, von: '2026-11-01', bis: '2026-11-02', text: '' })) assert.equal(gut(eingabe({ ausnahmen: liste })).ausnahmen.length, 100) nurPfad(schlecht(eingabe({ ausnahmen: [...liste, urlaub({ id: 'x_ccccc999', text: '' })] })), 'ausnahmen') nurPfad(schlecht(eingabe({ ausnahmen: 'x' })), 'ausnahmen') const umgekehrt = [urlaub({ id: 'x_zzzzzzz9', von: '2026-12-01', bis: '2026-12-02' }), urlaub({ id: 'x_aaaaaaa1', von: '2026-11-01', bis: '2026-11-02' })] assert.deepEqual(gut(eingabe({ ausnahmen: umgekehrt })).ausnahmen.map((a) => a.id), ['x_zzzzzzz9', 'x_aaaaaaa1']) }) test('Ausnahmen: Art, Datum, von <= bis, bis fehlt = ein Tag', () => { const id = 'x_aaaaaaa1' nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ art: 'feiertag' })] })), `ausnahmen.${id}.art`) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-13-01' })] })), `ausnahmen.${id}.von`) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: undefined })] })), `ausnahmen.${id}.von`) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ bis: '2026-02-30' })] })), `ausnahmen.${id}.bis`) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-10-16', bis: '2026-10-12' })] })), `ausnahmen.${id}.bis`) nurPfad(schlecht(eingabe({ ausnahmen: [null] })), 'ausnahmen.0') const eintag = gut(eingabe({ ausnahmen: [urlaub({ bis: undefined })] })).ausnahmen[0] assert.equal(eintag.bis, eintag.von) assert.equal(gut(eingabe({ ausnahmen: [urlaub({ bis: null })] })).ausnahmen[0].bis, '2026-10-12') }) test('Urlaub höchstens 366 Tage (einschließlich beider Endtage)', () => { const id = 'x_aaaaaaa1' const ok = gut(eingabe({ ausnahmen: [urlaub({ von: '2026-10-05', bis: '2027-10-05' })] })) assert.equal(ok.ausnahmen.length, 1) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-10-05', bis: '2027-10-06' })] })), `ausnahmen.${id}.bis`) // über den Jahreswechsel gerechnet, kein Vielfaches von 24 h nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-03-28', bis: '2027-03-30' })] })), `ausnahmen.${id}.bis`) assert.ok(gut(eingabe({ ausnahmen: [urlaub({ von: '2026-03-28', bis: '2027-03-27' })] }))) }) test('Urlaub ist immer geschlossen, Sondertag behält seine Zeiten', () => { const u = gut(eingabe({ ausnahmen: [urlaub({ modus: 'rund', fenster: [['09:00', '10:00']] })] })).ausnahmen[0] assert.equal(u.modus, 'zu') assert.deepEqual(u.fenster, []) const sonder = { id: 'x_aaaaaaa2', art: 'sondertag', von: '2026-11-01', bis: '2026-11-01', text: 'Tag der offenen Tür', modus: 'zeiten', fenster: [['10:00', '16:00'], ['17:00', '18:00']], plan: null } assert.deepEqual(gut(eingabe({ ausnahmen: [sonder] })).ausnahmen[0], sonder) }) test('Sondertag: Modus Pflicht, Fenster nach den Regeln', () => { const basis = { id: 'x_aaaaaaa2', art: 'sondertag', von: '2026-11-01', bis: '2026-11-01', text: 'Sonder', plan: null } nurPfad(schlecht(eingabe({ ausnahmen: [{ ...basis, modus: 'kaputt', fenster: [] }] })), 'ausnahmen.x_aaaaaaa2.modus') nurPfad(schlecht(eingabe({ ausnahmen: [{ ...basis, fenster: [] }] })), 'ausnahmen.x_aaaaaaa2.modus') nurPfad(schlecht(eingabe({ ausnahmen: [{ ...basis, modus: 'zeiten', fenster: [] }] })), 'ausnahmen.x_aaaaaaa2.fenster') nurPfad(schlecht(eingabe({ ausnahmen: [{ ...basis, modus: 'zeiten', fenster: [['10:00', '12:00'], ['11:00', '13:00']] }] })), 'ausnahmen.x_aaaaaaa2.fenster') const zu = gut(eingabe({ ausnahmen: [{ ...basis, modus: 'zu', fenster: [['10:00', '12:00']] }] })).ausnahmen[0] assert.deepEqual(zu.fenster, []) const sortiert = gut(eingabe({ ausnahmen: [{ ...basis, modus: 'zeiten', fenster: [['13:00', '15:00'], ['10:00', '12:00']] }] })).ausnahmen[0] assert.deepEqual(sortiert.fenster, [['10:00', '12:00'], ['13:00', '15:00']]) }) test('Ausnahme gilt für einen bekannten Plan oder für alle', () => { const id = 'x_aaaaaaa1' nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ plan: 'p_gibtsnicht' })] })), `ausnahmen.${id}.plan`) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ plan: 5 })] })), `ausnahmen.${id}.plan`) assert.equal(gut(eingabe({ ausnahmen: [urlaub({ plan: P1 })] })).ausnahmen[0].plan, P1) assert.equal(gut(eingabe({ ausnahmen: [urlaub({ plan: undefined })] })).ausnahmen[0].plan, null) assert.equal(gut(eingabe({ ausnahmen: [urlaub({ plan: '' })] })).ausnahmen[0].plan, null) }) // ------------------------------------------------------------------ Hinweise test('Hinweise: höchstens 20, Art Pflicht, Text Pflicht, Reihenfolge bleibt', () => { const liste = Array.from({ length: 20 }, (_, i) => hinweis({ id: `h_ddddd${String(i).padStart(3, '0')}` })) assert.equal(gut(eingabe({ hinweise: liste })).hinweise.length, 20) nurPfad(schlecht(eingabe({ hinweise: [...liste, hinweis({ id: 'h_ddddd999' })] })), 'hinweise') nurPfad(schlecht(eingabe({ hinweise: 'x' })), 'hinweise') nurPfad(schlecht(eingabe({ hinweise: [null] })), 'hinweise.0') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: 'laut' })] })), 'hinweise.h_aaaaaaa1.art') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: undefined })] })), 'hinweise.h_aaaaaaa1.art') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ text: ' ' })] })), 'hinweise.h_aaaaaaa1.text') for (const art of ['info', 'urlaub', 'aktion', 'wichtig']) assert.ok(gut(eingabe({ hinweise: [hinweis({ art, bis: '2026-12-01' })] }))) const umgekehrt = [hinweis({ id: 'h_zzzzzzz9' }), hinweis({ id: 'h_aaaaaaa1' })] assert.deepEqual(gut(eingabe({ hinweise: umgekehrt })).hinweise.map((h) => h.id), ['h_zzzzzzz9', 'h_aaaaaaa1']) }) test('Hinweis: von und bis sind optional (Kalendertage), bis >= von', () => { const id = 'h_aaaaaaa1' const ohne = gut(eingabe({ hinweise: [hinweis({ von: undefined, bis: undefined })] })).hinweise[0] assert.equal(ohne.von, null) assert.equal(ohne.bis, null) const leer = gut(eingabe({ hinweise: [hinweis({ von: '', bis: '' })] })).hinweise[0] assert.equal(leer.von, null) assert.equal(leer.bis, null) assert.equal(gut(eingabe({ hinweise: [hinweis({ von: '2026-10-10' })] })).hinweise[0].von, '2026-10-10') assert.equal(gut(eingabe({ hinweise: [hinweis({ bis: '2026-10-10' })] })).hinweise[0].bis, '2026-10-10') assert.ok(gut(eingabe({ hinweise: [hinweis({ von: '2026-10-10', bis: '2026-10-10' })] }))) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ von: '2026-10-11', bis: '2026-10-10' })] })), `hinweise.${id}.bis`) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ von: '10.10.2026' })] })), `hinweise.${id}.von`) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ bis: '2026-02-30' })] })), `hinweise.${id}.bis`) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ von: 5 })] })), `hinweise.${id}.von`) }) test('Aktion braucht Enddatum, Urlaub höchstens 366 Tage', () => { const id = 'h_aaaaaaa1' nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: 'aktion', bis: null })] })), `hinweise.${id}.bis`) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: 'aktion', bis: '' })] })), `hinweise.${id}.bis`) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: 'aktion', von: '2026-10-10' })] })), `hinweise.${id}.bis`) assert.equal(gut(eingabe({ hinweise: [hinweis({ art: 'aktion', bis: '2026-10-31' })] })).hinweise[0].bis, '2026-10-31') // Hinweis der Art Urlaub: ebenfalls höchstens 366 Tage assert.ok(gut(eingabe({ hinweise: [hinweis({ art: 'urlaub', von: '2026-10-05', bis: '2027-10-05' })] }))) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ art: 'urlaub', von: '2026-10-05', bis: '2027-10-06' })] })), `hinweise.${id}.bis`) assert.ok(gut(eingabe({ hinweise: [hinweis({ art: 'info', von: '2026-10-05', bis: '2028-10-06' })] }))) // andere Arten sind nicht begrenzt // Urlaub-Ausnahme: 366 Tage gehen, 367 nicht assert.ok(gut(eingabe({ ausnahmen: [urlaub({ von: '2026-10-05', bis: '2027-10-05' })] }))) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-10-05', bis: '2027-10-06' })] })), 'ausnahmen.x_aaaaaaa1.bis') }) test('Knopf: null bleibt null, ein leerer Knopf zählt als kein Knopf', () => { assert.equal(gut(eingabe({ hinweise: [hinweis({ knopf: null })] })).hinweise[0].knopf, null) assert.equal(gut(eingabe({ hinweise: [hinweis({ knopf: undefined })] })).hinweise[0].knopf, null) assert.equal(gut(eingabe({ hinweise: [hinweis({ knopf: { art: 'link', ziel: '', text: '' } })] })).hinweise[0].knopf, null) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ knopf: 'x' })] })), 'hinweise.h_aaaaaaa1.knopf') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ knopf: { art: 'mail', ziel: 'https://example.de/', text: 'Los' } })] })), 'hinweise.h_aaaaaaa1.knopf.art') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ knopf: { art: 'link', ziel: 'https://example.de/', text: '' } })] })), 'hinweise.h_aaaaaaa1.knopf.text') nurPfad(schlecht(eingabe({ hinweise: [hinweis({ knopf: { art: 'link', ziel: '', text: 'Los' } })] })), 'hinweise.h_aaaaaaa1.knopf.ziel') }) test('Link nur https ohne Benutzerdaten, höchstens 300 Zeichen', () => { const link = (ziel) => eingabe({ hinweise: [hinweis({ knopf: { art: 'link', ziel, text: 'Mehr' } })] }) const pfad = 'hinweise.h_aaaaaaa1.knopf.ziel' for (const schlechtes of ['javascript:alert(1)', 'http://example.de/', 'https://u:p@example.de/', 'https://u@example.de/', 'ftp://example.de/', 'example.de', '//example.de', 'https://', 'https://exa mple.de/', 'https://example.de/\nx', "https://exa'mple.de/", `https://example.de/${'a'.repeat(300)}`, 5, {}]) { nurPfad(schlecht(link(schlechtes)), pfad) } const genau300 = `https://example.de/${'a'.repeat(300 - 'https://example.de/'.length)}` assert.equal(genau300.length, 300) assert.equal(gut(link(genau300)).hinweise[0].knopf.ziel, genau300) assert.equal(gut(link('https://example.de/angebot?x=1')).hinweise[0].knopf.ziel, 'https://example.de/angebot?x=1') assert.equal(gut(link(' HTTPS://Beispiel.DE ')).hinweise[0].knopf.ziel, 'https://beispiel.de/') assert.deepEqual(gut(link('https://example.de/')).hinweise[0].knopf, { art: 'link', ziel: 'https://example.de/', text: 'Mehr' }) nurPfad(schlecht(link(`https://example.de/${'a'.repeat(282)}`)), pfad) // 301 Zeichen }) test('Knopf „anruf“: nur Ziffern, Leerzeichen, +, /, -', () => { const anruf = (ziel) => eingabe({ hinweise: [hinweis({ knopf: { art: 'anruf', ziel, text: 'Anrufen' } })] }) const pfad = 'hinweise.h_aaaaaaa1.knopf.ziel' for (const schlechtes of ['0171 ABC', '0171-12345; rm', 'tel:0171', '(06 31) 12 34', '+++', '- / -', '', 5, `0171${' 1'.repeat(15)}`]) { nurPfad(schlecht(anruf(schlechtes)), pfad) } for (const gutes of ['0171 1234567', '+49 171 1234567', '0631/12 34-56']) { assert.equal(gut(anruf(gutes)).hinweise[0].knopf.ziel, gutes) } assert.equal(gut(anruf(' 0171 1234567 ')).hinweise[0].knopf.ziel, '0171 1234567') }) test('Längen aus der Spec', () => { const l = (n) => 'x'.repeat(n) // Hinweistext 140 / 141 assert.ok(gut(eingabe({ hinweise: [hinweis({ text: l(140) })] }))) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ text: l(141) })] })), 'hinweise.h_aaaaaaa1.text') // Zeichen zählen als Codepoints, nicht als UTF-16-Einheiten assert.ok(gut(eingabe({ hinweise: [hinweis({ text: '😀'.repeat(140) })] }))) nurPfad(schlecht(eingabe({ hinweise: [hinweis({ text: '😀'.repeat(141) })] })), 'hinweise.h_aaaaaaa1.text') // ein Text, der nur durch Leerraum nach Zeichen 140 „passt“, ist trotzdem zu lang nurPfad(schlecht(eingabe({ hinweise: [hinweis({ text: `${l(140)} y` })] })), 'hinweise.h_aaaaaaa1.text') // Ausnahmetext 60 / 61 assert.ok(gut(eingabe({ ausnahmen: [urlaub({ text: l(60) })] }))) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ text: l(61) })] })), 'ausnahmen.x_aaaaaaa1.text') // Knopftext 30 / 31 const knopf = (n) => eingabe({ hinweise: [hinweis({ knopf: { art: 'link', ziel: 'https://example.de/', text: l(n) } })] }) assert.ok(gut(knopf(30))) nurPfad(schlecht(knopf(31)), 'hinweise.h_aaaaaaa1.knopf.text') // Starttext 200 / 201 const wa = (starttext) => eingabe({ kontakt: { an: true, telefon: '', adresse: '', whatsapp: { nummer: '0171 1234567', starttext, bestaetigtAt: '2026-10-04T08:00:00.000Z' } } }) assert.ok(gut(wa(l(200)))) nurPfad(schlecht(wa(l(201))), 'kontakt.whatsapp.starttext') // Adresse 200 / 201 const adr = (adresse) => eingabe({ kontakt: { an: true, telefon: '', adresse, whatsapp: null } }) assert.ok(gut(adr(l(200)))) nurPfad(schlecht(adr(l(201))), 'kontakt.adresse') // Telefon 30 / 31 const tel = (telefon) => eingabe({ kontakt: { an: true, telefon, adresse: '', whatsapp: null } }) assert.equal(gut(tel('0171 1234567'.padEnd(30, '-'))).kontakt.telefon, '491711234567') nurPfad(schlecht(tel('0171 1234567'.padEnd(31, '-'))), 'kontakt.telefon') // riesige Texte sind ein Fehler, kein Absturz nurPfad(schlecht(eingabe({ hinweise: [hinweis({ text: l(1000000) })] })), 'hinweise.h_aaaaaaa1.text') }) test('Texte werden bereinigt: Steuerzeichen und unsichtbare Zeichen raus, Umbrüche werden Leerzeichen', () => { const daten = gut(eingabe({ plaene: [plan({ name: ' Mittags​tisch\n\tneu ' })], ausnahmen: [urlaub({ text: 'Be\u0000triebs‮urlaub' })], hinweise: [hinweis({ text: 'Zeile eins\r\nZeile zwei' })], })) assert.equal(daten.plaene[0].name, 'Mittagstisch neu') assert.equal(daten.ausnahmen[0].text, 'Betriebsurlaub') assert.equal(daten.hinweise[0].text, 'Zeile eins Zeile zwei') }) test('Vergangenes (> 30 Tage) wird entfernt', () => { // heute (Berlin) = 2026-10-05, Grenze = 2026-09-05 const daten = gut(eingabe({ ausnahmen: [ urlaub({ id: 'x_alt00001', von: '2026-07-20', bis: '2026-08-01' }), urlaub({ id: 'x_alt00002', von: '2026-09-01', bis: '2026-09-04' }), urlaub({ id: 'x_neu00001', von: '2026-09-01', bis: '2026-09-05' }), urlaub({ id: 'x_neu00002', von: '2026-09-20', bis: '2026-09-25' }), urlaub({ id: 'x_neu00003', von: '2027-01-01', bis: '2027-01-05' }), ], hinweise: [ hinweis({ id: 'h_alt00001', bis: '2026-08-31' }), hinweis({ id: 'h_neu00001', bis: '2026-09-05' }), hinweis({ id: 'h_neu00002', bis: null }), hinweis({ id: 'h_neu00003', von: '2026-01-01' }), // ohne Ende bleibt, bis es gelöscht wird ], feiertage: { '2026-08-01': { modus: 'zu', fenster: [] }, '2026-09-04': { modus: 'zu', fenster: [] }, '2026-09-05': { modus: 'zu', fenster: [] }, '2026-12-25': { modus: 'zu', fenster: [] }, }, })) assert.deepEqual(daten.ausnahmen.map((a) => a.id), ['x_neu00001', 'x_neu00002', 'x_neu00003']) assert.deepEqual(daten.hinweise.map((h) => h.id), ['h_neu00001', 'h_neu00002', 'h_neu00003']) assert.deepEqual(Object.keys(daten.feiertage), ['2026-09-05', '2026-12-25']) }) test('Vergangenes zählt nicht gegen die Höchstzahl, wird aber vorher geprüft', () => { const alt = Array.from({ length: 100 }, (_, i) => urlaub({ id: `x_eeeee${String(i).padStart(3, '0')}`, von: '2026-01-01', bis: '2026-01-02', text: '' })) const neu = urlaub({ id: 'x_neuneuneu', von: '2026-11-01', bis: '2026-11-02' }) const daten = gut(eingabe({ ausnahmen: [...alt, neu] })) assert.deepEqual(daten.ausnahmen.map((a) => a.id), ['x_neuneuneu']) // Fehler in einem alten Eintrag bleiben Fehler (kein stilles Verschlucken) nurPfad(schlecht(eingabe({ ausnahmen: [urlaub({ von: '2026-01-02', bis: '2026-01-01' })] })), 'ausnahmen.x_aaaaaaa1.bis') }) test('Der Tag „heute“ ist der Berliner Kalendertag', () => { // 2026-10-05T22:30Z = Berlin Dienstag 2026-10-06 00:30 -> Grenze 2026-09-06 const spaet = new Date('2026-10-05T22:30:00.000Z') const e = eingabe({ ausnahmen: [urlaub({ id: 'x_aaaaaaa1', von: '2026-09-05', bis: '2026-09-05' }), urlaub({ id: 'x_aaaaaaa2', von: '2026-09-06', bis: '2026-09-06' })] }) assert.deepEqual(gut(e, { now: spaet }).ausnahmen.map((a) => a.id), ['x_aaaaaaa2']) assert.deepEqual(gut(e, { now: NOW }).ausnahmen.map((a) => a.id), ['x_aaaaaaa1', 'x_aaaaaaa2']) }) // ------------------------------------------------------------------ Kontakt und Telefon test('Telefon in allen Schreibweisen', () => { for (const s of ['0171 1234567', '+49 171 1234567', '0049 171 1234567', '0171/1234567', '0171-123 45 67', '+49 (0) 171 1234567', '+49 (0)171 1234567', '(0171) 1234567', '491711234567', '+491711234567', ' 0171 1234567 ']) { assert.equal(telefonZiffern(s), '491711234567', s) } assert.equal(telefonZiffern('(06 31) 12 34'), '496311234') assert.equal(telefonZiffern('0631 12 34'), '496311234') assert.equal(telefonZiffern('+43 660 1234567'), '436601234567') // Ausland bleibt Ausland assert.equal(telefonZiffern('0043 660 1234567'), '436601234567') assert.equal(telefonZiffern('+49 0171 1234567'), '491711234567') // häufiger Tippfehler for (const s of ['0171 ABC', 'abc', '', ' ', null, undefined, 5, '+', '0', '12', '+49', '0171 1234567 ext', '0171+1234567', '++49 171 1234567', '0171 1234567890123456', '0171 123;456']) { assert.equal(telefonZiffern(s), null, String(s)) } }) test('Telefon im Kontakt wird auf Ziffern gebracht, Fehler am Pfad kontakt.telefon', () => { const kontakt = (telefon) => eingabe({ kontakt: { an: true, telefon, adresse: '', whatsapp: null } }) assert.equal(gut(kontakt('0171 1234567')).kontakt.telefon, '491711234567') assert.equal(gut(kontakt('+49 171 1234567')).kontakt.telefon, '491711234567') assert.equal(gut(kontakt('0049 171 1234567')).kontakt.telefon, '491711234567') assert.equal(gut(kontakt('(06 31) 12 34')).kontakt.telefon, '496311234') assert.equal(gut(kontakt('')).kontakt.telefon, '') assert.equal(gut(kontakt(null)).kontakt.telefon, '') assert.equal(gut(kontakt(undefined)).kontakt.telefon, '') assert.equal(gut(kontakt(' ')).kontakt.telefon, '') nurPfad(schlecht(kontakt('0171 ABC')), 'kontakt.telefon') nurPfad(schlecht(kontakt(1234)), 'kontakt.telefon') }) test('Kontakt: Standardwerte, an muss ein Wahrheitswert sein, Adresse einzeilig', () => { const leer = gut(eingabe({ kontakt: undefined })).kontakt assert.deepEqual(leer, { an: false, telefon: '', adresse: '', whatsapp: null }) nurPfad(schlecht(eingabe({ kontakt: { an: 'ja' } })), 'kontakt.an') nurPfad(schlecht(eingabe({ kontakt: 'x' })), 'kontakt') assert.equal(gut(eingabe({ kontakt: { an: true, adresse: 'Hauptstraße 1\n67655 Kaiserslautern' } })).kontakt.adresse, 'Hauptstraße 1 67655 Kaiserslautern') // an: true ohne Angaben ist erlaubt (die öffentliche Antwort zeigt dann keine Leiste) assert.equal(gut(eingabe({ kontakt: { an: true } })).kontakt.an, true) }) test('WhatsApp verlangt Nummer und bestaetigtAt', () => { const wa = (whatsapp) => eingabe({ kontakt: { an: true, telefon: '', adresse: '', whatsapp } }) const ok = { nummer: '0171 1234567', starttext: 'Hallo!', bestaetigtAt: '2026-10-04T08:00:00Z' } const daten = gut(wa(ok)).kontakt.whatsapp assert.deepEqual(daten, { nummer: '491711234567', starttext: 'Hallo!', bestaetigtAt: '2026-10-04T08:00:00.000Z' }) assert.equal(gut(wa(null)).kontakt.whatsapp, null) assert.equal(gut(wa(undefined)).kontakt.whatsapp, null) nurPfad(schlecht(wa({ ...ok, nummer: '' })), 'kontakt.whatsapp.nummer') nurPfad(schlecht(wa({ ...ok, nummer: undefined })), 'kontakt.whatsapp.nummer') nurPfad(schlecht(wa({ ...ok, nummer: '0171 ABC' })), 'kontakt.whatsapp.nummer') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: undefined })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: null })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: 'gestern' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '2026-10-04' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '2026-13-04T08:00:00Z' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '2026-02-30T08:00:00Z' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '2026-10-04T24:00:00Z' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: '2026-10-04T08:00:00' })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa({ ...ok, bestaetigtAt: 1759564800000 })), 'kontakt.whatsapp.bestaetigtAt') nurPfad(schlecht(wa('ja')), 'kontakt.whatsapp') assert.equal(gut(wa({ ...ok, starttext: undefined })).kontakt.whatsapp.starttext, '') }) test('WhatsApp: bestaetigtAt wird aus der Eingabe übernommen und bleibt beim erneuten Speichern gleich', () => { const wa = (bestaetigtAt) => eingabe({ kontakt: { an: true, telefon: '', adresse: '', whatsapp: { nummer: '0171 1234567', starttext: '', bestaetigtAt } } }) const erst = gut(wa('2026-10-04T10:00:00+02:00')).kontakt.whatsapp.bestaetigtAt assert.equal(erst, '2026-10-04T08:00:00.000Z') const nochmal = gut(wa(erst), { bisher: { kontakt: { whatsapp: { bestaetigtAt: erst } } } }).kontakt.whatsapp.bestaetigtAt assert.equal(nochmal, erst) }) // ------------------------------------------------------------------ Größe test('zu viele Daten: verständliche Meldung statt Absturz', () => { const langerLink = `https://example.de/${'a'.repeat(281)}` const ausnahmen = Array.from({ length: 100 }, (_, i) => ({ id: `x_fffff${String(i).padStart(3, '0')}`, art: 'sondertag', von: '2026-11-01', bis: '2026-11-02', text: 'ä'.repeat(60), modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']], plan: P1, })) const hinweise = Array.from({ length: 20 }, (_, i) => hinweis({ id: `h_fffff${String(i).padStart(3, '0')}`, text: 'ö'.repeat(140), knopf: { art: 'link', ziel: langerLink, text: 'm'.repeat(30) } })) const gross = eingabe({ ausnahmen, hinweise }) const f = schlecht(gross) assert.equal(f.daten, 'Zu viele Einträge – bitte lösche alte Ausnahmen oder Hinweise.') assert.deepEqual(Object.keys(f), ['daten']) // dieselben 100 Ausnahmen allein passen noch const klein = gut(eingabe({ ausnahmen })) assert.ok(JSON.stringify(klein).length <= 32768) assert.equal(klein.ausnahmen.length, 100) }) test('Größenprüfung: genau 32768 Zeichen gehen, eines mehr nicht', () => { // Feiertage mit festem Platzbedarf je Eintrag füllen auf, die Adresse (je 1 Zeichen) macht den Rest. const feiertage = (n) => { const f = {} const t = new Date('2026-11-01T00:00:00Z') for (let i = 0; i < n; i += 1) { f[t.toISOString().slice(0, 10)] = { modus: 'zu', fenster: [] } t.setUTCDate(t.getUTCDate() + 1) } return f } const e = (n, laenge) => eingabe({ feiertage: feiertage(n), kontakt: { an: true, telefon: '', adresse: 'a'.repeat(laenge), whatsapp: null } }) const groesse = (n, laenge) => { const r = normalisiereZeiten(e(n, laenge), { now: NOW }) return r.daten ? JSON.stringify(r.daten).length : Infinity } let n = 700 assert.ok(groesse(n, 0) < 32768) while (groesse(n + 1, 0) <= 32768) n += 1 const luecke = 32768 - groesse(n, 0) assert.ok(luecke >= 0 && luecke < 200, `Lücke ${luecke}`) assert.equal(groesse(n, luecke), 32768) const f = schlecht(e(n, luecke + 1)) assert.equal(f.daten, 'Zu viele Einträge – bitte lösche alte Ausnahmen oder Hinweise.') }) // ------------------------------------------------------------------ Team test('navSelektor-Regeln', () => { const ok = (s) => normalisiereTeam({ navSelektor: s, kontaktleisteAus: false, hinweisleisteAus: false }) assert.deepEqual(ok('header.nav'), { team: { navSelektor: 'header.nav', kontaktleisteAus: false, hinweisleisteAus: false } }) for (const gutes of ['#main-nav', 'nav > ul', 'a[href="x"]', "div[data-x='y']", '.a_b-c:first-child', 'header .nav #x']) { assert.equal(ok(gutes).team.navSelektor, gutes, gutes) } for (const schlechtes of ['a{}', 'a;b', '', 'a{b', 'a}', 'x'.repeat(121), 'a,b', 'a\nb', 'a\\b', 'a*b', 'a(b)', 'ä', 'a`b', 'a$b', 5, {}, []]) { const r = ok(schlechtes) assert.equal(r.team, undefined, String(schlechtes)) nurPfad(r.fehler, 'navSelektor') } assert.equal(ok('x'.repeat(120)).team.navSelektor, 'x'.repeat(120)) for (const leer of [null, undefined, '', ' ']) assert.equal(ok(leer).team.navSelektor, null) assert.equal(ok(' header.nav ').team.navSelektor, 'header.nav') }) test('Team: Schalter sind Wahrheitswerte mit Vorgabe aus, unbekannte Schlüssel fallen weg', () => { assert.deepEqual(normalisiereTeam({}), { team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false } }) assert.deepEqual(normalisiereTeam({ navSelektor: 'nav', kontaktleisteAus: true, hinweisleisteAus: true, extra: 1 }), { team: { navSelektor: 'nav', kontaktleisteAus: true, hinweisleisteAus: true }, }) nurPfad(normalisiereTeam({ kontaktleisteAus: 'ja' }).fehler, 'kontaktleisteAus') nurPfad(normalisiereTeam({ hinweisleisteAus: 1 }).fehler, 'hinweisleisteAus') for (const x of [null, undefined, 'x', 5, []]) nurPfad(normalisiereTeam(x).fehler, 'team') const doppelt = normalisiereTeam({ navSelektor: 'a{}', kontaktleisteAus: 'x' }) assert.deepEqual(Object.keys(doppelt.fehler).sort(), ['kontaktleisteAus', 'navSelektor']) })