diff options
Diffstat (limited to 'test-suite')
| -rw-r--r-- | test-suite/unit/sandboxed-snapshot.test.mjs | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/test-suite/unit/sandboxed-snapshot.test.mjs b/test-suite/unit/sandboxed-snapshot.test.mjs new file mode 100644 index 0000000..9774f21 --- /dev/null +++ b/test-suite/unit/sandboxed-snapshot.test.mjs @@ -0,0 +1,108 @@ +// chrome_snapshot and chrome_inspect on a page served with CSP `sandbox` (no allow-scripts), such as +// raw.githubusercontent.com. There chrome.scripting.executeScript resolves with a null result +// instead of throwing, so the access-denied CDP fallback never ran and the snapshot came back +// undefined. The worker now redoes the injection over CDP Runtime.evaluate, which such pages allow. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import vm from "node:vm"; +import { test } from "node:test"; + +const workerUrl = new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url); +const src = fs.readFileSync(workerUrl, "utf8"); + +// Stands in for snapshot_injected.js when the worker reads it from the package for CDP injection. +const INJECTED_STUB = ` +globalThis.__piChromeSnapshotPage = async (...args) => ({ mode: args[4], title: "raw page" }); +globalThis.__piChromeInspectTarget = async (uid) => ({ target: { uid } }); +`; + +function loadWorker({ scriptingRuns, cdpReturnsNothing = false }) { + const pageWorld = vm.createContext({ Promise, Error, String, JSON }); + pageWorld.globalThis = pageWorld; + const calls = { scripting: 0, cdp: 0 }; + const noop = () => {}; + const listener = { addListener: noop, removeListener: noop }; + const sandbox = { + console, JSON, Date, Math, Promise, Array, Object, String, Number, Boolean, + Error, TypeError, Map, Set, Symbol, structuredClone, + setTimeout, clearTimeout, setInterval: () => 0, clearInterval: noop, + fetch: async (url) => { + assert.match(String(url), /snapshot_injected\.js$/); + return { ok: true, text: async () => INJECTED_STUB }; + }, + navigator: { userAgent: "unit-test" }, + WebSocket: function () {}, + chrome: { + runtime: { + id: "unittestextension", getManifest: () => ({ version: "0.0.0" }), + getURL: (file) => `chrome-extension://unittestextension/${file}`, + onInstalled: listener, onStartup: listener, lastError: null, + }, + alarms: { onAlarm: listener, create: noop, clear: noop, clearAll: noop }, + action: { onClicked: listener }, + debugger: { sendCommand: noop, attach: async () => {}, detach: async () => {}, getTargets: (cb) => cb([]) }, + scripting: { + // A sandboxed document runs no extension script and reports a null result. + executeScript: async ({ files, func, args }) => { + calls.scripting++; + if (!scriptingRuns) return [{ frameId: 0, result: null }]; + if (files) { + vm.runInContext(INJECTED_STUB, pageWorld); + return [{ frameId: 0, result: undefined }]; + } + const fn = vm.runInContext(`(${func.toString()})`, pageWorld); + return [{ frameId: 0, result: await fn(...(args || [])) }]; + }, + }, + tabs: { query: async () => [], get: async () => ({}), create: async () => ({}), update: async () => ({}), remove: async () => {} }, + windows: { update: async () => {} }, + webNavigation: { onCommitted: listener }, + }, + }; + sandbox.globalThis = sandbox; + sandbox.self = sandbox; + vm.createContext(sandbox); + vm.runInContext(src, sandbox); + sandbox.attachDebugger = async () => ({}); + sandbox.bringToFront = async () => {}; + sandbox.getTabByParams = async () => ({ id: 7, windowId: 1, url: "https://raw.githubusercontent.com/o/r/main/f.swift" }); + sandbox.cdp = async (_tabId, method, params) => { + assert.equal(method, "Runtime.evaluate"); + calls.cdp++; + if (cdpReturnsNothing) return { result: { type: "undefined" } }; + // returnByValue: JSON-able values come back as `value`; functions and undefined only as a type. + const value = await vm.runInContext(params.expression, pageWorld); + const json = JSON.stringify(value); + return json === undefined + ? { result: { type: typeof value } } + : { result: { type: "object", value: JSON.parse(json) } }; + }; + return { dispatch: sandbox.dispatch, calls }; +} + +test("snapshot of a sandboxed page falls back to CDP", async () => { + const { dispatch, calls } = loadWorker({ scriptingRuns: false }); + const snapshot = await dispatch("page.snapshot", { mode: "text" }); + assert.equal(snapshot?.title, "raw page"); + assert.equal(snapshot?.mode, "text"); + assert.equal(calls.scripting, 2, "scripting is tried first for both the injection and the call"); + assert.equal(calls.cdp, 2, "both are redone over CDP"); +}); + +test("inspect of a sandboxed page falls back to CDP", async () => { + const { dispatch } = loadWorker({ scriptingRuns: false }); + const inspect = await dispatch("page.inspect", { uid: "e12" }); + assert.equal(inspect?.target?.uid, "e12"); +}); + +test("snapshot of an ordinary page does not touch CDP", async () => { + const { dispatch, calls } = loadWorker({ scriptingRuns: true }); + const snapshot = await dispatch("page.snapshot", { mode: "auto" }); + assert.equal(snapshot?.title, "raw page"); + assert.equal(calls.cdp, 0); +}); + +test("a page that runs neither path reports an error instead of an empty snapshot", async () => { + const { dispatch } = loadWorker({ scriptingRuns: false, cdpReturnsNothing: true }); + await assert.rejects(dispatch("page.snapshot", { mode: "text" }), /snapshot script returned no result in tab 7 \(https:\/\/raw\.githubusercontent\.com/); +}); |
