import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import vm from 'node:vm' // Statische Pruefung der Gaesteseite public/termin.html + termin.js + termin.css (Aufgabe 20, Rulings R3 und "termin.html / // termin.js: kein Browser-Speicher, keine Cookies, noindex"): Datums- und Uhrzeittexte, Kalenderdaten und der Eingang eines // Widerrufs kommen vom Server (Berliner Zeit), die Seite rechnet nie mit Datum oder Zeitzone des Browsers. Kundentext nur // ueber textContent. const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8') const QUELLE = lies('public/termin.js') const HTML = lies('public/termin.html') const CSS = lies('public/termin.css') /** 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: 'termin.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 in termin.js 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 in termin.js nicht vorkommen`) } }) test('Kundentext nur ueber textContent: kein innerHTML, outerHTML, insertAdjacentHTML, document.write, eval', () => { for (const verboten of ['innerHTML', 'outerHTML', 'insertAdjacentHTML', 'document.write', 'eval(', 'new Function', 'setTimeout("', "setTimeout('", 'createContextualFragment', 'srcdoc']) { assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf in termin.js nicht vorkommen`) } }) test('keine festen Fremdadressen im Code, keine nachgeladenen Skripte', () => { assert.doesNotMatch(OHNE_KOMMENTARE, /https?:\/\//, 'Adressen gehoeren nicht in den Code') assert.doesNotMatch(OHNE_KOMMENTARE, /\bimport\s*\(|\bimportScripts\b|\bXMLHttpRequest\b|\bWebSocket\b/) }) test('genau ein fetch, ohne Cookies, nur POST an die Token-Seiten der Schnittstelle', () => { assert.equal([...OHNE_KOMMENTARE.matchAll(/\bfetch\(/g)].length, 1, 'genau ein fetch (eine Stelle fuer alle Anfragen)') assert.ok(OHNE_KOMMENTARE.includes("credentials: 'omit'"), "fetch braucht credentials: 'omit'") assert.ok(OHNE_KOMMENTARE.includes("method: 'POST'"), 'Zustandsaenderungen und Abfragen nur per POST') assert.ok(OHNE_KOMMENTARE.includes("'/api/public/termine/v1/termin/'")) for (const aktion of ['vorschau', 'bestaetigen', 'absagen', 'verschieben', 'widerruf', 'tage', 'zeiten']) { assert.ok(OHNE_KOMMENTARE.includes(`'${aktion}'`), `Aktion ${aktion} fehlt`) } }) test('der Token wird aus der Adresszeile entfernt und nur im Speicher gehalten', () => { assert.ok(OHNE_KOMMENTARE.includes('history.replaceState'), 'Hash muss per history.replaceState entfernt werden') assert.ok(OHNE_KOMMENTARE.includes('location.hash')) assert.ok(!OHNE_KOMMENTARE.includes('location.href'), 'keine Weitergabe der Adresse') assert.ok(!/location\.(?:assign|replace)\(|window\.open\(/.test(OHNE_KOMMENTARE), 'keine Weiterleitung mit dem Token') }) test('Pflichtbausteine der Aufgabe (Ansichten, Knopftexte, Fuss, Farbe)', () => { for (const noetig of [ 'Widerruf bestätigen', 'Vertrag widerrufen', 'Termin absagen', 'Termin verschieben', 'Ja, Termin absagen', 'Nein, Termin behalten', 'bereits bestätigt', 'Eingang: ', 'eingang.datum', 'eingang.uhrzeit', 'websiteUrl', 'telefon', 'impressumUrl', 'datenschutzUrl', 'noopener noreferrer', 'httpsUrl', 'v.knopf', 'd.anrede', 'd.farbe', '§ 356a BGB', 'role', ]) { assert.ok(QUELLE.includes(noetig), `${noetig} fehlt in termin.js`) } assert.ok(!/\bconfirm\(|\balert\(|\bprompt\(/.test(OHNE_KOMMENTARE), 'keine Browser-Dialoge: die Rueckfrage steht in der Seite') }) test('termin.html: noindex, kein Referrer, Stile und Skript von der eigenen Herkunft, Fuss mit Impressum WEBklar', () => { assert.match(HTML, //) assert.match(HTML, //) assert.match(HTML, /