import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import vm from 'node:vm' import { luminanz as serverLuminanz, textFarbeAuf as serverTextFarbeAuf } from '../server/services/termine/einbauanleitung.js' // Statische Pruefung des Einbau-Skripts public/embed/termine.js (Aufgabe 19, Rulings R3 und R4): // - Das Widget rechnet nie mit dem Datum oder der Zeitzone des Browsers (Kalender, Wochentage, Uhrzeiten und Datumstexte kommen // vom Server: monat, tage, zeiten, datumLang). Darum kein Date, getDay, toLocale*, Intl. // - Kein Browser-Speicher, keine Cookies, kein Auslesen von Referrer oder Zeitzone, keine Fremdadressen im Code. // - Kundentext nur ueber textContent/Attribute: innerHTML nur mit festen Zeichenketten. // Die Pruefung der Seiten drumherum (Demo, Einbauanleitung) steht am Ende. const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8') const QUELLE = lies('public/embed/termine.js') /** Quelltext ohne Kommentare (die Kopfzeile darf Beispiele mit Adressen enthalten, der Code nicht). */ const OHNE_KOMMENTARE = QUELLE.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1') test('Quelltext ist gueltiges JavaScript (kompiliert, wird nicht ausgefuehrt)', () => { assert.doesNotThrow(() => new vm.Script(QUELLE, { filename: 'termine.js' })) }) test('kein Browser-Speicher, keine Cookies, kein Referrer, keine Zeitzone', () => { for (const verboten of ['localStorage', 'sessionStorage', 'document.cookie', 'indexedDB', 'document.referrer', 'sendBeacon', 'navigator.userAgent', 'resolvedOptions', 'getTimezoneOffset']) { assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Widget nicht vorkommen`) } }) test('keine Datums- und Zeitzonenrechnung im Browser (Date, getDay, toLocale, Intl.)', () => { assert.ok(!/\bDate\b/.test(OHNE_KOMMENTARE), 'Date darf nicht vorkommen') for (const verboten of ['getDay', 'toLocale', 'Intl.', 'getFullYear', 'getMonth', 'getHours', 'getTime(', 'Date.now']) { assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Widget nicht vorkommen`) } }) test('innerHTML nur mit festen Zeichenketten; kein outerHTML, insertAdjacentHTML, document.write, eval', () => { const treffer = [...OHNE_KOMMENTARE.matchAll(/innerHTML/g)] for (const t of treffer) { const rest = OHNE_KOMMENTARE.slice(t.index + 'innerHTML'.length) assert.match(rest, /^\s*=\s*(?:'[^'\\+]*'|"[^"\\+]*")\s*(?:;|\n|\))/, 'innerHTML darf nur eine feste Zeichenkette bekommen') } for (const verboten of ['outerHTML', 'insertAdjacentHTML', 'document.write', 'eval(', 'new Function', 'setTimeout("', "setTimeout('", 'createContextualFragment', 'srcdoc']) { assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Widget nicht vorkommen`) } }) test('keine festen Fremdadressen im Code (nur die Adresse aus data-api oder dem Skript)', () => { assert.doesNotMatch(OHNE_KOMMENTARE, /https?:\/\//, 'Adressen gehoeren in data-api, nicht in den Code') assert.doesNotMatch(OHNE_KOMMENTARE, /\bimport\s*\(|\bimportScripts\b|\bXMLHttpRequest\b|\bWebSocket\b/) }) test('Anfragen ohne Cookies und nur an die Termine-Schnittstelle', () => { assert.ok(OHNE_KOMMENTARE.includes("credentials: 'omit'"), "fetch braucht credentials: 'omit'") assert.ok(OHNE_KOMMENTARE.includes('/api/public/termine/v1/')) assert.equal([...OHNE_KOMMENTARE.matchAll(/\bfetch\(/g)].length, 1, 'genau ein fetch (eine Stelle fuer alle Anfragen)') }) test('Pflichtbausteine der Spezifikation (Einbau, Stile, Honeypot, Fuss, Texte)', () => { for (const noetig of [ 'data-webklar-termine', 'data-api', 'data-modus', 'data-knopf', 'data-leistung', "'wkt-style'", '@layer wkt', 'wkt-', '--wkt-akzent', '--wkt-akzent-text', '--wkt-hintergrund', '--wkt-text', '--wkt-rand', '--wkt-radius', '--wkt-schrift', '--wkt-schrift-titel', 'kontakt_url', 'autocomplete', 'tabindex', 'aria-hidden', 'aria-live', 'polite', 'aria-selected', 'aria-disabled', 'role', 'dialog', 'performance.now', '100 * 60 * 1000', 'neu_laden', '/widerruf', 'Vertrag widerrufen', 'Datenschutz', 'Terminbuchung über WEBklar', 'Widerruf bestätigen', 'Gerade nicht erreichbar. Bitte rufen Sie an: ', 'Fast geschafft', 'widerrufsbelehrung', 'details', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', ]) { assert.ok(QUELLE.includes(noetig), `${noetig} fehlt im Skript`) } }) test('Stile stehen einmal in