theseus/dev/blocklist-selftest.js

100 lines
6.3 KiB
JavaScript
Raw Normal View History

// 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=<scratch dir>
// 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|<button|<a /i.test(inline));
// 3. continue anyway
check("continuing is refused for a name that is not flagged", B.remember("not-flagged.x") === false);
check("continuing is remembered for the flagged name", B.remember(name) === true);
check("after that the name opens", B.decision({ name }, name) === null && (await statusOf(`bns://${name}/`)) !== 403);
const saved = JSON.parse(fs.readFileSync(path.join(app.getPath("userData"), "blocklist.json"), "utf8"));
check("the choice is persisted per name", !!saved.byName[name] && saved.byName[name].reason === "malware", Object.keys(saved.byName));
// 4. policies
B.setPolicy("refuse");
const r = B.decision({ name }, name);
check('policy "refuse" ignores an earlier "continue"', r && r.canContinue === false);
check('…and "continue" cannot be recorded under it', B.remember(name) === false);
B.setPolicy("off");
check('policy "off" ignores the lists', B.decision({ name }, name) === null);
B.setPolicy("warn"); B.forget();
B.setFlags([FLAG("csam")]);
const c = B.decision({ name }, name);
check("reason csam is never offered a way through", c && c.canContinue === false && B.remember(name) === false);
B.setFlags([]);
check("an emptied list lifts the flag", B.decision({ name }, name) === null);
// 5. the interstitial page
const page = (proceed) => ({ 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);
});