#!/usr/bin/env node /** * Kundenansicht neu (P10 Umschalten): prueft per HTTP, ob die oeffentlichen Seiten, das Manifest und die Abo-Bedingungen * so ausgeliefert werden, wie der Schalter KUNDENANSICHT_NEU es verlangt: "alt" (off und pilot) oder "neu" (on). * Nur lesend: je Pruefung ein GET ohne Cookies und ohne Umleitungen, kein Appwrite, kein Schreiben, keine Namen * oder Kundendaten in der Ausgabe (nur Adressen der Seiten und Gruende). * Usage: * node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=alt * node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu * Exit 0 = alle Zeilen gruen, 1 = mindestens eine Zeile rot, 2 = falscher Aufruf. */ import { pathToFileURL } from 'node:url' const ZEITLIMIT_MS = 15000 const html = (pfad, enthaelt, enthaeltNicht = []) => Object.freeze({ pfad, typ: 'html', enthaelt, enthaeltNicht }) // Manifest: "schluessel=wert" prueft ein Feld, "shortcuts.name=X" einen Shortcut mit diesem Namen const manifest = (enthaelt) => Object.freeze({ pfad: '/manifest.webmanifest', typ: 'json', enthaelt, enthaeltNicht: [] }) const pdf = Object.freeze({ pfad: '/WEBklar-Abo-Vertragsunterlagen.pdf', typ: 'pdf', enthaelt: ['application/pdf', '%PDF-'], enthaeltNicht: [] }) const NEU_FORMULAR = ['/neu/oeffentlich/oeffentlich.css', '/legal-form.js'] /** Marker je Erwartung; jede Zeile verlangt Status 200. */ export const PRUEFUNGEN = Object.freeze({ neu: Object.freeze([ html('/dashboard.html', [''], ['kb-page']), html('/login.html', ['content="#ffffff"', '/neu/tokens.css'], ['#9d3dfe', '/login.css']), html('/kuendigen.html', NEU_FORMULAR, ['/legal-form.css']), html('/widerrufen.html', NEU_FORMULAR, ['/legal-form.css']), html('/auftrag.html', ['/neu/oeffentlich/oeffentlich.css'], ['/auftrag.css']), // Uebersicht N40 html('/offline.html', ['content="#ffffff"'], ['#9d3dfe', ' { basis, erwartet }; sonst Error "Aufruf: ...". */ export function parseArgs(argv) { const args = Array.isArray(argv) ? argv : [] const optionen = args.filter((a) => typeof a === 'string' && a.startsWith('--')) const rest = args.filter((a) => !optionen.includes(a)) const erwartet = optionen.length === 1 ? /^--erwartet=(neu|alt)$/.exec(optionen[0])?.[1] : undefined const basis = rest.length === 1 ? rest[0] : undefined // nur Schema, Host und optional Port: kein Pfad, keine Zugangsdaten, keine Parameter if (!erwartet || typeof basis !== 'string' || !/^https?:\/\/([A-Za-z0-9.-]+|\[[0-9A-Fa-f:]+\])(:\d{1,5})?$/.test(basis)) throw new Error(AUFRUF) return { basis, erwartet } } /** Ein Feld des Manifests nach "schluessel=wert" / "shortcuts.name=X" pruefen. */ function manifestHat(daten, marker) { const gleich = marker.indexOf('=') const schluessel = marker.slice(0, gleich) const wert = marker.slice(gleich + 1) if (schluessel === 'shortcuts.name') return Array.isArray(daten?.shortcuts) && daten.shortcuts.some((s) => s?.name === wert) return daten?.[schluessel] === wert } /** Eine Pruefung auswerten -> { pfad, ok, grund }. */ async function pruefe(basis, pruefung, fetchFn) { const zeile = (ok, grund = '') => ({ pfad: pruefung.pfad, ok, grund }) let antwort let text try { antwort = await fetchFn(basis + pruefung.pfad, { redirect: 'manual', headers: { 'cache-control': 'no-cache' }, signal: AbortSignal.timeout(ZEITLIMIT_MS), }) text = await antwort.text() } catch { return zeile(false, 'Netzfehler') } if (antwort.status !== 200) return zeile(false, `Status ${antwort.status} statt 200`) if (pruefung.typ === 'pdf') { const art = antwort.headers.get('content-type') || '' if (!art.includes('application/pdf')) return zeile(false, `Inhaltstyp ${art || 'fehlt'} statt application/pdf`) return text.startsWith('%PDF-') ? zeile(true) : zeile(false, 'Beginn %PDF- fehlt') } if (pruefung.typ === 'json') { let daten try { daten = JSON.parse(text) } catch { return zeile(false, 'kein JSON') } const fehlt = pruefung.enthaelt.find((m) => !manifestHat(daten, m)) return fehlt ? zeile(false, `fehlt: ${fehlt}`) : zeile(true) } const fehlt = pruefung.enthaelt.find((m) => !text.includes(m)) if (fehlt) return zeile(false, `fehlt: ${fehlt}`) const verboten = pruefung.enthaeltNicht.find((m) => text.includes(m)) return verboten ? zeile(false, `verboten: ${verboten}`) : zeile(true) } /** * Alle Pruefungen der Erwartung ("neu" | "alt") gegen basis ausfuehren; ohne: Adressen, die ausgelassen werden * (z. B. die Fassung-6-Zeilen, solange sie nicht ausgeliefert werden). -> { ok, zeilen: [{ pfad, ok, grund }] }. */ export async function nachpruefen(basis, erwartet, { fetchFn = fetch, ohne = [] } = {}) { const liste = PRUEFUNGEN[erwartet] if (!liste) throw new Error(AUFRUF) const zeilen = [] for (const pruefung of liste) { if (ohne.includes(pruefung.pfad)) continue zeilen.push(await pruefe(basis, pruefung, fetchFn)) } return { ok: zeilen.every((z) => z.ok), zeilen } } const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href if (invokedDirectly) { let args try { args = parseArgs(process.argv.slice(2)) } catch (err) { console.error(err.message) process.exit(2) } nachpruefen(args.basis, args.erwartet) .then(({ ok, zeilen }) => { for (const z of zeilen) console.log(z.ok ? `ok ${z.pfad}` : `FEHLER ${z.pfad} ${z.grund}`) process.exit(ok ? 0 : 1) }) .catch((err) => { console.error(`FEHLER ${err.message}`) process.exit(1) }) }