Files
NetMesh/electron/plugins/browserPluginRuntime.test.cjs

205 lines
7.4 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const test = require("node:test");
const {
BrowserPluginRuntime,
normalizeConsoleLevel,
sanitizePluginSource,
} = require("./browserPluginRuntime.cjs");
class FakePort extends EventEmitter {
constructor() {
super();
this.sent = [];
this.transferLists = [];
}
postMessage(message, transfer = []) {
this.sent.push(message);
this.transferLists.push(transfer);
if (message.method === "plugin.initialize") {
queueMicrotask(() => this.emit("message", { data: {
jsonrpc: "2.0",
id: message.id,
result: {
pluginId: "com.example.browser-runtime",
pluginVersion: "1.0.0",
apiVersion: "0.1.0-internal",
enabledFeatures: [],
},
} }));
} else if (message.method === "plugin.activate" || message.method === "plugin.deactivate") {
queueMicrotask(() => this.emit("message", { data: {
jsonrpc: "2.0",
id: message.id,
result: null,
} }));
}
}
start() {}
close() { this.emit("close"); }
}
test("browser host waits for preload and the installed plugin RPC listener", async () => {
const contents = new EventEmitter();
let resolveLoading;
let postedPort = false;
contents.loadURL = () => new Promise((resolve) => { resolveLoading = resolve; });
contents.postMessage = (_channel, _message, ports) => { postedPort = ports.length === 1; };
contents.setWindowOpenHandler = () => {};
let webRtcPolicy;
contents.setWebRTCIPHandlingPolicy = (policy) => { webRtcPolicy = policy; };
const pluginSession = new EventEmitter();
pluginSession.protocol = { handle() {}, unhandle() {} };
pluginSession.webRequest = {
onBeforeRequest() {},
onCompleted() {},
onErrorOccurred() {},
};
pluginSession.setPermissionCheckHandler = () => {};
pluginSession.setPermissionRequestHandler = () => {};
pluginSession.setDevicePermissionHandler = () => {};
let proxyOptions;
let networkEmulation;
pluginSession.setProxy = async (options) => { proxyOptions = options; };
pluginSession.enableNetworkEmulation = (options) => { networkEmulation = options; };
let windowOptions;
class FakeWindow extends EventEmitter {
constructor(options) {
super();
windowOptions = options;
this.webContents = contents;
}
isDestroyed() { return false; }
destroy() {}
}
const port1 = new FakePort();
const port2 = new FakePort();
const onBeforeMessage = () => {};
let processReadyCalls = 0;
let sessionDisposed = false;
const runtime = new BrowserPluginRuntime({
electron: {
BrowserWindow: FakeWindow,
MessageChannelMain: class { constructor() { this.port1 = port1; this.port2 = port2; } },
session: { fromPartition: () => pluginSession },
},
protocol: {
registerSession: () => ({ dispose() { sessionDisposed = true; } }),
registerRuntime: () => ({
token: "runtime-token",
url: "netcatty-plugin://runtime-token/index.html",
dispose() {},
}),
},
plugin: {
id: "com.example.browser-runtime",
manifest: { main: { browser: "dist/index.js" } },
},
packageRoot: "/plugins/com.example.browser-runtime/1.0.0/package",
preloadPath: "/runtime/browserPreload.cjs",
handlers: {},
onBeforeMessage,
onProcessReady: () => { processReadyCalls += 1; },
});
const started = runtime.start({
pluginId: "com.example.browser-runtime",
pluginVersion: "1.0.0",
netcattyVersion: "0.0.0",
apiVersion: "0.1.0-internal",
supportedFeatures: [],
enabledFeatures: [],
environment: { locale: "en" },
}, {
getActivationEnvironment: () => ({ locale: "zh-CN", theme: "dark" }),
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(processReadyCalls, 1);
contents.emit("ipc-message", {}, "netcatty-plugin:preload-ready");
resolveLoading();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(postedPort, true);
assert.equal(port1.sent.length, 0, "initialize must wait until the peer installs its RPC listener");
contents.emit("ipc-message", {}, "netcatty-plugin:runtime-connected");
const initialized = await started;
assert.equal(initialized.pluginId, "com.example.browser-runtime");
assert.equal(runtime.router.onBeforeMessage, onBeforeMessage);
assert.deepEqual(port1.sent.map((message) => message.method), ["plugin.initialize", "plugin.activate"]);
assert.deepEqual(port1.sent.find((message) => message.method === "plugin.activate").params, {
environment: { locale: "zh-CN", theme: "dark" },
});
const transferable = new ArrayBuffer(8);
runtime.router.send({ type: "transfer-test", transferable }, [transferable]);
assert.deepEqual(port1.transferLists.at(-1), [transferable]);
assert.equal(windowOptions.webPreferences.sandbox, true);
assert.equal(windowOptions.webPreferences.nodeIntegration, false);
assert.equal(windowOptions.webPreferences.contextIsolation, true);
assert.equal(proxyOptions.mode, "fixed_servers");
assert.equal(proxyOptions.proxyBypassRules, "<-loopback>");
assert.equal(webRtcPolicy, "disable_non_proxied_udp");
assert.deepEqual(networkEmulation, { offline: true });
let downloadPrevented = false;
pluginSession.emit("will-download", { preventDefault() { downloadPrevented = true; } });
assert.equal(downloadPrevented, true);
await runtime.stop();
assert.equal(sessionDisposed, true);
assert.doesNotThrow(() => port1.emit("message", { data: {
jsonrpc: "2.0",
method: "late.notification",
} }));
});
test("browser console levels and source paths are normalized without runtime tokens", () => {
assert.equal(normalizeConsoleLevel("error"), "error");
assert.equal(normalizeConsoleLevel("warning"), "warn");
assert.equal(normalizeConsoleLevel("debug"), "debug");
assert.equal(normalizeConsoleLevel(3), "error");
assert.equal(normalizeConsoleLevel(2), "warn");
assert.equal(normalizeConsoleLevel(1), "info");
assert.equal(
sanitizePluginSource("netcatty-plugin://secret-runtime-token/package/dist/index.js"),
"/package/dist/index.js",
);
});
test("aborted browser startup cannot create a window after proxy setup resumes", async () => {
let releaseProxy;
const proxyBlocked = new Promise((resolve) => { releaseProxy = resolve; });
const pluginSession = new EventEmitter();
pluginSession.protocol = { handle() {}, unhandle() {} };
pluginSession.setProxy = () => proxyBlocked;
let windows = 0;
const runtime = new BrowserPluginRuntime({
electron: {
BrowserWindow: class { constructor() { windows += 1; } },
MessageChannelMain: class {},
session: { fromPartition: () => pluginSession },
},
protocol: {
registerSession: () => ({ dispose() {} }),
registerRuntime: () => ({ token: "token", dispose() {} }),
},
plugin: {
id: "com.example.cancelled-browser",
manifest: { main: { browser: "dist/index.js" } },
},
packageRoot: "/plugins/cancelled/package",
preloadPath: "/runtime/browserPreload.cjs",
handlers: {},
});
const controller = new AbortController();
const starting = runtime.start({}, { signal: controller.signal });
await new Promise((resolve) => setImmediate(resolve));
controller.abort(new Error("cancelled"));
await runtime.stop();
releaseProxy();
await assert.rejects(starting, /cancelled/);
assert.equal(windows, 0);
});