Files
ClaudeandClaude Opus 5.5 88fb9263f9 Öffnungszeiten: Prüfregeln für Zeiten und Team-Einstellungen
normalisiereZeiten/normalisiereTeam prüfen und normalisieren alles, was Kunde und
Team speichern: Fensterregeln, Ausnahmen, Hinweise, Kontakt, Telefon, Links, Größe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:35:10 +00:00

768 lines
44 KiB
JavaScript
Raw Permalink 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.
This file contains Unicode characters that might be confused with other characters. 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 {
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', '<x>', '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'])
})