/* ===================================================================== Kasse & Anmeldung gegen die Inhaltsrichtlinie (27.08.2026) WORUM ES GEHT Auf abonnieren.html kommen zwei Dinge von fremden Servern: die PayPal-Kasse und die Google-Anmeldung. Beide werden erst NACH einer Nutzerhandlung nachgeladen (Klick bzw. Anmeldung) und beide sind derzeit nicht scharf geschaltet -- googleClientId und paypalClientId stehen auf null. Ein Fehler dort fällt deshalb heute NICHT auf. Er fällt erst an dem Tag auf, an dem Dogi seine echten Zugangsdaten einträgt: Dann stünde an der Kasse ein leerer Bereich, ohne Fehlermeldung, ohne Eintrag im Server-Protokoll. Genau dieses Muster hat schon einmal zugeschlagen (Musik-Knopf, 26.-27.08.2026). Deshalb wird hier vorgezogen geprüft, statt zu warten. WIE ECHT DIESER TEST IST - Er startet den ECHTEN server/index.js, damit die echte Kopfzeile ausgeliefert wird. Ein Datei-Server erzeugt sie gar nicht. - Er lädt das ECHTE PayPal-SDK mit deren offizieller Testkennung ("client-id=test") und baut die Knöpfe wirklich auf. - Er lädt die ECHTE Google-Bibliothek und rendert den Knopf wirklich. - Verstöße werden nicht aus Konsolentext geraten, sondern über das Ereignis "securitypolicyviolation" gezählt -- dort stehen Richtlinie und blockierte Adresse maschinenlesbar drin. Kein echter Zugangscode und kein echtes Geld sind beteiligt. ===================================================================== */ import { chromium } from "playwright"; import { spawn } from "node:child_process"; import { createServer } from "node:net"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const HIER = dirname(fileURLToPath(import.meta.url)); /* Freien Port suchen statt festen nehmen: Laeuft noch ein Server aus einem frueheren Lauf, kann der neue seinen Port nicht belegen, stirbt still -- und der Test misst gegen den ALTEN Code. Genau das ist am 27.08.2026 passiert und hat fast zu einer Fehlersuche in die falsche Richtung gefuehrt. */ const PORT = await new Promise((auf, ab) => { const probe = createServer(); probe.on("error", ab); probe.listen(0, "127.0.0.1", () => { const p = probe.address().port; probe.close(() => auf(p)); }); }); const BASIS = `http://127.0.0.1:${PORT}`; let ok = 0, fehl = 0; const funde = []; const pruefe = (name, gut, info) => { if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); } }; const server = spawn(process.execPath, ["index.js"], { cwd: join(HIER, "server"), env: { ...process.env, PORT: String(PORT) }, stdio: "ignore", }); const beenden = (code) => { try { server.kill(); } catch { /* schon weg */ } process.exit(code); }; /* Notbremse: PayPals render() wartet ohne echten Bezahlvorgang unbegrenzt. Ohne diese Grenze bliebe der Test im Zweifel ewig hängen. */ const notbremse = setTimeout(() => { console.log("\nZEIT ABGELAUFEN — Testlauf abgebrochen."); beenden(1); }, 180000); notbremse.unref(); async function warteAufServer() { for (let i = 0; i < 40; i++) { try { const res = await fetch(BASIS + "/abonnieren.html"); if (res.ok) return res; } catch { /* noch nicht oben */ } await new Promise((r) => setTimeout(r, 250)); } throw new Error("Server ist nicht hochgekommen"); } (async () => { const antwort = await warteAufServer(); const richtlinie = antwort.headers.get("content-security-policy") || ""; const regel = (name) => (richtlinie.split(";").find((t) => t.trim().startsWith(name + " ")) || "").trim(); console.log("\n== Was die Kopfzeile verspricht =="); pruefe("script-src kennt PayPal", /www\.paypal\.com/.test(regel("script-src")), regel("script-src").slice(0, 120)); pruefe("script-src kennt Google", /accounts\.google\.com\/gsi\/client/.test(regel("script-src")), ""); pruefe("frame-src kennt PayPal (Betrieb + Übung)", /www\.paypal\.com/.test(regel("frame-src")) && /sandbox\.paypal\.com/.test(regel("frame-src")), regel("frame-src")); pruefe("frame-src kennt Google", /accounts\.google\.com\/gsi\//.test(regel("frame-src")), ""); pruefe("style-src kennt Googles Stilvorlage", /accounts\.google\.com\/gsi\/style/.test(regel("style-src")), regel("style-src")); pruefe("Musik bleibt erlaubt", /open\.spotify\.com/.test(regel("frame-src")), ""); pruefe("Kein Sternchen in frame-src", !/\*/.test(regel("frame-src")), regel("frame-src")); pruefe("Kein 'unsafe-inline' bei Skripten", !/unsafe-inline/.test(regel("script-src")), ""); const browser = await chromium.launch(); const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } }); await seite.goto(BASIS + "/abonnieren.html", { waitUntil: "domcontentloaded" }); await seite.evaluate(() => { window.__verstoesse = []; document.addEventListener("securitypolicyviolation", (e) => window.__verstoesse.push({ regel: e.violatedDirective, blockiert: e.blockedURI || "(inline)" }) ); }); /* ---------- PayPal ---------- */ console.log("\n== PayPal-Kasse (offizielle Testkennung) =="); const paypal = await seite.evaluate(async () => { const geladen = await new Promise((res) => { const sk = document.createElement("script"); sk.src = "https://www.paypal.com/sdk/js?client-id=test¤cy=EUR"; sk.onload = () => res(true); sk.onerror = () => res(false); document.body.appendChild(sk); setTimeout(() => res(false), 20000); }); if (!geladen || !window.paypal) return { geladen, knoepfe: false }; const ziel = document.createElement("div"); ziel.id = "kassen-test"; ziel.style.width = "320px"; document.body.appendChild(ziel); /* Absichtlich NICHT abwarten: render() loest ohne echten Bezahlvorgang nie auf. Es geht nur darum, ob der Rahmen entstehen darf. */ try { window.paypal.Buttons({}).render("#kassen-test").catch(() => {}); } catch { /* egal */ } await new Promise((r) => setTimeout(r, 9000)); const rahmen = ziel.querySelector("iframe"); return { geladen, knoepfe: !!rahmen, hoehe: rahmen ? Math.round(rahmen.getBoundingClientRect().height) : 0 }; }); pruefe("PayPal-SDK wird geladen", paypal.geladen, paypal.geladen ? "geladen" : "BLOCKIERT"); pruefe("Bezahlknöpfe werden aufgebaut", paypal.knoepfe, paypal.knoepfe ? paypal.hoehe + "px hoch" : "kein Rahmen entstanden"); /* ---------- Google ---------- */ console.log("\n== Google-Anmeldung =="); const google = await seite.evaluate(async () => { const geladen = await new Promise((res) => { const sk = document.createElement("script"); sk.src = "https://accounts.google.com/gsi/client"; sk.onload = () => res(true); sk.onerror = () => res(false); document.head.appendChild(sk); setTimeout(() => res(false), 20000); }); if (!geladen || !window.google) return { geladen, knopf: false }; const ziel = document.createElement("div"); ziel.id = "anmelde-test"; document.body.appendChild(ziel); try { window.google.accounts.id.initialize({ client_id: "000000000000-testtesttesttesttesttest.apps.googleusercontent.com", callback: () => {}, }); window.google.accounts.id.renderButton(ziel, { type: "standard" }); } catch { return { geladen, knopf: false }; } await new Promise((r) => setTimeout(r, 5000)); return { geladen, knopf: ziel.children.length > 0 }; }); pruefe("Google-Bibliothek wird geladen", google.geladen, google.geladen ? "geladen" : "BLOCKIERT"); pruefe("Anmelde-Knopf wird aufgebaut", google.knopf, google.knopf ? "gerendert" : "leer geblieben"); const googleRahmen = seite.frames().filter((f) => f.url().startsWith("https://accounts.google.com/gsi/")).length; pruefe("Googles Rahmen darf laden", googleRahmen >= 1, googleRahmen + " Rahmen"); /* ---------- Das eigentliche Urteil ---------- */ console.log("\n== Verstöße gegen die Inhaltsrichtlinie =="); const verstoesse = await seite.evaluate(() => window.__verstoesse); const gezaehlt = {}; for (const v of verstoesse) { const schluessel = v.regel + " <- " + v.blockiert.split("?")[0]; gezaehlt[schluessel] = (gezaehlt[schluessel] || 0) + 1; } Object.entries(gezaehlt).forEach(([k, n]) => console.log(" " + n + "x " + k)); pruefe("Kein einziger Verstoß", verstoesse.length === 0, verstoesse.length + " Verstöße"); const fehlerRahmen = seite.frames().filter((f) => f.url().startsWith("chrome-error://")).length; pruefe("Kein Rahmen endet als Fehlerseite", fehlerRahmen === 0, fehlerRahmen + " Fehlerseiten"); /* Bilder von genau den beiden Testelementen. WICHTIG: Die Elemente werden dafür NICHT im Seitenbaum verschoben. PayPal baut seine Knöpfe wieder ab, sobald ihr Rahmen umgehängt wird — ein Bild davon zeigte dann fälschlich eine leere Kasse, obwohl sie kurz zuvor sauber stand. */ for (const [wahl, datei] of [["#kassen-test", "bild-kasse-paypal.png"], ["#anmelde-test", "bild-kasse-google.png"]]) { try { await seite.locator(wahl).scrollIntoViewIfNeeded(); await seite.waitForTimeout(600); await seite.locator(wahl).screenshot({ path: datei }); } catch { /* Bild ist Beiwerk, kein Prüfkriterium */ } } await browser.close(); clearTimeout(notbremse); console.log("\n===== " + ok + " ok, " + fehl + " Fehler ====="); if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); } beenden(fehl ? 1 : 0); })().catch((e) => { console.error("Testlauf abgebrochen:", e.message); beenden(1); });