import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { hauptplan, planNachKurz, zuUtcOderDanach, tagesbild, oeffnungsIntervalle, } from '../server/services/zeiten/zeitlogik.js' import { ausUtc } from '../server/services/termine/zeit.js' // Fixture: Wochentage 1..7 (Mo..So). Wert: ['09:00','18:00'] = ein Fenster, [[..],[..]] = zwei Fenster, // 'rund' / 'vereinbarung' = Modus, fehlt = zu. function plan(woche = {}, extra = {}) { const tage = {} for (let d = 1; d <= 7; d += 1) { const w = woche[d] if (!w) tage[d] = { modus: 'zu', fenster: [] } else if (typeof w === 'string') tage[d] = { modus: w, fenster: [] } else tage[d] = { modus: 'zeiten', fenster: Array.isArray(w[0]) ? w : [w] } } return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage, ...extra } } function daten({ land = 'RP', plaene = [plan({ 1: ['09:00', '18:00'] })], ausnahmen = [], feiertage = {} } = {}) { return { land, plaene, feiertage, ausnahmen, hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null }, } } function tiefEinfrieren(x) { if (x && typeof x === 'object' && !Object.isFrozen(x)) { Object.freeze(x) for (const v of Object.values(x)) tiefEinfrieren(v) } return x } const urlaub = (extra) => ({ id: 'u', art: 'urlaub', text: 'Betriebsurlaub', modus: 'zu', fenster: [], plan: null, ...extra }) const sondertag = (extra) => ({ id: 's', art: 'sondertag', text: 'Sonderöffnung', modus: 'zeiten', fenster: [['10:00', '14:00']], plan: null, ...extra }) // Berliner Wanduhr eines Zeitpunkts in ms: '2026-10-10 18:00' const berlin = (ms) => { const { tag, uhrzeit } = ausUtc(new Date(ms)) return `${tag} ${uhrzeit}` } const iv = (liste) => liste.map((i) => [berlin(i.vonMs), berlin(i.bisMs)]) // ---------------------------------------------------------------- Plan-Suche test('hauptplan liefert den Plan mit haupt: true, sonst null', () => { const a = plan({}, { id: 'a', kurz: 'sommer', haupt: false }) const b = plan({}, { id: 'b', kurz: 'standard', haupt: true }) assert.equal(hauptplan(daten({ plaene: [a, b] })), b) assert.equal(hauptplan(daten({ plaene: [a] })), null) assert.equal(hauptplan(daten({ plaene: [] })), null) assert.equal(hauptplan({}), null) assert.equal(hauptplan(null), null) assert.equal(hauptplan(undefined), null) assert.equal(hauptplan({ plaene: 'kaputt' }), null) }) test('planNachKurz findet den Plan über den Kurznamen', () => { const a = plan({}, { id: 'a', kurz: 'sommer', haupt: false }) const b = plan({}, { id: 'b', kurz: 'standard', haupt: true }) const d = daten({ plaene: [a, b] }) assert.equal(planNachKurz(d, 'sommer'), a) assert.equal(planNachKurz(d, 'standard'), b) assert.equal(planNachKurz(d, 'winter'), null) assert.equal(planNachKurz(d, 'SOMMER'), null, 'Kurznamen sind [a-z0-9-], kein Case-Folding') assert.equal(planNachKurz(d, undefined), null) assert.equal(planNachKurz(d, 5), null) assert.equal(planNachKurz(null, 'sommer'), null) }) // ---------------------------------------------------------------- zuUtcOderDanach test('März-Lücke rückt auf 03:00', () => { assert.equal(zuUtcOderDanach('2026-03-29', '02:30').toISOString(), '2026-03-29T01:00:00.000Z') assert.equal(zuUtcOderDanach('2026-03-29', '02:00').toISOString(), '2026-03-29T01:00:00.000Z') assert.equal(zuUtcOderDanach('2026-03-29', '02:59').toISOString(), '2026-03-29T01:00:00.000Z') assert.equal(zuUtcOderDanach('2026-03-29', '03:00').toISOString(), '2026-03-29T01:00:00.000Z') assert.equal(zuUtcOderDanach('2026-03-29', '01:59').toISOString(), '2026-03-29T00:59:00.000Z') }) test('Oktober: doppelte Stunde nimmt das erste Vorkommen', () => { assert.equal(zuUtcOderDanach('2026-10-25', '02:30').toISOString(), '2026-10-25T00:30:00.000Z') assert.equal(zuUtcOderDanach('2026-10-25', '03:00').toISOString(), '2026-10-25T02:00:00.000Z') }) test('zuUtcOderDanach: normale Tage wie zuUtc, 24:00 ist der Folgetag 00:00', () => { assert.equal(zuUtcOderDanach('2026-10-05', '09:00').toISOString(), '2026-10-05T07:00:00.000Z') assert.equal(zuUtcOderDanach('2026-01-15', '09:00').toISOString(), '2026-01-15T08:00:00.000Z') assert.equal(zuUtcOderDanach('2026-10-05', '24:00').toISOString(), '2026-10-05T22:00:00.000Z') assert.equal(zuUtcOderDanach('2026-10-24', '24:00').toISOString(), '2026-10-24T22:00:00.000Z', 'Mitternacht vor der Umstellung: Sommerzeit') assert.equal(zuUtcOderDanach('2026-10-25', '24:00').toISOString(), '2026-10-25T23:00:00.000Z', 'Mitternacht nach der Umstellung: Winterzeit') assert.equal(zuUtcOderDanach('2026-12-31', '24:00').toISOString(), '2026-12-31T23:00:00.000Z') assert.equal(zuUtcOderDanach('2026-03-28', '24:00').toISOString(), '2026-03-28T23:00:00.000Z') }) test('zuUtcOderDanach wirft bei ungültiger Eingabe (nur die Lücke wird überbrückt)', () => { assert.throws(() => zuUtcOderDanach('2026-02-30', '09:00'), RangeError) assert.throws(() => zuUtcOderDanach('2026-10-05', '25:00'), RangeError) assert.throws(() => zuUtcOderDanach('2026-10-05', '9:00'), RangeError) assert.throws(() => zuUtcOderDanach('2026-10-05', '24:30'), RangeError) assert.throws(() => zuUtcOderDanach(undefined, '09:00'), RangeError) assert.throws(() => zuUtcOderDanach('2026-10-05'), RangeError) }) // ---------------------------------------------------------------- tagesbild: Vorrang test('Wochenplan an einem normalen Montag', () => { const d = daten() assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-05'), { tag: '2026-10-05', art: 'regulaer', name: null, modus: 'zeiten', fenster: [['09:00', '18:00']], }) }) test('Wochenplan: zwei Fenster, geschlossene Tage, rund und nach Vereinbarung', () => { const p = plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 3: 'rund', 4: 'vereinbarung' }) const d = daten({ plaene: [p] }) assert.deepEqual(tagesbild(d, p, '2026-10-05').fenster, [['09:00', '12:30'], ['13:30', '18:00']]) assert.deepEqual(tagesbild(d, p, '2026-10-06'), { tag: '2026-10-06', art: 'regulaer', name: null, modus: 'zu', fenster: [] }) assert.deepEqual(tagesbild(d, p, '2026-10-07'), { tag: '2026-10-07', art: 'regulaer', name: null, modus: 'rund', fenster: [] }) assert.deepEqual(tagesbild(d, p, '2026-10-08'), { tag: '2026-10-08', art: 'regulaer', name: null, modus: 'vereinbarung', fenster: [] }) assert.equal(tagesbild(d, p, '2026-10-11').modus, 'zu', 'Sonntag ohne Eintrag') }) test('fehlende oder kaputte Tageseinträge ergeben „zu“, kaputte Fenster fallen heraus', () => { const p = { id: 'p1', kurz: 'standard', name: 'S', haupt: true, tage: { 1: { modus: 'zeiten', fenster: [['9:00', '18:00'], ['09:00'], 'x', ['10:00', '10:00'], ['24:00', '02:00'], ['09:00', '25:00']] }, 2: { modus: 'unbekannt', fenster: [['09:00', '18:00']] }, 3: { modus: 'zeiten', fenster: [['09:00', '12:00'], ['kaputt', '18:00']] } } } const d = daten({ plaene: [p] }) assert.deepEqual(tagesbild(d, p, '2026-10-05'), { tag: '2026-10-05', art: 'regulaer', name: null, modus: 'zu', fenster: [] }, 'Modus zeiten ohne brauchbares Fenster = zu') assert.equal(tagesbild(d, p, '2026-10-06').modus, 'zu', 'unbekannter Modus') assert.deepEqual(tagesbild(d, p, '2026-10-07').fenster, [['09:00', '12:00']]) assert.equal(tagesbild(d, p, '2026-10-08').modus, 'zu', 'Tag fehlt') assert.equal(tagesbild(d, { id: 'x', tage: null }, '2026-10-05').modus, 'zu') assert.equal(tagesbild(d, null, '2026-10-05').modus, 'zu', 'ohne Plan nur Ausnahmen') }) test('Fenster werden nach Beginn sortiert und kopiert', () => { const p = plan({ 1: [['13:30', '18:00'], ['09:00', '12:30']] }) const d = daten({ plaene: [p] }) const t = tagesbild(d, p, '2026-10-05') assert.deepEqual(t.fenster, [['09:00', '12:30'], ['13:30', '18:00']]) t.fenster[0][0] = '00:00' t.fenster.push(['20:00', '21:00']) assert.deepEqual(tagesbild(d, p, '2026-10-05').fenster, [['09:00', '12:30'], ['13:30', '18:00']], 'Rückgabe teilt keine Objekte mit den Daten') assert.deepEqual(p.tage[1].fenster, [['13:30', '18:00'], ['09:00', '12:30']], 'Eingabe bleibt unverändert') }) test('Feiertag ist zu, mit Namen', () => { const d = daten() assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-03'), { tag: '2026-10-03', art: 'feiertag', name: 'Tag der Deutschen Einheit', modus: 'zu', fenster: [], }) }) test('Feiertag gilt auch an einem Tag, an dem der Plan offen wäre', () => { const p = plan({ 6: ['10:00', '16:00'] }) const d = daten({ plaene: [p] }) const t = tagesbild(d, p, '2026-10-03') // Samstag assert.equal(t.art, 'feiertag') assert.equal(t.modus, 'zu') }) test('Feiertag hängt vom Bundesland ab; fehlendes oder ungültiges Land = RP', () => { const p = plan({ 1: ['09:00', '18:00'] }) // Fronleichnam 2026 = 04.06. (Donnerstag): in RP gesetzlich, in BE nicht const ohneLand = daten({ plaene: [p] }) delete ohneLand.land assert.equal(tagesbild(daten({ land: 'RP', plaene: [p] }), p, '2026-06-04').art, 'feiertag') assert.equal(tagesbild(daten({ land: 'BE', plaene: [p] }), p, '2026-06-04').art, 'regulaer') assert.equal(tagesbild(ohneLand, p, '2026-06-04').art, 'feiertag', 'Vorgabe RP') assert.equal(tagesbild(daten({ land: 'XX', plaene: [p] }), p, '2026-06-04').art, 'feiertag', 'ungültiges Land: Vorgabe RP') assert.equal(tagesbild(daten({ land: 'rp', plaene: [p] }), p, '2026-06-04').art, 'feiertag', 'Kleinschreibung: Vorgabe RP') assert.equal(tagesbild({ plaene: [p] }, p, '2026-10-03').name, 'Tag der Deutschen Einheit', 'daten ohne Land, Ausnahmen und Feiertage') }) test('Feiertags-Abweichung gilt', () => { const d = daten({ feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } }) assert.deepEqual(tagesbild(d, hauptplan(d), '2026-10-03'), { tag: '2026-10-03', art: 'feiertag', name: 'Tag der Deutschen Einheit', modus: 'zeiten', fenster: [['10:00', '14:00']], }) const rund = daten({ feiertage: { '2026-10-03': { modus: 'rund', fenster: [] } } }) assert.equal(tagesbild(rund, hauptplan(rund), '2026-10-03').modus, 'rund') const verein = daten({ feiertage: { '2026-10-03': { modus: 'vereinbarung', fenster: [] } } }) assert.equal(tagesbild(verein, hauptplan(verein), '2026-10-03').modus, 'vereinbarung') }) test('Feiertags-Abweichung auf einem Tag ohne Feiertag oder mit kaputtem Inhalt wirkt nicht', () => { const d = daten({ feiertage: { '2026-10-05': { modus: 'zu', fenster: [] }, '2026-10-03': { modus: 'kaputt' } } }) const p = hauptplan(d) assert.equal(tagesbild(d, p, '2026-10-05').modus, 'zeiten', 'kein Feiertag: Wochenplan') assert.equal(tagesbild(d, p, '2026-10-03').modus, 'zu', 'kaputte Abweichung: Vorgabe des Feiertags') assert.equal(tagesbild(d, p, '2026-10-03').art, 'feiertag') }) test('Prüftag folgt dem Wochenplan', () => { const p = plan({ 4: ['09:00', '16:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(tagesbild(d, p, '2026-12-24'), { tag: '2026-12-24', art: 'regulaer', name: null, modus: 'zeiten', fenster: [['09:00', '16:00']], }) // Silvester 2026 (Do) ebenso; 1. Weihnachtstag 2026 (Fr) ist gesetzlich assert.equal(tagesbild(d, p, '2026-12-31').art, 'regulaer') assert.equal(tagesbild(d, p, '2026-12-25').art, 'feiertag') // Mariä Himmelfahrt: in BY ein Prüftag, in SL gesetzlich const by = daten({ land: 'BY', plaene: [plan({ 6: ['09:00', '12:00'] })] }) assert.equal(tagesbild(by, by.plaene[0], '2026-08-15').art, 'regulaer') const sl = daten({ land: 'SL', plaene: [plan({ 6: ['09:00', '12:00'] })] }) assert.equal(tagesbild(sl, sl.plaene[0], '2026-08-15').art, 'feiertag') }) test('Prüftag mit gespeicherter Abweichung: die Abweichung gilt, der Name bleibt', () => { const p = plan({ 4: ['09:00', '18:00'] }) const d = daten({ plaene: [p], feiertage: { '2026-12-24': { modus: 'zeiten', fenster: [['09:00', '13:00']] } } }) assert.deepEqual(tagesbild(d, p, '2026-12-24'), { tag: '2026-12-24', art: 'feiertag', name: 'Heiligabend', modus: 'zeiten', fenster: [['09:00', '13:00']], }) const zu = daten({ plaene: [p], feiertage: { '2026-12-24': { modus: 'zu', fenster: [] } } }) assert.equal(tagesbild(zu, p, '2026-12-24').modus, 'zu') }) test('Urlaub schlägt Feiertag, Sondertag schlägt Urlaub', () => { const p = plan({ 1: ['09:00', '18:00'], 6: ['10:00', '16:00'] }) // Feiertag Sa 03.10.: Urlaub zu; Sondertag offen const nurUrlaub = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06', text: 'Herbstpause' })] }) assert.deepEqual(tagesbild(nurUrlaub, p, '2026-10-03'), { tag: '2026-10-03', art: 'urlaub', name: 'Herbstpause', modus: 'zu', fenster: [], }) const urlaubUndFeiertagsAbweichung = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06' })], feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } }, }) assert.equal(tagesbild(urlaubUndFeiertagsAbweichung, p, '2026-10-03').art, 'urlaub', 'Urlaub schlägt auch die Feiertags-Abweichung') const mitSondertag = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-06' }), sondertag({ von: '2026-10-03', bis: '2026-10-03', text: 'Tag der offenen Tür' })], }) assert.deepEqual(tagesbild(mitSondertag, p, '2026-10-03'), { tag: '2026-10-03', art: 'sondertag', name: 'Tag der offenen Tür', modus: 'zeiten', fenster: [['10:00', '14:00']], }) assert.equal(tagesbild(mitSondertag, p, '2026-10-05').art, 'urlaub', 'Montag im Urlaub, kein Sondertag') assert.equal(tagesbild(mitSondertag, p, '2026-10-07').art, 'regulaer', 'nach dem Urlaub') assert.equal(tagesbild(mitSondertag, p, '2026-09-30').art, 'regulaer', 'vor dem Urlaub') }) test('Urlaub ist immer zu, auch wenn der gespeicherte Modus etwas anderes sagt', () => { const p = plan({ 1: ['09:00', '18:00'] }) const d = daten({ plaene: [p], ausnahmen: [{ id: 'u', art: 'urlaub', von: '2026-10-05', bis: '2026-10-05', plan: null }] }) const t = tagesbild(d, p, '2026-10-05') assert.equal(t.art, 'urlaub') assert.equal(t.modus, 'zu') assert.deepEqual(t.fenster, []) assert.equal(t.name, null, 'ohne Text kein Name') }) test('Sondertag mit Modus zu, rund und nach Vereinbarung', () => { const p = plan({ 1: ['09:00', '18:00'] }) for (const modus of ['zu', 'rund', 'vereinbarung']) { const d = daten({ plaene: [p], ausnahmen: [sondertag({ von: '2026-10-05', bis: '2026-10-05', modus, fenster: [] })] }) const t = tagesbild(d, p, '2026-10-05') assert.equal(t.art, 'sondertag') assert.equal(t.modus, modus) assert.deepEqual(t.fenster, []) } }) test('Ausnahme gilt von bis bis einschließlich, Grenzen inklusive', () => { const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 4: ['09:00', '18:00'] }) const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-07' })] }) assert.equal(tagesbild(d, p, '2026-10-05').art, 'regulaer') assert.equal(tagesbild(d, p, '2026-10-06').art, 'urlaub') assert.equal(tagesbild(d, p, '2026-10-07').art, 'urlaub') assert.equal(tagesbild(d, p, '2026-10-08').art, 'regulaer') }) test('planbezogene Ausnahme schlägt globale gleicher Art', () => { const sommer = plan({ 1: ['09:00', '18:00'] }, { id: 'p-sommer', kurz: 'sommer', haupt: false }) const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true }) const ausnahmen = [ // planbezogene Ausnahme steht VOR der globalen, schlägt sie trotzdem urlaub({ id: 'u1', von: '2026-10-05', bis: '2026-10-09', text: 'Nur Sommerplan', plan: 'p-sommer' }), urlaub({ id: 'u2', von: '2026-10-05', bis: '2026-10-09', text: 'Alle Pläne', plan: null }), ] const d = daten({ plaene: [haupt, sommer], ausnahmen }) assert.equal(tagesbild(d, sommer, '2026-10-05').name, 'Nur Sommerplan') assert.equal(tagesbild(d, haupt, '2026-10-05').name, 'Alle Pläne') }) test('Ausnahme eines anderen Plans gilt nicht', () => { const sommer = plan({ 1: ['09:00', '18:00'] }, { id: 'p-sommer', kurz: 'sommer', haupt: false }) const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true }) const d = daten({ plaene: [haupt, sommer], ausnahmen: [urlaub({ von: '2026-10-05', bis: '2026-10-05', plan: 'p-sommer' })] }) assert.equal(tagesbild(d, sommer, '2026-10-05').art, 'urlaub') assert.equal(tagesbild(d, haupt, '2026-10-05').art, 'regulaer') }) test('Die Art schlägt die Planbezogenheit: globaler Sondertag vor planbezogenem Urlaub', () => { const haupt = plan({ 1: ['09:00', '18:00'] }, { id: 'p-haupt', kurz: 'standard', haupt: true }) const d = daten({ plaene: [haupt], ausnahmen: [ urlaub({ id: 'u1', von: '2026-10-05', bis: '2026-10-05', plan: 'p-haupt' }), sondertag({ id: 's1', von: '2026-10-05', bis: '2026-10-05', plan: null, text: 'Alle' }), ], }) assert.equal(tagesbild(d, haupt, '2026-10-05').art, 'sondertag') }) test('zwei überlappende Urlaube: der später angelegte gilt', () => { const p = plan({ 1: ['09:00', '18:00'] }) const a = urlaub({ id: 'a', von: '2026-10-05', bis: '2026-10-12', text: 'Erster' }) const b = urlaub({ id: 'b', von: '2026-10-05', bis: '2026-10-07', text: 'Zweiter' }) assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-05').name, 'Zweiter') assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [b, a] }), p, '2026-10-05').name, 'Erster', 'höherer Index = später angelegt') assert.equal(tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-10').name, 'Erster', 'außerhalb des späteren gilt der frühere') }) test('zwei überlappende Sondertage: der später angelegte gilt', () => { const p = plan({ 1: ['09:00', '18:00'] }) const a = sondertag({ id: 'a', von: '2026-10-05', bis: '2026-10-05', text: 'Erster', fenster: [['10:00', '12:00']] }) const b = sondertag({ id: 'b', von: '2026-10-05', bis: '2026-10-05', text: 'Zweiter', fenster: [['15:00', '17:00']] }) const t = tagesbild(daten({ plaene: [p], ausnahmen: [a, b] }), p, '2026-10-05') assert.equal(t.name, 'Zweiter') assert.deepEqual(t.fenster, [['15:00', '17:00']]) }) test('Urlaub über den Jahreswechsel', () => { const p = plan({ 6: ['10:00', '16:00'], 7: ['10:00', '16:00'], 1: ['09:00', '18:00'] }) const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-12-28', bis: '2027-01-03' })] }) for (const tag of ['2026-12-28', '2026-12-31', '2027-01-01', '2027-01-02', '2027-01-03']) { assert.equal(tagesbild(d, p, tag).art, 'urlaub', tag) } assert.equal(tagesbild(d, p, '2026-12-27').art, 'regulaer') assert.equal(tagesbild(d, p, '2027-01-04').art, 'regulaer') }) test('Ausnahmen mit fehlendem oder umgekehrtem Zeitraum werden ignoriert, fehlendes bis = ein Tag', () => { const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'] }) const d = daten({ plaene: [p], ausnahmen: [ { id: 'x', art: 'urlaub', von: '2026-10-06', bis: '2026-10-05', modus: 'zu', fenster: [], plan: null }, { id: 'y', art: 'urlaub', von: undefined, bis: undefined, modus: 'zu', fenster: [], plan: null }, { id: 'z', art: 'ferien', von: '2026-10-05', bis: '2026-10-05', modus: 'zu', fenster: [], plan: null }, { id: 'w', art: 'urlaub', von: '2026-10-06', text: 'ein Tag', modus: 'zu', fenster: [], plan: null }, null, ], }) assert.equal(tagesbild(d, p, '2026-10-05').art, 'regulaer') assert.equal(tagesbild(d, p, '2026-10-06').art, 'urlaub') assert.equal(tagesbild(d, p, '2026-10-07').art, 'regulaer') }) test('tagesbild rechnet nichts in die Daten zurück und wirft bei ungültigem Tag', () => { const d = tiefEinfrieren(daten({ ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-07' })], feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } })) const p = hauptplan(d) assert.doesNotThrow(() => tagesbild(d, p, '2026-10-03')) assert.doesNotThrow(() => tagesbild(d, p, '2026-10-06')) assert.doesNotThrow(() => oeffnungsIntervalle(d, p, '2026-10-01', '2026-10-10')) for (const tag of [undefined, null, '', '2026-10-3', '03.10.2026', '2026-02-30', 5]) { assert.throws(() => tagesbild(d, p, tag), RangeError, String(tag)) } }) // ---------------------------------------------------------------- oeffnungsIntervalle test('einfaches Fenster wird ein Intervall in Berliner Zeit', () => { const d = daten() const r = oeffnungsIntervalle(d, hauptplan(d), '2026-10-05', '2026-10-05') assert.deepEqual(r, [{ vonMs: Date.parse('2026-10-05T07:00:00.000Z'), bisMs: Date.parse('2026-10-05T16:00:00.000Z') }]) }) test('zwei Fenster am Tag bleiben getrennt, Wochen über mehrere Tage', () => { const p = plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 2: ['09:00', '12:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11')), [ ['2026-10-05 09:00', '2026-10-05 12:30'], ['2026-10-05 13:30', '2026-10-05 18:00'], ['2026-10-06 09:00', '2026-10-06 12:00'], ]) }) test('aneinander grenzende Fenster werden zusammengefügt', () => { const p = plan({ 1: [['09:00', '12:00'], ['12:00', '18:00']] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-05')), [['2026-10-05 09:00', '2026-10-05 18:00']]) }) test('Fenster über Mitternacht gehört zum Starttag', () => { // Bar: Samstag 18:00–02:00; Sonntag ist (in Brandenburg) Feiertag, Ostersonntag 2026-04-05 const p = plan({ 6: ['18:00', '02:00'] }) const d = daten({ land: 'BB', plaene: [p] }) assert.equal(tagesbild(d, p, '2026-04-05').art, 'feiertag') const r = oeffnungsIntervalle(d, p, '2026-04-04', '2026-04-04') assert.equal(r.length, 1) assert.equal(new Date(r[0].vonMs).toISOString(), '2026-04-04T16:00:00.000Z') assert.equal(new Date(r[0].bisMs).toISOString(), '2026-04-05T00:00:00.000Z') assert.deepEqual(iv(r), [['2026-04-04 18:00', '2026-04-05 02:00']]) // auch wenn der Folgetag Urlaub oder ein geschlossener Sondertag ist const urlaubSo = daten({ land: 'RP', plaene: [p], ausnahmen: [urlaub({ von: '2026-10-11', bis: '2026-10-11' })] }) assert.deepEqual(iv(oeffnungsIntervalle(urlaubSo, p, '2026-10-10', '2026-10-11')), [['2026-10-10 18:00', '2026-10-11 02:00']]) }) test('Intervalle enthalten das in vonTag hineinlaufende Fenster des Vortags', () => { const p = plan({ 6: ['18:00', '02:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-11', '2026-10-11')), [['2026-10-10 18:00', '2026-10-11 02:00']]) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-12', '2026-10-12')), [], 'Fenster endet vor vonTag: nicht dabei') }) test('Fenster, das genau um 00:00 endet, läuft nicht in vonTag hinein', () => { const p = plan({ 6: ['18:00', '24:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-11', '2026-10-11')), []) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-10', '2026-10-10')), [['2026-10-10 18:00', '2026-10-11 00:00']]) }) test('Nächtliches Fenster am letzten Tag des Bereichs läuft bis zu seinem Ende', () => { const p = plan({ 6: ['18:00', '02:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-10', '2026-10-10')), [['2026-10-10 18:00', '2026-10-11 02:00']]) }) test('Mitternachtsfenster 18:00–00:00 und Folgetag ab 00:00 werden ein Intervall', () => { const p = plan({ 1: ['18:00', '00:00'], 2: ['00:00', '06:00'] }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-06')), [['2026-10-05 18:00', '2026-10-06 06:00']]) }) test('rund um die Uhr an zwei Tagen wird ein Intervall', () => { const p = plan({ 1: 'rund', 2: 'rund' }) const d = daten({ plaene: [p] }) const r = oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11') assert.equal(r.length, 1) assert.deepEqual(iv(r), [['2026-10-05 00:00', '2026-10-07 00:00']]) assert.equal(new Date(r[0].vonMs).toISOString(), '2026-10-04T22:00:00.000Z') assert.equal(new Date(r[0].bisMs).toISOString(), '2026-10-06T22:00:00.000Z') }) test('rund und ein Fenster am Folgetag werden zusammengefügt, nicht benachbarte bleiben getrennt', () => { const p = plan({ 1: 'rund', 2: ['00:00', '08:00'], 4: 'rund' }) const d = daten({ plaene: [p] }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11')), [ ['2026-10-05 00:00', '2026-10-06 08:00'], ['2026-10-08 00:00', '2026-10-09 00:00'], ]) }) test('nach Vereinbarung und zu erzeugen kein Intervall', () => { const p = plan({ 1: 'vereinbarung', 2: 'zu' }) const d = daten({ plaene: [p] }) assert.deepEqual(oeffnungsIntervalle(d, p, '2026-10-05', '2026-10-11'), []) }) test('Feiertag, Urlaub und Sondertag wirken auf die Intervalle', () => { const p = plan({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 6: ['10:00', '16:00'] }) const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-10-06', bis: '2026-10-06' }), sondertag({ von: '2026-10-07', bis: '2026-10-07', fenster: [['12:00', '13:00']] })], }) assert.deepEqual(iv(oeffnungsIntervalle(d, p, '2026-10-03', '2026-10-07')), [ // 03.10. Feiertag (zu), 05.10. regulär, 06.10. Urlaub, 07.10. Sondertag ['2026-10-05 09:00', '2026-10-05 18:00'], ['2026-10-07 12:00', '2026-10-07 13:00'], ]) }) test('Feiertags-Abweichung öffnet an einem Feiertag', () => { const d = daten({ feiertage: { '2026-10-03': { modus: 'zeiten', fenster: [['10:00', '14:00']] } } }) assert.deepEqual(iv(oeffnungsIntervalle(d, hauptplan(d), '2026-10-03', '2026-10-03')), [['2026-10-03 10:00', '2026-10-03 14:00']]) }) test('Sommerzeit: Fenster in der März-Lücke rückt auf 03:00, Länge in echter Zeit', () => { // So 29.03.2026: 02:00–03:00 springt, Fenster 02:30–05:00 beginnt um 03:00 (01:00Z) und endet 05:00 CEST (03:00Z) const p = plan({ 7: ['02:30', '05:00'] }) const d = daten({ plaene: [p] }) const r = oeffnungsIntervalle(d, p, '2026-03-29', '2026-03-29') assert.equal(new Date(r[0].vonMs).toISOString(), '2026-03-29T01:00:00.000Z') assert.equal(new Date(r[0].bisMs).toISOString(), '2026-03-29T03:00:00.000Z') // Fenster, das ganz in der Lücke liegt, verschwindet const lueckenfenster = plan({ 7: ['02:10', '02:50'] }) assert.deepEqual(oeffnungsIntervalle(daten({ plaene: [lueckenfenster] }), lueckenfenster, '2026-03-29', '2026-03-29'), []) // Ende in der Lücke: bis 02:30 ⇒ 03:00 const endeInLuecke = plan({ 7: ['00:00', '02:30'] }) const r2 = oeffnungsIntervalle(daten({ plaene: [endeInLuecke] }), endeInLuecke, '2026-03-29', '2026-03-29') assert.equal(new Date(r2[0].bisMs).toISOString(), '2026-03-29T01:00:00.000Z') }) test('Sommerzeit: Tag „rund“ ist am Umstellungstag 23 bzw. 25 Stunden lang', () => { const p = plan({ 7: 'rund' }) const d = daten({ plaene: [p] }) const stunden = (tag) => { const [i] = oeffnungsIntervalle(d, p, tag, tag) return (i.bisMs - i.vonMs) / 3600000 } assert.equal(stunden('2026-03-29'), 23) assert.equal(stunden('2026-10-25'), 25) assert.equal(stunden('2026-10-18'), 24) }) test('Oktober: Fenster 02:00–06:00 am Umstellungstag, doppelte Stunde zählt mit', () => { const p = plan({ 7: ['02:00', '06:00'] }) const d = daten({ plaene: [p] }) const [i] = oeffnungsIntervalle(d, p, '2026-10-25', '2026-10-25') assert.equal(new Date(i.vonMs).toISOString(), '2026-10-25T00:00:00.000Z', '02:00 Sommerzeit') assert.equal(new Date(i.bisMs).toISOString(), '2026-10-25T05:00:00.000Z', '06:00 Winterzeit') assert.equal((i.bisMs - i.vonMs) / 3600000, 5) }) test('Nächtliches Fenster über die Zeitumstellung: Ende in der Wanduhr des Folgetags', () => { const p = plan({ 6: ['22:00', '02:00'] }) // Sa 24.10. auf So 25.10. (Ende der Sommerzeit) const d = daten({ plaene: [p] }) const [i] = oeffnungsIntervalle(d, p, '2026-10-24', '2026-10-24') assert.equal(new Date(i.vonMs).toISOString(), '2026-10-24T20:00:00.000Z') assert.equal(new Date(i.bisMs).toISOString(), '2026-10-25T00:00:00.000Z', '02:00 erstes Vorkommen = Sommerzeit') }) test('Intervalle: Bereichsgrenzen, umgekehrter Bereich, ungültige Tage', () => { const d = daten() const p = hauptplan(d) assert.deepEqual(oeffnungsIntervalle(d, p, '2026-10-06', '2026-10-05'), []) assert.throws(() => oeffnungsIntervalle(d, p, 'x', '2026-10-05'), RangeError) assert.throws(() => oeffnungsIntervalle(d, p, '2026-10-05', undefined), RangeError) assert.throws(() => oeffnungsIntervalle(d, p, '2026-02-30', '2026-03-01'), RangeError) }) test('Intervalle über 60 Tage und mehrere Jahre sind sortiert und überschneidungsfrei', () => { const p = plan({ 1: [['09:00', '12:00'], ['13:00', '22:00']], 2: ['09:00', '12:00'], 4: ['20:00', '03:00'], 5: 'rund', 6: ['00:00', '08:00'] }) const d = daten({ plaene: [p], ausnahmen: [urlaub({ von: '2026-12-28', bis: '2027-01-03' })] }) const r = oeffnungsIntervalle(d, p, '2026-12-01', '2027-02-15') assert.ok(r.length > 20) for (let k = 0; k < r.length; k += 1) { assert.ok(r[k].bisMs > r[k].vonMs) if (k > 0) assert.ok(r[k].vonMs > r[k - 1].bisMs, 'sortiert, mit Lücke (grenzende wurden zusammengefügt)') } // im Urlaub (28.12.–03.01.) beginnt kein Fenster, ein hineinlaufendes des 27.12. ist erlaubt const imUrlaub = r.filter((i) => i.vonMs >= Date.parse('2026-12-27T23:00:00Z') && i.vonMs < Date.parse('2027-01-03T23:00:00Z')) assert.deepEqual(imUrlaub, []) }) test('Rückgabe der Intervalle ist reine Zahlen-Struktur ohne Seiteneffekt auf die Daten', () => { const d = tiefEinfrieren(daten({ plaene: [plan({ 1: [['09:00', '12:00'], ['13:00', '18:00']], 6: ['22:00', '02:00'] })] })) const r = oeffnungsIntervalle(d, hauptplan(d), '2026-10-05', '2026-10-11') assert.ok(r.every((i) => Object.keys(i).sort().join() === 'bisMs,vonMs' && Number.isInteger(i.vonMs) && Number.isInteger(i.bisMs))) }) // ---------------------------------------------------------------- Reinheit test('zeitlogik.js ist rein: keine Appwrite- und keine Config-Importe, keine Millisekunden-Tagesrechnung', () => { const quelle = readFileSync(new URL('../server/services/zeiten/zeitlogik.js', import.meta.url), 'utf8') const importe = [...quelle.matchAll(/^import[\s\S]*?from\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]) assert.deepEqual(importe.sort(), ['../termine/feiertage.js', '../termine/zeit.js']) assert.doesNotMatch(quelle, /86400000|864e5/, 'Kalendertage nie mit +86400000') assert.doesNotMatch(quelle, /Date\.now|new Date\(\)/, 'now kommt immer als Parameter') })