#!/usr/bin/env node // test-url-policy.js — unit tests for the Vestibule URL policy engine. // // The engine (extension/url-policy.js) is pure logic with no browser // dependencies, so Node loads it directly through the module.exports // arm of its UMD-lite export. Run: node scripts/test-url-policy.js // // Structure follows the project's table-driven test convention: // every case is a row; the driver is one loop; the failure report // names the case, the input, and both expectation and result. "use strict"; const P = require("../extension/url-policy.js"); let passed = 0; let failed = 0; function check(label, actual, expected) { const ok = actual === expected; if (ok) { passed += 1; } else { failed += 1; console.error(` [FAIL] ${label}`); console.error(` expected: ${JSON.stringify(expected)}`); console.error(` actual: ${JSON.stringify(actual)}`); } } // ─── Entry normalization ──────────────────────────────────────────── console.log("==> normalizeDomainEntry"); const NORMALIZE_CASES = [ // [input, expected hostname] ["example.org", "example.org"], [" Example.ORG ", "example.org"], // surrounding whitespace + case ["*.example.org", "example.org"], // wildcard prefix stripped ["https://portal.example.org/welcome", "portal.example.org"], // pasted URL ["http://example.org:8080/path?q=1", "example.org"], // port and path dropped ["https://Example.Org/", "example.org"], ["not a domain", null], // spaces inside — no hostname ["", null], [null, null], [" ", null], ]; NORMALIZE_CASES.forEach(([input, expected]) => { check(`normalize(${JSON.stringify(input)})`, P.normalizeDomainEntry(input), expected); }); // ─── Home hostname extraction ─────────────────────────────────────── console.log("==> homeHostnameOf"); check("http home", P.homeHostnameOf("http://kiosk.example.org/start"), "kiosk.example.org"); check("https home", P.homeHostnameOf("https://portal.example.org/"), "portal.example.org"); check("about:blank is not a home origin", P.homeHostnameOf("about:blank"), null); check("empty", P.homeHostnameOf(""), null); check("missing", P.homeHostnameOf(undefined), null); // ─── Safelist mode decisions ──────────────────────────────────────── console.log("==> safelist mode"); const SAFE = { mode: "safelist", safelist: ["example.org", "cdn.example.net"] }; const sub = { mainFrame: true }; // subresource context would be {mainFrame:false} check("listed domain allowed", P.shouldBlockRequest("https://example.org/welcome", SAFE, sub), false); check("subdomain of listed domain allowed", P.shouldBlockRequest("https://portal.example.org/", SAFE, sub), false); check("deep subdomain allowed", P.shouldBlockRequest("https://a.b.example.org/x", SAFE, sub), false); check("second entry allowed", P.shouldBlockRequest("https://cdn.example.net/lib.js", SAFE, sub), false); check("unlisted domain blocked", P.shouldBlockRequest("https://evil.com/", SAFE, sub), true); check("sibling domain blocked", P.shouldBlockRequest("https://evilexample.org/", SAFE, sub), true); check("query-string smuggle blocked", P.shouldBlockRequest("https://evil.com/?q=example.org", SAFE, sub), true); check("path smuggle blocked", P.shouldBlockRequest("https://evil.com/example.org", SAFE, sub), true); check("userinfo smuggle blocked", P.shouldBlockRequest("https://example.org@evil.com/", SAFE, sub), true); check("empty hostname (file:) blocked", P.shouldBlockRequest("file:///etc/passwd", SAFE, sub), true); // Internal schemes — the browser machinery must keep working. check("about:blank always allowed", P.shouldBlockRequest("about:blank", SAFE, sub), false); check("moz-extension always allowed", P.shouldBlockRequest("moz-extension://abc/blocked.html?u=x", SAFE, sub), false); check("chrome: always allowed", P.shouldBlockRequest("chrome://global/skin/", SAFE, sub), false); check("resource: always allowed", P.shouldBlockRequest("resource://gre/modules/", SAFE, sub), false); // data:/blob: — subresource yes, top-level no. check("data: subresource allowed", P.shouldBlockRequest("data:image/png;base64,AAA", SAFE, { mainFrame: false }), false); check("blob: subresource allowed", P.shouldBlockRequest("blob:https://example.org/uuid", SAFE, { mainFrame: false }), false); check("data: top-level blocked", P.shouldBlockRequest("data:text/html,", SAFE, { mainFrame: true }), true); check("blob: top-level blocked", P.shouldBlockRequest("blob:https://example.org/uuid", SAFE, { mainFrame: true }), true); // Empty safelist — the safe-by-default posture: nothing external loads. const EMPTY = { mode: "safelist", safelist: [] }; check("empty safelist blocks http", P.shouldBlockRequest("https://example.org/", EMPTY, sub), true); check("empty safelist allows about:blank", P.shouldBlockRequest("about:blank", EMPTY, sub), false); // Unnormalized entries in the list still match (storage written by // hand, older tools, etc. — the engine normalizes on read). const RAW = { mode: "safelist", safelist: ["https://Example.ORG/path"] }; check("raw URL entry normalizes on read", P.shouldBlockRequest("https://sub.example.org/", RAW, sub), false); // ─── Home-origin guarantee ────────────────────────────────────────── console.log("==> home-origin guarantee"); const HOME_CTX = { mainFrame: true, homeHostname: "lobby.example.org" }; check("home origin passes empty safelist", P.shouldBlockRequest("https://lobby.example.org/start", EMPTY, HOME_CTX), false); check("home origin passes with safelist active", P.shouldBlockRequest("https://lobby.example.org/", SAFE, HOME_CTX), false); check("subdomain of home is NOT auto-covered", P.shouldBlockRequest("https://www.lobby.example.org/", EMPTY, HOME_CTX), true); check("sibling of home blocked", P.shouldBlockRequest("https://lobby.example.org.evil.com/", EMPTY, HOME_CTX), true); check("home exemption applies to subresources too", P.shouldBlockRequest("https://lobby.example.org/app.js", EMPTY, { mainFrame: false, homeHostname: "lobby.example.org" }), false); // ─── Legacy modes (behavior unchanged from 1.2.0) ────────────────── console.log("==> legacy modes"); check("open never blocks", P.shouldBlockRequest("https://anything.anywhere/", { mode: "open" }, sub), false); check("blocklist blocks substring", P.shouldBlockRequest("https://example.org/blocked/x", { mode: "blocklist", blocklist: ["example.org/blocked"] }, sub), true); check("blocklist allows the rest", P.shouldBlockRequest("https://example.org/ok", { mode: "blocklist", blocklist: ["example.org/blocked"] }, sub), false); check("allowlist allows listed substring", P.shouldBlockRequest("https://example.org/x", { mode: "allowlist", allowlist: ["example.org"] }, sub), false); check("allowlist blocks unlisted", P.shouldBlockRequest("https://other.org/", { mode: "allowlist", allowlist: ["example.org"] }, sub), true); check("allowlist with empty list allows everything (documented legacy quirk)", P.shouldBlockRequest("https://anything.org/", { mode: "allowlist", allowlist: [] }, sub), false); check("substring allowlist passes query-string smuggle (the weakness safelist fixes)", P.shouldBlockRequest("https://evil.com/?q=example.org", { mode: "allowlist", allowlist: ["example.org"] }, sub), false); // ─── Fail-closed on unknown mode ──────────────────────────────────── console.log("==> unknown mode fails closed"); const GARBAGE = { mode: "alllowlist", safelist: [] }; // corrupted policy check("unknown mode blocks external", P.shouldBlockRequest("https://evil.com/", GARBAGE, sub), true); check("unknown mode keeps internal pages working", P.shouldBlockRequest("about:blank", GARBAGE, sub), false); check("unknown mode keeps home working", P.shouldBlockRequest("https://home.org/", GARBAGE, { mainFrame: true, homeHostname: "home.org" }), false); // ─── First-boot startup adoption ──────────────────────────────────── console.log("==> startup adoption"); const DEFAULTS = { mode: "safelist", safelist: [], homeUrl: "about:blank" }; let adopted = P.adoptStartupPolicy( ["about:blank", "https://checkin.example.org/welcome"], DEFAULTS); check("provisioned startup page adopted", adopted !== null, true); check("adopted home URL is the startup page", !!(adopted && adopted.homeUrl === "https://checkin.example.org/welcome"), true); check("adopted safelist is the page's domain", !!(adopted && adopted.safelist.length === 1 && adopted.safelist[0] === "checkin.example.org"), true); check("no http startup tabs → no adoption", P.adoptStartupPolicy(["about:blank"], DEFAULTS), null); check("no tabs at all → no adoption", P.adoptStartupPolicy([], DEFAULTS), null); check("configured home → no adoption", P.adoptStartupPolicy(["https://checkin.example.org/"], { ...DEFAULTS, homeUrl: "https://other.example.org/" }), null); check("non-empty safelist → no adoption", P.adoptStartupPolicy(["https://checkin.example.org/"], { ...DEFAULTS, safelist: ["existing.example.org"] }), null); check("null policy → no adoption", P.adoptStartupPolicy(["https://x.example.org/"], null), null); // The adopted policy must actually admit the startup page through the // engine (home guarantee + safelist entry). check("adopted policy admits the startup page", !!(adopted && !P.shouldBlockRequest(adopted.homeUrl, adopted, { mainFrame: true })), true); check("adopted policy still blocks other domains", !!(adopted && P.shouldBlockRequest("https://evil.com/", adopted, { mainFrame: true })), true); check("adopted policy admits subdomains of the home domain", !!(adopted && !P.shouldBlockRequest("https://portal.checkin.example.org/", adopted, { mainFrame: true })), true); // ─── Report ───────────────────────────────────────────────────────── console.log(""); if (failed === 0) { console.log(`OK — ${passed}/${passed + failed} URL policy assertions pass.`); process.exit(0); } console.log(`FAIL: ${failed} of ${passed + failed} assertions failed.`); process.exit(1);