// Blocklist harness — exercises the blocklist policy through the REAL // serveBns handler and the real decision functions in main.js, without // starting the app (so no updater, no Ariadne, no real profile). // // set THESEUS_USER_DATA= // npx electron dev/blocklist-selftest.js [name] (default: hello.bch) // // The flags normally arrive from the indexer process, which reads the lists // from the chain; here the harness delivers them through the same message // handler. It checks: // 1. an unflagged name is served as before // 2. a flagged name gets the inline warning from the protocol handler (403) // 3. "continue anyway" is remembered per name, and only for a flagged name // 4. policy "refuse" ignores that choice; reason "csam" never offers one; // policy "off" ignores the lists // 5. blocked.html renders the flag and shows/hides "continue" as told // Prints a JSON report and exits 0 only if every check passed. process.env.THESEUS_NO_AUTOSTART = "1"; if (!process.env.THESEUS_USER_DATA) { console.log("refusing to run without THESEUS_USER_DATA (it would use the real profile)"); process.exit(2); } const { app, BrowserWindow, protocol } = require("electron"); const { serveBns, _blocklistTest: B } = require("../main.js"); const fs = require("fs"); const path = require("path"); app.disableHardwareAcceleration(); app.commandLine.appendSwitch("disable-gpu"); app.commandLine.appendSwitch("no-sandbox"); const name = (process.argv[2] || "hello.bch").toLowerCase(); const outDir = path.join(__dirname, "..", "dev-out"); const watchdog = setTimeout(() => { console.log("WATCHDOG: timed out"); try { app.exit(3); } catch {} }, 90000); const checks = []; const check = (what, ok, detail) => { checks.push({ what, ok: !!ok, ...(detail !== undefined ? { detail } : {}) }); }; const FLAG = (reason) => ({ name, reason, reportTxid: "ab".repeat(32), txid: "cd".repeat(32), height: 1, source: "test-list.x" }); app.whenReady().then(async () => { protocol.handle("bns", serveBns); const win = new BrowserWindow({ width: 1000, height: 700, show: false, webPreferences: { offscreen: true } }); const wc = win.webContents; const titleOf = async (url) => { try { await wc.loadURL(url); } catch {} await new Promise((r) => setTimeout(r, 1500)); return wc.getTitle(); }; const statusOf = async (url) => (await serveBns(new Request(url))).status; const WARN = `Warning: ${name} is flagged`; // 1. unflagged const plainTitle = await titleOf(`bns://${name}/`); check("unflagged name is served as before", plainTitle !== WARN && (await statusOf(`bns://${name}/`)) !== 403, plainTitle); check("no decision without a flag", B.decision({ name }, name) === null); // 2. flagged B.setFlags([FLAG("malware")]); const d = B.decision({ name }, name); check("flagged name: decision offers continue under the default policy", d && d.canContinue === true && d.flag.reason === "malware"); check("a subdomain is governed by its parent's flag", !!B.decision({ name }, "www." + name)); check("protocol handler answers 403 for a flagged name", (await statusOf(`bns://${name}/`)) === 403); check("…and for its subresources", (await statusOf(`bns://${name}/style.css`)) === 403); check("the inline warning is what renders", (await titleOf(`bns://${name}/`)) === WARN); const inline = await wc.executeJavaScript("document.body.innerHTML"); check("the inline warning has no way to approve itself", !/block-choose| ({ search: new URLSearchParams({ host: name, name, reason: "phishing", list: "test-list.x", report: "ab".repeat(32), resturl: "/", proceed }).toString() }); await wc.loadFile(path.join(__dirname, "..", "blocked.html"), page("1")); await new Promise((r2) => setTimeout(r2, 800)); const withGo = await wc.executeJavaScript(`({ title: document.title, name: document.getElementById("name").textContent, why: document.getElementById("why").textContent, go: !document.getElementById("go").hidden, refused: !document.getElementById("refused").hidden, list: document.getElementById("list").textContent })`); check("blocked.html shows the flag", withGo.title === WARN && withGo.name === name && /imitates another site/.test(withGo.why) && withGo.list === "test-list.x", withGo); check("blocked.html offers continue when told it may", withGo.go === true && withGo.refused === false); fs.mkdirSync(outDir, { recursive: true }); try { fs.writeFileSync(path.join(outDir, "blocked.png"), (await wc.capturePage()).toPNG()); } catch {} await wc.loadFile(path.join(__dirname, "..", "blocked.html"), page("0")); await new Promise((r2) => setTimeout(r2, 800)); const noGo = await wc.executeJavaScript(`({ go: !document.getElementById("go").hidden, refused: !document.getElementById("refused").hidden })`); check("blocked.html hides continue when refused", noGo.go === false && noGo.refused === true); const failed = checks.filter((x) => !x.ok); console.log(JSON.stringify({ name, passed: checks.length - failed.length, failed: failed.length, checks }, null, 1)); clearTimeout(watchdog); app.exit(failed.length ? 1 : 0); });