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', 'bereits abgesendet', '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('Token: replaceState steht in try/catch (wirft der Browser, laeuft die Seite weiter) und vor dem ersten Aufruf', () => { assert.match(OHNE_KOMMENTARE, /try\s*\{\s*window\.history\.replaceState\([^)]*\)\s*\}\s*catch/) assert.ok(OHNE_KOMMENTARE.indexOf('history.replaceState') < OHNE_KOMMENTARE.indexOf('lade()', OHNE_KOMMENTARE.indexOf('function start')), 'replaceState vor lade()') }) test('Widerruf ist ein gleichrangiger Knopf (§ 356a BGB), kein Textlink; kein totes absaetze()', () => { assert.ok(OHNE_KOMMENTARE.includes("knopf('t-btn t-btn-sek', 'Vertrag widerrufen'"), 'Widerruf als sekundaerer Knopf') assert.doesNotMatch(OHNE_KOMMENTARE, /['"\s]t-link['"\s]/, 'kein Textlink-Knopf') assert.doesNotMatch(CSS, /\.t-link\b(?!s)/, 'kein Textlink-Knopf in termin.css') assert.ok(!OHNE_KOMMENTARE.includes('absaetze'), 'totes absaetze() entfernt') }) test('termin.css: Reihenfolge im DOM = Anzeige (kein *-reverse), Ueberschrift ohne Rahmen, Bedienelemente mit Rahmen', () => { assert.doesNotMatch(CSS, /-reverse/, 'kein row-reverse/column-reverse: Tab-Reihenfolge muss der Anzeige entsprechen') // die Regel fuer die Ueberschrift muss spezifischer sein als `.t-karte :focus-visible` assert.match(CSS, /\.t-karte \.t-titel:focus-visible\s*\{[^}]*outline:\s*none/) assert.match(CSS, /\.t-karte :focus-visible[^{]*\{[^}]*outline:\s*3px solid/) }) 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, /