// Node test for the param-redirect security validator and the v1→v2 // migration. Zero dependencies: run with `node --test tests/`. "use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { DEFAULT_STATE, PRESET_TWITTER, PRESET_REDDIT_ACCEPT, PRESET_REDDIT_MEDIA, migrateState, isValidMatchPattern, matchPatternTest, allowedHostMatches, resolveParamDestination, simulateRewrite, } = require("../common.js"); // -------------------------------------------------------------------------- // allowedHostMatches — the allowlist is a redirect primitive, must be airtight // -------------------------------------------------------------------------- test("allowedHostMatches: *.redd.it matches subdomains on label boundaries", () => { assert.ok(allowedHostMatches("i.redd.it", "*.redd.it")); assert.ok(allowedHostMatches("preview.redd.it", "*.redd.it")); assert.ok(!allowedHostMatches("evilredd.it", "*.redd.it")); assert.ok(!allowedHostMatches("redd.it.evil.com", "*.redd.it")); assert.ok(!allowedHostMatches("aredd.it", "*.redd.it")); }); test("allowedHostMatches: matches the bare apex too (matches the old reddit-media behavior)", () => { assert.ok(allowedHostMatches("redd.it", "*.redd.it")); assert.ok(allowedHostMatches("redditmedia.com", "*.redditmedia.com")); }); test("allowedHostMatches: plain host matches itself and subdomains", () => { assert.ok(allowedHostMatches("example.com", "example.com")); assert.ok(allowedHostMatches("img.example.com", "example.com")); assert.ok(!allowedHostMatches("example.com.evil.net", "example.com")); }); test("allowedHostMatches: empty/undefined allowed host never matches", () => { assert.ok(!allowedHostMatches("i.redd.it", "")); assert.ok(!allowedHostMatches("i.redd.it", undefined)); }); // -------------------------------------------------------------------------- // resolveParamDestination — scheme + allowlist + parse // -------------------------------------------------------------------------- test("resolveParamDestination: accepts valid https on an allowed host", () => { const r = resolveParamDestination("https://i.redd.it/abc.jpeg", ["*.redd.it"]); assert.ok(r.ok); assert.equal(r.href, "https://i.redd.it/abc.jpeg"); }); test("resolveParamDestination: accepts http on an allowed host", () => { const r = resolveParamDestination("http://i.redd.it/abc.jpeg", ["*.redd.it"]); assert.ok(r.ok); }); test("resolveParamDestination: handles spaces in a decoded URL", () => { // The content script already decodes the query param, so the destination // arrives here decoded; spaces are re-encoded by `new URL`. const r = resolveParamDestination("https://i.redd.it/ab c.jpeg", ["*.redd.it"]); assert.ok(r.ok); assert.equal(r.href, "https://i.redd.it/ab%20c.jpeg"); }); test("resolveParamDestination: rejects non-http(s) schemes", () => { assert.equal(resolveParamDestination("javascript:alert(1)", ["*"]).reason, "scheme"); assert.equal(resolveParamDestination("file:///etc/passwd", ["*"]).reason, "scheme"); assert.equal(resolveParamDestination("data:text/html,x", ["*"]).reason, "scheme"); }); test("resolveParamDestination: rejects hosts not on the allowlist", () => { const r = resolveParamDestination("https://evil.example/x", ["*.redd.it"]); assert.equal(r.ok, false); assert.equal(r.reason, "host-not-allowed"); assert.equal(r.host, "evil.example"); }); test("resolveParamDestination: rejects unparseable destinations", () => { assert.equal(resolveParamDestination("not a url", ["*"]).reason, "invalid-destination"); assert.equal(resolveParamDestination("", ["*"]).reason, "no-url-param"); assert.equal(resolveParamDestination(undefined, ["*"]).reason, "no-url-param"); }); test("resolveParamDestination: dotted-suffix hosts fail the allowlist", () => { // *.redd.it must not match redd.it.evil.com assert.equal( resolveParamDestination("https://i.redd.it.evil.com/x", ["*.redd.it"]).reason, "host-not-allowed" ); }); // -------------------------------------------------------------------------- // match pattern validation + matching // -------------------------------------------------------------------------- test("isValidMatchPattern: accepts well-formed patterns", () => { assert.ok(isValidMatchPattern("*://*.reddit.com/media*")); assert.ok(isValidMatchPattern("*://*.reddit.com/*")); assert.ok(isValidMatchPattern("https://example.com/*")); assert.ok(isValidMatchPattern("http://example.net/")); assert.ok(isValidMatchPattern("*://example.org/watch")); }); test("isValidMatchPattern: rejects malformed patterns", () => { assert.ok(!isValidMatchPattern("")); assert.ok(!isValidMatchPattern("example.com")); assert.ok(!isValidMatchPattern("https://")); assert.ok(!isValidMatchPattern("javascript://x")); assert.ok(!isValidMatchPattern("https://x y/")); }); test("isValidMatchPattern: a missing path defaults to /", () => { assert.ok(isValidMatchPattern("https://*.reddit.com")); assert.ok(isValidMatchPattern("https://reddit.com")); }); test("matchPatternTest: broad star host matches any host", () => { assert.ok(matchPatternTest("*://*/*", "https://example.com/a")); assert.ok(matchPatternTest("*://*/*", "http://whatever.org/x")); }); test("matchPatternTest: *.reddit.com matches subdomains and the apex", () => { assert.ok(matchPatternTest("*://*.reddit.com/media*", "https://www.reddit.com/media?url=x")); assert.ok(matchPatternTest("*://*.reddit.com/media*", "https://old.reddit.com/media?url=x")); assert.ok(matchPatternTest("*://*.reddit.com/media*", "https://reddit.com/media?url=x")); }); test("matchPatternTest: does not match other hosts or non-matching paths", () => { assert.ok(!matchPatternTest("*://*.reddit.com/media*", "https://evilreddit.com/media")); assert.ok(!matchPatternTest("*://*.reddit.com/media*", "https://www.reddit.com/other")); assert.ok(!matchPatternTest("https://example.com/a", "http://example.com/a")); }); test("matchPatternTest: path wildcard", () => { assert.ok(matchPatternTest("*://*.example.com/foo*", "https://a.example.com/foobar")); assert.ok(!matchPatternTest("*://*.example.com/foo*", "https://a.example.com/bar")); }); // -------------------------------------------------------------------------- // migration v1 → v2 // -------------------------------------------------------------------------- test("migrateState: no stored state seeds fresh v2 defaults", () => { const { state, changed } = migrateState(null); assert.equal(state.version, 2); assert.equal(changed, true); assert.ok(!("builtins" in state)); assert.ok(state.rules.some((r) => r.id === "preset-reddit-accept")); assert.ok(state.rules.some((r) => r.id === "preset-reddit-media")); assert.equal(state.rules.length, 5); }); test("migrateState: v2 state is returned unchanged", () => { const v2 = { version: 2, enabled: true, rules: [] }; const { state, changed } = migrateState(v2); assert.equal(changed, false); assert.equal(state, v2); }); test("migrateState: v1 keeps user rules and appends the two presets", () => { const v1 = { version: 1, enabled: true, builtins: { redditAcceptHeader: true, redditMediaRedirect: true }, rules: [{ id: "user-1", type: "host", enabled: true }], }; const { state, changed } = migrateState(v1); assert.equal(changed, true); assert.equal(state.version, 2); assert.ok(!("builtins" in state)); assert.equal(state.rules.length, 3); assert.equal(state.rules[0].id, "user-1"); const accept = state.rules.find((r) => r.id === "preset-reddit-accept"); const media = state.rules.find((r) => r.id === "preset-reddit-media"); assert.equal(accept.enabled, true); assert.equal(media.enabled, true); }); test("migrateState: v1 disabled builtins become disabled preset rules", () => { const v1 = { version: 1, enabled: true, builtins: { redditAcceptHeader: false, redditMediaRedirect: false }, rules: [], }; const { state } = migrateState(v1); assert.equal(state.rules.find((r) => r.id === "preset-reddit-accept").enabled, false); assert.equal(state.rules.find((r) => r.id === "preset-reddit-media").enabled, false); }); test("migrateState: preserves the global kill switch", () => { const v1 = { version: 1, enabled: false, builtins: {}, rules: [] }; const { state } = migrateState(v1); assert.equal(state.enabled, false); }); // -------------------------------------------------------------------------- // legacy preset upgrade (stock flat preset-twitter → health-checked pool) // -------------------------------------------------------------------------- test("migrateState: stock old preset-twitter upgrades to the pool", () => { const old = { version: 2, enabled: true, rules: [ { id: "preset-twitter", name: "Twitter / X → xcancel", type: "host", enabled: true, allRequests: false, fromDomains: ["twitter.com", "x.com"], toHosts: ["xcancel.com"], }, ], }; const { state, changed } = migrateState(old); assert.equal(changed, true); const tw = state.rules.find((r) => r.id === "preset-twitter"); assert.equal(tw.name, "Twitter / X → nitter"); assert.equal(tw.toHosts.length, 8); assert.equal(tw.healthCheck, true); assert.ok(tw.toHosts.includes("xcancel.com")); assert.ok(tw.toHosts.includes("lightbrd.com")); }); test("migrateState: customized preset-twitter is left alone", () => { const custom = { version: 2, enabled: true, rules: [ { id: "preset-twitter", name: "My twitter rule", type: "host", enabled: true, allRequests: false, fromDomains: ["twitter.com"], toHosts: ["my-nitter.example.com"], }, ], }; const { state, changed } = migrateState(custom); assert.equal(changed, false); assert.equal(state, custom); assert.equal(state.rules[0].toHosts[0], "my-nitter.example.com"); }); test("migrateState: modern pool preset-twitter is left alone", () => { const modern = { version: 2, enabled: true, rules: [Object.assign({}, PRESET_TWITTER)], }; const { state, changed } = migrateState(modern); assert.equal(changed, false); assert.equal(state, modern); }); // -------------------------------------------------------------------------- // simulateRewrite — the two new rule types show up in the tester // -------------------------------------------------------------------------- test("simulateRewrite: accept rule reports a header match", () => { const res = simulateRewrite("https://i.redd.it/abc.jpeg", { enabled: true, rules: [Object.assign({}, PRESET_REDDIT_ACCEPT)], }); assert.equal(res.status, "header"); assert.equal(res.header, "Accept"); assert.ok(res.hosts.includes("i.redd.it")); }); test("simulateRewrite: param-redirect decodes and validates the destination", () => { const res = simulateRewrite( "https://www.reddit.com/media?url=https%3A%2F%2Fi.redd.it%2Fabc.jpeg", { enabled: true, rules: [Object.assign({}, PRESET_REDDIT_MEDIA)] } ); assert.equal(res.status, "param"); assert.equal(res.decoded, "https://i.redd.it/abc.jpeg"); assert.equal(res.dest.ok, true); }); test("simulateRewrite: param-redirect flags a disallowed destination", () => { const res = simulateRewrite( "https://www.reddit.com/media?url=https%3A%2F%2Fevil.example%2Fx", { enabled: true, rules: [Object.assign({}, PRESET_REDDIT_MEDIA)] } ); assert.equal(res.status, "param"); assert.equal(res.dest.ok, false); assert.equal(res.dest.reason, "host-not-allowed"); }); test("simulateRewrite: global kill switch disables everything", () => { const res = simulateRewrite("https://i.redd.it/abc.jpeg", { enabled: false, rules: [Object.assign({}, PRESET_REDDIT_ACCEPT)], }); assert.equal(res.status, "disabled"); }); test("defaults: the seeded presets are valid and self-consistent", () => { assert.equal(DEFAULT_STATE.version, 2); const accept = DEFAULT_STATE.rules.find((r) => r.id === "preset-reddit-accept"); const media = DEFAULT_STATE.rules.find((r) => r.id === "preset-reddit-media"); assert.equal(accept.type, "accept"); assert.equal(media.type, "param-redirect"); assert.ok(isValidMatchPattern(media.pattern)); assert.deepEqual(media.allowedHosts, ["*.redd.it", "*.redditmedia.com"]); });