import { test } from 'node:test' import assert from 'node:assert/strict' import { ETAGE_FUER, consentVon, anfrageStatusVon, einschaltWeg, tagNachFrist, datumKurz, beginnLage, einschaltKoerper, linkzeile, sichereAdresse, fehlerAktion } from '../public/neu/funktion-einschalten.js' import { daten, katalogEintrag, projekt, feature } from './support/knFunktionenDaten.js' const abo = (produkt, extra = {}) => ({ produkt, laeuft: produkt !== null, gekuendigt: false, widerrufsfristBis: null, ...extra }) const angenommen = (key) => projekt('p1', 'B', { [key]: feature('off', { anfrageStatus: 'angenommen' }) }) const GEKUENDIGT = 'Dein Abo ist gekündigt. Solange die Kündigung läuft, kannst du nichts Neues einschalten.' const NICHT_AKTIV = 'Dein Abo ist noch nicht aktiv. Sobald die erste Zahlung da ist, kannst du einschalten.' const NUR_BETRIEB = 'Webshop gibt es für Betriebe. Stimmt das nicht? Schreib uns im Chat.' test('Etagen-Tabelle und Zugriffsfunktionen', () => { assert.deepEqual(ETAGE_FUER, { news: 'news', termine: 'termine', shop: 'webshop', unterkunft: 'buchungen', zeiten: 'zeiten', analyse: 'analyse' }) assert.equal(consentVon(daten(), 'news').version, '2026-10-20') assert.equal(consentVon(daten({ katalogEdit: { analyse: { consent: null } } }), 'analyse'), null) assert.equal(consentVon(daten(), 'gibtsnicht'), null) assert.equal(anfrageStatusVon(angenommen('shop'), 'shop'), 'angenommen') assert.equal(anfrageStatusVon(projekt('p1', 'B'), 'shop'), '') }) test('einschaltWeg: Vorprüfung in der Reihenfolge des Servers (V5)', () => { const faelle = [ // [erwartet, key, abo, kundenart, projekt, Katalogänderung] [{ art: 'meldung', text: 'Diese Funktion ist für dich gerade nicht verfügbar.' }, 'news', abo('wartung'), 'unternehmen', undefined, { verfuegbar: false }], [{ art: 'meldung', text: GEKUENDIGT }, 'news', abo('wartung', { gekuendigt: true })], [{ art: 'bestellen', produkt: 'wartung' }, 'news', abo(null)], [{ art: 'bestellen', produkt: 'hosting' }, 'analyse', abo(null)], [{ art: 'meldung', text: NICHT_AKTIV }, 'news', abo('wartung', { laeuft: false })], [{ art: 'wechsel' }, 'news', abo('hosting')], [{ art: 'etage', etage: 'analyse' }, 'analyse', abo('hosting')], [{ art: 'meldung', text: NUR_BETRIEB }, 'shop', abo('wartung'), 'privat'], [{ art: 'meldung', text: NUR_BETRIEB }, 'shop', abo('wartung'), ''], // leere Kundenart zählt als privat (E4) [{ art: 'seite' }, 'news', abo('wartung')], [{ art: 'seite' }, 'zeiten', abo('wartung')], [{ art: 'etage', etage: 'termine' }, 'termine', abo('wartung')], [{ art: 'etage', etage: 'webshop' }, 'shop', abo('wartung')], [{ art: 'etage', etage: 'buchungen' }, 'unterkunft', abo('wartung')], [{ art: 'seite' }, 'shop', abo('wartung'), 'unternehmen', angenommen('shop')], [{ art: 'seite' }, 'unterkunft', abo('wartung'), 'unternehmen', angenommen('unterkunft')], ] for (const [erwartet, key, a, kundenart = 'unternehmen', p = projekt('p1', 'B'), edit = {}] of faelle) { assert.deepEqual(einschaltWeg({ eintrag: katalogEintrag(key, edit), projekt: p, abo: a, kundenart }), erwartet, `${key} ${JSON.stringify(a)} ${kundenart}`) } }) test('tagNachFrist und datumKurz: Berliner Folgetag, Sommer- und Winterzeit', () => { assert.equal(tagNachFrist('2026-10-30T22:59:59.999Z'), '2026-10-31') // 23:59:59 MEZ assert.equal(tagNachFrist('2026-08-14T21:59:59.999Z'), '2026-08-15') // 23:59:59 MESZ assert.equal(tagNachFrist('2026-12-31T22:59:59.999Z'), '2027-01-01') assert.deepEqual([tagNachFrist(null), tagNachFrist('kein Datum'), datumKurz('2026-10-31'), datumKurz('')], ['', '', '31.10.', '']) }) test('beginnLage: Häkchen nur für privat bis zum Fristende; die Antwort des Servers gewinnt', () => { const jetzt = new Date('2026-10-20T10:00:00Z') const frist = '2026-10-30T22:59:59.999Z' const lage = (kundenart, widerrufsfristBis, extra = {}) => beginnLage({ kundenart, abo: { widerrufsfristBis }, jetzt, ...extra }) assert.deepEqual(lage('privat', frist), { noetig: true, ab: '2026-10-31' }) assert.deepEqual(lage('', frist), { noetig: true, ab: '2026-10-31' }) assert.deepEqual(lage('unternehmen', frist), { noetig: false, ab: '' }) assert.deepEqual(lage('privat', null), { noetig: false, ab: '' }) assert.equal(lage('privat', frist, { jetzt: new Date(frist) }).noetig, true) // genau am Ende noch Frist assert.equal(lage('privat', frist, { jetzt: new Date('2026-10-30T23:00:00.000Z') }).noetig, false) assert.deepEqual(lage('unternehmen', null, { serverAb: '2026-11-02T23:00:00.000Z' }), { noetig: true, ab: '2026-11-03' }) // Form von P5: Berliner Mitternacht assert.equal(lage('privat', null, { serverAb: '2026-11-03' }).ab, '2026-11-03') }) test('einschaltKoerper: Fassung aus dem Register, Haken nur "beginn", nie die Kundenart', () => { const c = consentVon(daten(), 'news') assert.deepEqual(einschaltKoerper({ projectId: 'p1', consent: c, beginnGesetzt: false }), { projectId: 'p1', zustimmung: { version: '2026-10-20', haken: [] } }) const mit = einschaltKoerper({ projectId: 'p1', consent: c, beginnGesetzt: true }) assert.deepEqual(mit.zustimmung.haken, ['beginn']) assert.equal('kundenart' in mit, false) }) test('sichereAdresse: nur https und Pfade unter /rechtliches/, /vertragsunterlagen/ oder /neu/rechtliches/', () => { for (const gut of ['/rechtliches/avv-2026-10-05.html', '/vertragsunterlagen/funktionen-leistungsbeschreibung.html', '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html', 'https://example.org/a.pdf']) assert.equal(sichereAdresse(gut), gut) for (const schlecht of ['javascript:alert(1)', 'http://example.org/', '//example.org/x', '/rechtliches/../geheim', '/rechtliches/%2e%2e/x', 'https://user:pw@example.org/', '/api/abo', '/neu/rechtliches/../geheim', '/neu/api/abo', '/rechtliches/a b', '', null, 42]) assert.equal(sichereAdresse(schlecht), '', String(schlecht)) }) test('linkzeile: die zwei Wörter werden Links; unsichere oder fehlende Adressen nicht', () => { const text = 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-05).' const u = katalogEintrag('termine').unterlagen assert.deepEqual(linkzeile(text, u), [ { text: 'Es gelten ' }, { text: 'Leistungsbeschreibung', href: u.leistungsbeschreibung.url }, { text: ' und ' }, { text: 'Auftragsverarbeitung', href: u.avv.url }, { text: ' (Fassung 2026-10-05).' }, ]) assert.deepEqual(linkzeile('Es gilt die Leistungsbeschreibung (Fassung 2026-10-04).', { leistungsbeschreibung: u.leistungsbeschreibung, avv: null }), [ { text: 'Es gilt die ' }, { text: 'Leistungsbeschreibung', href: u.leistungsbeschreibung.url }, { text: ' (Fassung 2026-10-04).' }, ]) assert.deepEqual(linkzeile(text, { leistungsbeschreibung: { url: 'javascript:alert(1)' }, avv: null }), [{ text }]) assert.deepEqual(linkzeile('Es gelten Leistungsbeschreibung (Fassung 2026-10-05).', u), [ { text: 'Es gelten ' }, { text: 'Leistungsbeschreibung', href: u.leistungsbeschreibung.url }, { text: ' (Fassung 2026-10-05). ' }, { text: 'Auftragsverarbeitung', href: u.avv.url }, ]) // beide Wörter fehlen im Text: beide Links hängen hinten an, mit " · " dazwischen assert.deepEqual(linkzeile('Es gelten die Unterlagen.', u), [ { text: 'Es gelten die Unterlagen. ' }, { text: 'Leistungsbeschreibung', href: u.leistungsbeschreibung.url }, { text: ' · ' }, { text: 'Auftragsverarbeitung', href: u.avv.url }, ]) assert.deepEqual(linkzeile(text, null), [{ text }]) }) test('fehlerAktion: alle 409-Codes aus V5 und die Randfälle', () => { const f = (status, code, body = {}, message = 'Servertext') => ({ status, code, message, body: { code, ...body } }) const AB = '2026-11-02T23:00:00.000Z' const faelle = [ // [Fehler, Funktion, erwartete Aktion] [f(409, 'nicht_verfuegbar'), 'termine', { art: 'meldung', text: 'Diese Funktion ist für dich gerade nicht verfügbar.' }], [f(409, 'abo_fehlt', { grund: 'falsches_abo', wechsel: true }), 'termine', { art: 'wechsel' }], [f(409, 'abo_fehlt', { grund: 'kein_abo' }), 'termine', { art: 'bestellen', produkt: 'wartung' }], [f(409, 'abo_fehlt', { grund: 'kein_abo' }), 'analyse', { art: 'bestellen', produkt: 'hosting' }], [f(409, 'abo_fehlt', { grund: 'gekuendigt' }), 'termine', { art: 'meldung', text: GEKUENDIGT }], [f(409, 'abo_fehlt', { grund: 'nicht_aktiv' }), 'termine', { art: 'meldung', text: NICHT_AKTIV }], [f(409, 'abo_fehlt', { grund: 'falsches_abo' }), 'termine', { art: 'meldung', text: 'Dein Abo enthält diese Funktion nicht.' }], [f(409, 'abo_fehlt', { grund: 'beginnt_spaeter', ab: AB }), 'termine', { art: 'spaeter', ab: AB, text: 'Dein Abo beginnt am 03.11.2026. Ab dann kannst du einschalten.' }], // N20 [f(409, 'abo_fehlt', { grund: 'beginnt_spaeter' }), 'termine', { art: 'meldung', text: 'Servertext' }], // ohne Datum: Text des Servers [f(409, 'abo_fehlt', { grund: 'unbekannt' }), 'termine', { art: 'meldung', text: 'Servertext' }], [f(409, 'nur_betrieb'), 'shop', { art: 'meldung', text: NUR_BETRIEB }], [f(409, 'bestellung_offen', { offen: [{ schritt: 'zeiten', text: 'x' }] }), 'termine', { art: 'etage', etage: 'termine' }], [f(409, 'bestellung_offen', { offen: ['Anfrage'] }), 'shop', { art: 'etage', etage: 'webshop' }], [f(409, 'beginn_noetig', { ab: AB }), 'termine', { art: 'beginn', ab: AB }], [f(409, 'bereits_eingeschaltet'), 'termine', { art: 'schon' }], [f(409, 'fassung_veraltet'), 'termine', { art: 'neu-laden', text: 'Die Bedingungen wurden aktualisiert. Bitte lies die Zeile noch einmal.' }], [f(409, 'bestellung_laeuft'), 'termine', { art: 'meldung', text: 'Servertext' }], // weitere Codes aus P5: Text des Servers [f(409, 'abgeschaltet'), 'termine', { art: 'meldung', text: 'Servertext' }], [f(503, 'unterlagen_fehlen'), 'termine', { art: 'meldung', text: 'Servertext' }], [f(403, 'view_blocked'), 'termine', { art: 'meldung', text: 'Servertext' }], [{ status: 0, message: 'Keine Verbindung. Bitte versuch es gleich noch einmal.', body: {} }, 'termine', { art: 'meldung', text: 'Keine Verbindung. Bitte versuch es gleich noch einmal.' }], [{ status: 500, message: '', body: {} }, 'termine', { art: 'meldung', text: 'Das Einschalten hat gerade nicht geklappt. Versuch es gleich noch einmal oder schreib uns im Chat.' }], ] for (const [fehler, key, erwartet] of faelle) assert.deepEqual(fehlerAktion(fehler, { eintrag: katalogEintrag(key) }), erwartet, `${fehler.status} ${fehler.code}`) })