import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import { loadModule } from './load.mjs' const { safeHref, phoneLabel, botleadRow, ROOM_FILTERS, pageInfo } = await loadModule('src/lib/botleadsView.js') // Die React-Teile sind unter Node nicht ladbar (JSX): der Quelltext wird geprüft (Muster workersComponents.test.mjs); das Verhalten prüft botleadList.test.mjs const read = (p) => fs.readFileSync(new URL('../../' + p, import.meta.url), 'utf8') test('safeHref: nur http und https, keine Zugangsdaten in der Adresse', () => { assert.equal(safeHref('https://beispiel-elektro.de/impressum'), 'https://beispiel-elektro.de/impressum') assert.equal(safeHref(' http://beispiel-elektro.de/ '), 'http://beispiel-elektro.de/') for (const bose of ['javascript:alert(1)', '//evil.example', 'data:text/html,x', '', ' ', 'ftp://x.example/', 'mailto:a@b.de', '/pfad', 'https://', null, undefined, 42, {}]) assert.equal(safeHref(bose), '', String(bose)) assert.equal(safeHref('https://user:pw@beispiel-elektro.de/'), '') // Zugangsdaten assert.equal(safeHref('https://beispiel-elektro.de@evil.example/'), '') // sähe nach dem Betrieb aus, führt aber woandershin }) test('phoneLabel: Nummer nur mit Quelle, Heilberuf nie', () => { assert.equal(phoneLabel({ phone: '0631 111', phoneSource: 'https://x.de/i', isHealth: false }), '0631 111') assert.equal(phoneLabel({ phone: '0631 111', phoneSource: '', isHealth: false }), 'nicht verfügbar') assert.equal(phoneLabel({ phone: '', phoneSource: '', isHealth: true }), 'bewusst nicht gespeichert') assert.equal(phoneLabel({ phone: '0631 111', phoneSource: 'https://x.de/i', isHealth: true }), 'bewusst nicht gespeichert') assert.equal(phoneLabel({ phone: '', phoneSource: 'https://x.de/i', isHealth: false }), 'nicht verfügbar') assert.equal(phoneLabel({}), 'nicht verfügbar') assert.equal(phoneLabel(null), 'nicht verfügbar') }) test('botleadRow: alle zehn Felder', () => { const item = { id: 'a', room: 'handwerk', roomLabel: 'Handwerk', name: 'Elektro Beispiel GmbH', postalCode: '67655', city: 'Kaiserslautern', score: 82, reason: 'keine veröffentlichte Mail', sourceUrl: 'javascript:x', phone: '', phoneSource: '', isHealth: false, foundAt: '2026-10-19T20:00:00Z', gekuerzt: false } const r = botleadRow(item) assert.deepEqual(Object.keys(r), ['id', 'title', 'place', 'roomLabel', 'scoreLabel', 'reason', 'phone', 'sourceHref', 'found', 'gekuerzt']) assert.deepEqual(r, { id: 'a', title: 'Elektro Beispiel GmbH', place: '67655 Kaiserslautern', roomLabel: 'Handwerk', scoreLabel: '82 Punkte', reason: 'keine veröffentlichte Mail', phone: 'nicht verfügbar', sourceHref: '', found: '19.10.2026', gekuerzt: false }) // sichere Quelle, Nummer mit Quelle, gekürzt const b = botleadRow({ ...item, sourceUrl: 'https://beispiel-elektro.de/impressum', phone: '0631 111', phoneSource: 'https://beispiel-elektro.de/impressum', gekuerzt: true }) assert.deepEqual([b.sourceHref, b.phone, b.gekuerzt], ['https://beispiel-elektro.de/impressum', '0631 111', true]) // Heilberuf: nie die Nummer, auch wenn der Dienst eine mitschickt assert.equal(botleadRow({ ...item, isHealth: true, phone: '0631 111', phoneSource: 'https://x.de/i' }).phone, 'bewusst nicht gespeichert') }) test('botleadRow: fehlende Werte ergeben Platzhalter, nie undefined oder NaN', () => { const r = botleadRow({ id: 'b' }) assert.deepEqual(r, { id: 'b', title: 'nicht verfügbar', place: '', roomLabel: '–', scoreLabel: '–', reason: '–', phone: 'nicht verfügbar', sourceHref: '', found: '–', gekuerzt: false }) assert.equal(botleadRow({ id: 'c', name: ' ', postalCode: '', city: 'Mainz' }).title, 'nicht verfügbar') assert.equal(botleadRow({ id: 'c', city: 'Mainz' }).place, 'Mainz') assert.equal(botleadRow({ id: 'c', postalCode: '55116' }).place, '55116') assert.equal(botleadRow({ id: 'c', score: 0 }).scoreLabel, '0 Punkte') assert.equal(botleadRow({ id: 'c', score: 'x' }).scoreLabel, '–') assert.equal(botleadRow({ id: 'c', foundAt: 'kein Datum' }).found, '–') assert.equal(botleadRow(null).id, '') }) test('botleadRow: das Funddatum zählt in Berlin (Mitternacht und Zeitumstellung)', () => { assert.equal(botleadRow({ id: 'a', foundAt: '2026-10-19T22:30:00Z' }).found, '20.10.2026') // 00:30 Uhr am nächsten Tag assert.equal(botleadRow({ id: 'a', foundAt: '2026-12-31T23:30:00Z' }).found, '01.01.2027') assert.equal(botleadRow({ id: 'a', foundAt: '2026-10-25T00:30:00Z' }).found, '25.10.2026') // Nacht der Zeitumstellung }) test('ROOM_FILTERS: Reihenfolge, Werte und Beschriftungen', () => { assert.deepEqual(ROOM_FILTERS, [ { value: '', label: 'Alle Räume' }, { value: 'aerzte', label: 'Ärzte & Praxen' }, { value: 'handwerk', label: 'Handwerk' }, { value: 'selbst', label: 'Selbstständige' }, { value: 'laden', label: 'Läden & Gastro' }, ]) }) test('pageInfo: Seiten, Blättern und Beschriftung', () => { assert.deepEqual(pageInfo({ total: 120, page: 2, pageSize: 50 }), { pages: 3, hasPrev: true, hasNext: true, label: 'Seite 2 von 3 · 120 Einträge' }) assert.deepEqual(pageInfo({ total: 120, page: 1, pageSize: 50 }), { pages: 3, hasPrev: false, hasNext: true, label: 'Seite 1 von 3 · 120 Einträge' }) assert.deepEqual(pageInfo({ total: 120, page: 3, pageSize: 50 }), { pages: 3, hasPrev: true, hasNext: false, label: 'Seite 3 von 3 · 120 Einträge' }) assert.equal(pageInfo({ total: 0, page: 1, pageSize: 50 }).label, 'Keine Einträge') assert.deepEqual(pageInfo({ total: 0, page: 1, pageSize: 50 }), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' }) assert.equal(pageInfo({ total: 1, page: 1, pageSize: 50 }).label, 'Seite 1 von 1 · 1 Eintrag') // Einzahl assert.equal(pageInfo({ total: 50, page: 1, pageSize: 50 }).pages, 1) assert.equal(pageInfo({ total: 51, page: 1, pageSize: 50 }).pages, 2) assert.equal(pageInfo({ total: 1234, page: 1, pageSize: 50 }).label, 'Seite 1 von 25 · 1.234 Einträge') // deutsche Tausenderpunkte // der Dienst schickt pageSize mit; fehlt es oder ist es unbrauchbar, gilt 50 assert.equal(pageInfo({ total: 120, page: 1 }).pages, 3) for (const bad of [0, -5, NaN, 'x', null]) assert.equal(pageInfo({ total: 120, page: 1, pageSize: bad }).pages, 3, String(bad)) assert.equal(pageInfo({ total: 120, page: 1, pageSize: 25 }).pages, 5) // unbrauchbare Zahlen ergeben nie NaN assert.deepEqual(pageInfo({ total: 'x', page: 'y', pageSize: 50 }), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' }) assert.deepEqual(pageInfo({}), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' }) }) test('BotLeadList.jsx: Links nur aus der Zeilen-Ableitung, extern mit rel noopener, kein HTML-Einschub, Knöpfe mit type="button"', () => { const src = read('src/components/workers/BotLeadList.jsx') assert.match(src, /botleadRow\(/) assert.match(src, /rel="noopener noreferrer"/); assert.match(src, /target="_blank"/) // jeder href kommt aus row.sourceHref (botleadRow → safeHref), nie aus dem Eintrag selbst assert.match(src, /href=\{row\.sourceHref\}/) assert.doesNotMatch(src, /\bhref=\{(?!row\.sourceHref\})/) assert.doesNotMatch(src, /\bhref="/) assert.doesNotMatch(src, /dangerouslySetInnerHTML|innerHTML/) // kein Anruf-Link, kein Mailversand assert.doesNotMatch(src, /tel:|mailto:/) // WOMS-Button hat keinen Standard-type: jeder Knopf nennt ihn assert.equal((src.match(/<(?:button|Button)\b/g) || []).length > 0, true) assert.equal((src.match(/<(?:button|Button)\b/g) || []).length, (src.match(/<(?:button|Button)\b[^>]*type="button"/g) || []).length) // kein Polling assert.doesNotMatch(src, /setInterval/) })