mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the web-endpoint plugin, add ctx.desktop (B8)
This commit is contained in:
1 parent
1682861853
commit
535126fd1b
50 files changed
+2018
-898
No files matched your search
@@ -0,0 +1,130 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
isElectronIpcAvailable,
|
||||
requestFromElectronMain,
|
||||
} from "../../utils/electron-ipc-bridge.js";
|
||||
|
||||
const originalEnv = process.env.ELECTRON_EMBEDDED;
|
||||
const originalSend = process.send;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.ELECTRON_EMBEDDED = "true";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.ELECTRON_EMBEDDED = originalEnv;
|
||||
process.send = originalSend;
|
||||
});
|
||||
|
||||
describe("isElectronIpcAvailable", () => {
|
||||
it("is false outside the embedded desktop backend", () => {
|
||||
delete process.env.ELECTRON_EMBEDDED;
|
||||
process.send = (() => true) as typeof process.send;
|
||||
expect(isElectronIpcAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when there is no fork IPC channel", () => {
|
||||
process.send = undefined;
|
||||
expect(isElectronIpcAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("is true when embedded with a live channel", () => {
|
||||
process.send = (() => true) as typeof process.send;
|
||||
expect(isElectronIpcAvailable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFromElectronMain", () => {
|
||||
it("refuses when Electron IPC is unavailable", async () => {
|
||||
process.send = undefined;
|
||||
await expect(
|
||||
requestFromElectronMain("open-isolated-window", {}),
|
||||
).rejects.toThrow(/desktop app/);
|
||||
});
|
||||
|
||||
// The module attaches its "message" listener to the real process.on only
|
||||
// once, the first time requestFromElectronMain actually reaches that point
|
||||
// (ensureListening's module-scoped flag mirrors how a real forked
|
||||
// process's IPC channel is a single persistent thing it listens on for the
|
||||
// process lifetime). vi.spyOn(process, "on") does not reliably intercept
|
||||
// that call in this environment, so this replaces process.on directly for
|
||||
// the one call the module makes and hands the captured listener a plain
|
||||
// function this file can invoke to simulate a message from main -
|
||||
// simpler and more direct than getting a spy to survive across a Node
|
||||
// global.
|
||||
let deliver: (msg: unknown) => void = () => {};
|
||||
|
||||
beforeAll(() => {
|
||||
const originalOn = process.on.bind(process);
|
||||
process.on = ((event: string, listener: (...args: unknown[]) => void) => {
|
||||
if (event === "message") {
|
||||
deliver = listener;
|
||||
return process;
|
||||
}
|
||||
return originalOn(event, listener);
|
||||
}) as typeof process.on;
|
||||
});
|
||||
|
||||
let sent: unknown[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
sent = [];
|
||||
process.send = ((msg: unknown) => {
|
||||
sent.push(msg);
|
||||
return true;
|
||||
}) as typeof process.send;
|
||||
});
|
||||
|
||||
it("resolves with main's response, matched by request id", async () => {
|
||||
const pending = requestFromElectronMain<{ success: true }>(
|
||||
"open-isolated-window",
|
||||
{ url: "https://example.test" },
|
||||
);
|
||||
const [message] = sent as Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
channel: string;
|
||||
payload: unknown;
|
||||
}>;
|
||||
expect(message).toMatchObject({
|
||||
type: "backend-request",
|
||||
channel: "open-isolated-window",
|
||||
payload: { url: "https://example.test" },
|
||||
});
|
||||
|
||||
deliver({
|
||||
type: "backend-response",
|
||||
id: message.id,
|
||||
ok: true,
|
||||
result: { success: true },
|
||||
});
|
||||
await expect(pending).resolves.toEqual({ success: true });
|
||||
});
|
||||
|
||||
it("rejects with main's error", async () => {
|
||||
const pending = requestFromElectronMain("open-isolated-window", {});
|
||||
const [message] = sent as Array<{ id: string }>;
|
||||
|
||||
deliver({
|
||||
type: "backend-response",
|
||||
id: message.id,
|
||||
ok: false,
|
||||
error: "refused",
|
||||
});
|
||||
await expect(pending).rejects.toThrow(/refused/);
|
||||
});
|
||||
|
||||
it("ignores a response for a different request id", async () => {
|
||||
const pending = requestFromElectronMain("open-isolated-window", {});
|
||||
const [message] = sent as Array<{ id: string }>;
|
||||
|
||||
deliver({ type: "backend-response", id: "other", ok: true, result: {} });
|
||||
deliver({
|
||||
type: "backend-response",
|
||||
id: message.id,
|
||||
ok: true,
|
||||
result: { success: true },
|
||||
});
|
||||
await expect(pending).resolves.toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* The web endpoint host-columns migration.
|
||||
*
|
||||
* An upgrade must be lossless: a host with web endpoints on and a saved
|
||||
* config must keep both once the ssh_data columns are dropped. Running it
|
||||
* twice must not duplicate or clobber what it moved.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
interface HostRow {
|
||||
id: number;
|
||||
enable_web_ui: boolean;
|
||||
web_ui_config: string | null;
|
||||
}
|
||||
|
||||
interface SettingsRow {
|
||||
pluginId: string;
|
||||
scope: string;
|
||||
scopeId: string | null;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
const hostRows: HostRow[] = [];
|
||||
const settingsRows: SettingsRow[] = [];
|
||||
|
||||
const find = (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
) =>
|
||||
settingsRows.find(
|
||||
(row) =>
|
||||
row.pluginId === pluginId &&
|
||||
row.scope === scope &&
|
||||
row.scopeId === scopeId &&
|
||||
row.key === key,
|
||||
);
|
||||
|
||||
const pluginSettingsRepository = {
|
||||
get: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
) => find(pluginId, scope, scopeId, key) ?? null,
|
||||
set: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
value: string | null,
|
||||
) => {
|
||||
const existing = find(pluginId, scope, scopeId, key);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
return;
|
||||
}
|
||||
settingsRows.push({ pluginId, scope, scopeId, key, value });
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginSettingsRepository: () => pluginSettingsRepository,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/db/index.js", () => ({
|
||||
getDb: () => ({
|
||||
all: async () =>
|
||||
hostRows.filter((row) => row.enable_web_ui || row.web_ui_config !== null),
|
||||
}),
|
||||
}));
|
||||
|
||||
const { runWebEndpointSettingsMigration } =
|
||||
await import("../../utils/crypto-migration/web-endpoint-settings-migration.js");
|
||||
|
||||
function storedValue(hostId: number, key: string): unknown {
|
||||
const row = settingsRows.find(
|
||||
(entry) => entry.scopeId === String(hostId) && entry.key === key,
|
||||
);
|
||||
return row?.value === null || row?.value === undefined
|
||||
? undefined
|
||||
: JSON.parse(row.value);
|
||||
}
|
||||
|
||||
const endpoint = {
|
||||
id: "e1",
|
||||
label: "Proxmox",
|
||||
scheme: "https",
|
||||
port: 8006,
|
||||
path: "/",
|
||||
access: "direct",
|
||||
render: "embedded",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
hostRows.length = 0;
|
||||
settingsRows.length = 0;
|
||||
});
|
||||
|
||||
describe("runWebEndpointSettingsMigration", () => {
|
||||
it("moves a host's switch and saved config into plugin settings", async () => {
|
||||
hostRows.push({
|
||||
id: 1,
|
||||
enable_web_ui: true,
|
||||
web_ui_config: JSON.stringify({ endpoints: [endpoint] }),
|
||||
});
|
||||
|
||||
const result = await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(1);
|
||||
expect(storedValue(1, "enableWebUi")).toBe(true);
|
||||
expect(storedValue(1, "webUiConfig")).toEqual({ endpoints: [endpoint] });
|
||||
expect(settingsRows.every((row) => row.pluginId === "web-endpoint")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(settingsRows.every((row) => row.scope === "host")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a disabled host's saved config and its disabled switch", async () => {
|
||||
hostRows.push({
|
||||
id: 3,
|
||||
enable_web_ui: false,
|
||||
web_ui_config: JSON.stringify({ endpoints: [endpoint] }),
|
||||
});
|
||||
|
||||
await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(storedValue(3, "enableWebUi")).toBe(false);
|
||||
expect(storedValue(3, "webUiConfig")).toEqual({ endpoints: [endpoint] });
|
||||
});
|
||||
|
||||
it("stores an unreadable config as null rather than failing", async () => {
|
||||
hostRows.push({ id: 4, enable_web_ui: true, web_ui_config: "{nope" });
|
||||
|
||||
const result = await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(1);
|
||||
expect(storedValue(4, "webUiConfig")).toBeNull();
|
||||
});
|
||||
|
||||
it("does nothing on a fresh install", async () => {
|
||||
const result = await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(0);
|
||||
expect(settingsRows).toEqual([]);
|
||||
});
|
||||
|
||||
it("is idempotent: a second run changes nothing", async () => {
|
||||
hostRows.push({ id: 2, enable_web_ui: true, web_ui_config: null });
|
||||
|
||||
await runWebEndpointSettingsMigration();
|
||||
const afterFirst = JSON.parse(JSON.stringify(settingsRows));
|
||||
|
||||
const second = await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(second.moved).toBe(0);
|
||||
expect(second.skipped).toBe(1);
|
||||
expect(settingsRows).toEqual(afterFirst);
|
||||
});
|
||||
|
||||
it("does not overwrite a value the plugin already saved", async () => {
|
||||
hostRows.push({
|
||||
id: 5,
|
||||
enable_web_ui: true,
|
||||
web_ui_config: JSON.stringify({ endpoints: [endpoint] }),
|
||||
});
|
||||
await pluginSettingsRepository.set(
|
||||
"web-endpoint",
|
||||
"host",
|
||||
"5",
|
||||
"enableWebUi",
|
||||
"false",
|
||||
);
|
||||
|
||||
const result = await runWebEndpointSettingsMigration();
|
||||
|
||||
expect(result.skipped).toBe(1);
|
||||
expect(storedValue(5, "enableWebUi")).toBe(false);
|
||||
expect(storedValue(5, "webUiConfig")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user