mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
255 lines
9.9 KiB
TypeScript
255 lines
9.9 KiB
TypeScript
/**
|
|
* Tests for Electron Remote Server Mode
|
|
*
|
|
* Covers:
|
|
* - resolveRemoteServerUrl precedence (env > persisted prefs > null)
|
|
* - URL validation (only http/https accepted, trailing slash stripped)
|
|
* - Corrupt/partial prefs file handled gracefully (falls back to local server)
|
|
* - remoteServerPreferences read/write round-trip
|
|
* - main.js wiring: startNextServer() short-circuits in remote mode, tray
|
|
* menu exposes the toggle, packaging manifest ships the new files
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createRequire } from "node:module";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const {
|
|
resolveRemoteServerUrl,
|
|
isValidHttpUrl,
|
|
} = require("../../electron/lib/resolveRemoteServerUrl");
|
|
const {
|
|
readPreferences,
|
|
writeRemoteServerUrl,
|
|
} = require("../../electron/lib/remoteServerPreferences");
|
|
|
|
function withTempDir(fn: (dir: string) => void) {
|
|
const dir = mkdtempSync(join(tmpdir(), "omniroute-remote-server-"));
|
|
try {
|
|
fn(dir);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe("resolveRemoteServerUrl precedence", () => {
|
|
it("returns null when neither env var nor prefs file are set", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, null);
|
|
});
|
|
});
|
|
|
|
it("prefers OMNIROUTE_REMOTE_URL env var over the persisted prefs file", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://from-prefs:20128");
|
|
|
|
const result = resolveRemoteServerUrl({
|
|
env: { OMNIROUTE_REMOTE_URL: "http://from-env:20128" },
|
|
prefsPath,
|
|
});
|
|
assert.equal(result, "http://from-env:20128");
|
|
});
|
|
});
|
|
|
|
it("falls back to the persisted prefs file when no env var is set", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, "http://localhost:20128");
|
|
});
|
|
});
|
|
|
|
it("strips a trailing slash from the resolved URL", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
const result = resolveRemoteServerUrl({
|
|
env: { OMNIROUTE_REMOTE_URL: "http://localhost:20128/" },
|
|
prefsPath,
|
|
});
|
|
assert.equal(result, "http://localhost:20128");
|
|
});
|
|
});
|
|
|
|
it("rejects a non-http(s) URL (e.g. file:// or javascript:) and returns null", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
for (const bad of ["file:///etc/passwd", "javascript:alert(1)", "not a url", ""]) {
|
|
const result = resolveRemoteServerUrl({ env: { OMNIROUTE_REMOTE_URL: bad }, prefsPath });
|
|
assert.equal(result, null, `expected null for ${JSON.stringify(bad)}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
it("ignores a corrupt prefs file and falls back to null rather than throwing", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
require("node:fs").writeFileSync(prefsPath, "{ not valid json", "utf8");
|
|
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, null);
|
|
});
|
|
});
|
|
|
|
it("treats a missing prefs file as absent rather than throwing", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "does-not-exist.json");
|
|
assert.doesNotThrow(() => resolveRemoteServerUrl({ env: {}, prefsPath }));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isValidHttpUrl", () => {
|
|
it("accepts http and https", () => {
|
|
assert.equal(isValidHttpUrl("http://localhost:20128"), true);
|
|
assert.equal(isValidHttpUrl("https://omniroute.example.com"), true);
|
|
});
|
|
|
|
it("rejects other protocols and invalid strings", () => {
|
|
assert.equal(isValidHttpUrl("ftp://example.com"), false);
|
|
assert.equal(isValidHttpUrl("file:///etc/passwd"), false);
|
|
assert.equal(isValidHttpUrl("javascript:alert(1)"), false);
|
|
assert.equal(isValidHttpUrl("not a url"), false);
|
|
});
|
|
});
|
|
|
|
describe("remoteServerPreferences read/write", () => {
|
|
it("round-trips a URL through write then read", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: "http://localhost:20128" });
|
|
});
|
|
});
|
|
|
|
it("clearing with null removes the preference", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
writeRemoteServerUrl(prefsPath, null);
|
|
assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: null });
|
|
});
|
|
});
|
|
|
|
it("creates the parent directory if it does not exist yet", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "nested", "deep", "electron-preferences.json");
|
|
assert.doesNotThrow(() => writeRemoteServerUrl(prefsPath, "http://localhost:20128"));
|
|
assert.equal(existsSync(prefsPath), true);
|
|
assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: "http://localhost:20128" });
|
|
});
|
|
});
|
|
|
|
it("reading a nonexistent prefs file returns remoteServerUrl: null", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: null });
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── main.js wiring (static-analysis style, matching the repo's existing
|
|
// convention for asserting structure without importing the Electron binary) ───
|
|
|
|
describe("Electron main.js Remote Server Mode wiring", () => {
|
|
const mainSrc = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
|
|
|
|
it("startNextServer() short-circuits before the isDev branch when remoteServerUrl is set", () => {
|
|
const fn = mainSrc.match(/function startNextServer\(\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "startNextServer function should exist in electron/main.js");
|
|
const body = fn![0];
|
|
const remoteIdx = body.indexOf("if (remoteServerUrl)");
|
|
const devIdx = body.indexOf("if (isDev)");
|
|
assert.ok(remoteIdx !== -1, "startNextServer must check remoteServerUrl");
|
|
assert.ok(devIdx !== -1, "startNextServer must still check isDev");
|
|
assert.ok(remoteIdx < devIdx, "the remoteServerUrl check must come before the isDev check");
|
|
});
|
|
|
|
it("getServerUrl() prefers remoteServerUrl over the local port", () => {
|
|
assert.match(
|
|
mainSrc,
|
|
/const getServerUrl = \(\) => remoteServerUrl \|\| `http:\/\/localhost:\$\{serverPort\}`;/
|
|
);
|
|
});
|
|
|
|
it("exposes a tray menu entry to configure or clear the remote server", () => {
|
|
assert.match(mainSrc, /label: "Remote Server"/);
|
|
assert.match(mainSrc, /Connect to Remote Server/);
|
|
assert.match(mainSrc, /Disconnect \(use Local Server\)/);
|
|
});
|
|
|
|
it("the remote-server prompt window uses contextIsolation and disables nodeIntegration", () => {
|
|
const fn = mainSrc.match(/function showRemoteServerPrompt\(\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "showRemoteServerPrompt function should exist");
|
|
const body = fn![0];
|
|
assert.match(body, /contextIsolation:\s*true/);
|
|
assert.match(body, /nodeIntegration:\s*false/);
|
|
});
|
|
|
|
// setRemoteServerUrl() is the runtime, UI-driven path (tray prompt / IPC) for
|
|
// applying an operator-supplied URL — distinct from resolveRemoteServerUrl()'s
|
|
// startup precedence, which is already covered above. A URL typed into the
|
|
// "Connect to Remote Server…" prompt must go through the same isValidHttpUrl
|
|
// guard (only http/https accepted) *before* any server-lifecycle mutation, so
|
|
// an arbitrary/malicious string (file://, javascript:, garbage) can never reach
|
|
// stopNextServer()/startNextServer() or get persisted to prefs. Exercised via
|
|
// static analysis (matching this file's convention) since setRemoteServerUrl
|
|
// requires the full Electron main process to invoke directly.
|
|
it("setRemoteServerUrl() validates via isValidHttpUrl and rejects before mutating server state", () => {
|
|
const fn = mainSrc.match(/async function setRemoteServerUrl\(nextUrl\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "setRemoteServerUrl function should exist in electron/main.js");
|
|
const body = fn![0];
|
|
|
|
const validationIdx = body.indexOf("isValidHttpUrl(normalized)");
|
|
const stopServerIdx = body.indexOf("stopNextServer()");
|
|
assert.ok(validationIdx !== -1, "setRemoteServerUrl must validate via isValidHttpUrl");
|
|
assert.ok(
|
|
stopServerIdx !== -1,
|
|
"setRemoteServerUrl must stop the running server when switching modes"
|
|
);
|
|
assert.ok(
|
|
validationIdx < stopServerIdx,
|
|
"URL validation must run before any server-lifecycle mutation"
|
|
);
|
|
|
|
const rejectBranch = body.slice(validationIdx, stopServerIdx);
|
|
assert.match(
|
|
rejectBranch,
|
|
/return;/,
|
|
"an invalid URL must short-circuit setRemoteServerUrl instead of falling through"
|
|
);
|
|
assert.match(
|
|
rejectBranch,
|
|
/console\.warn/,
|
|
"an invalid URL should be logged so operators can see it was rejected"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Electron packaging manifest includes Remote Server Mode files", () => {
|
|
const pkg = JSON.parse(
|
|
readFileSync(join(import.meta.dirname, "../../electron/package.json"), "utf8")
|
|
);
|
|
const files: string[] = pkg.build?.files ?? [];
|
|
|
|
for (const expected of [
|
|
"lib/resolveRemoteServerUrl.js",
|
|
"lib/remoteServerPreferences.js",
|
|
"remoteServerPromptPreload.js",
|
|
"remoteServerPromptRenderer.js",
|
|
"assets/remoteServerPrompt.html",
|
|
]) {
|
|
it(`ships ${expected} in package.json build.files`, () => {
|
|
assert.ok(files.includes(expected), `${expected} is missing from build.files`);
|
|
});
|
|
}
|
|
});
|