feat: finish the remote-desktop plugin (B14)

This commit is contained in:
LukeGus committed 2026-09-24 01:02:45 -05:00
1 parent 0af2c94983
commit 14eb558b85
215 files changed
+30029 -3921

No files matched your search

+1 -2
View File
@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";
import {
fromTriState,
parseRemoteDesktopDefaults,
parseTerminalDefaults,
resolveConnectionDefaults,
toTriState,
@@ -10,7 +9,7 @@ import {
describe("connection defaults", () => {
it("ignores malformed persisted defaults", () => {
expect(parseTerminalDefaults("not-json")).toEqual({});
expect(parseRemoteDesktopDefaults("[]")).toEqual({});
expect(parseTerminalDefaults("[]")).toEqual({});
});
it("lets host values override user defaults", () => {
+31 -37
View File
@@ -15,7 +15,7 @@ describe("resolveConnectionOrigin", () => {
// Support#1240: these can now originate from the desktop, but only when a
// host opts in. Left on Default they stay remote, so an upgrade never moves
// an existing host onto a local guacd the user has not set up.
it("resolves rdp/vnc/telnet to remote when the host has no override", async () => {
it("resolves to remote with defaultRemote when the host has no override", async () => {
win.IS_ELECTRON = true;
win.electronAPI = {
invoke: async (channel: string) =>
@@ -23,44 +23,42 @@ describe("resolveConnectionOrigin", () => {
? { defaultConnectionOrigin: "local" }
: null,
};
for (const connectionType of ["rdp", "vnc", "telnet"]) {
await expect(
resolveConnectionOrigin({ connectionType, connectionOrigin: null }),
).resolves.toBe("remote");
}
});
it("honors an explicit local override for rdp/vnc/telnet", async () => {
win.IS_ELECTRON = true;
for (const connectionType of ["rdp", "vnc", "telnet"]) {
await expect(
resolveConnectionOrigin({ connectionType, connectionOrigin: "local" }),
).resolves.toBe("local");
}
});
it("honors an explicit remote override for rdp/vnc/telnet", async () => {
win.IS_ELECTRON = true;
for (const connectionType of ["rdp", "vnc", "telnet"]) {
await expect(
resolveConnectionOrigin({ connectionType, connectionOrigin: "remote" }),
).resolves.toBe("remote");
}
});
it("resolves rdp to local outside Electron, where there is only one backend", async () => {
await expect(
resolveConnectionOrigin({
connectionType: "rdp",
connectionOrigin: null,
}),
resolveConnectionOrigin(
{ connectionOrigin: null },
{ defaultRemote: true },
),
).resolves.toBe("remote");
});
it("honors an explicit override over defaultRemote", async () => {
win.IS_ELECTRON = true;
await expect(
resolveConnectionOrigin(
{ connectionOrigin: "local" },
{ defaultRemote: true },
),
).resolves.toBe("local");
await expect(
resolveConnectionOrigin(
{ connectionOrigin: "remote" },
{ defaultRemote: true },
),
).resolves.toBe("remote");
});
it("resolves to local outside Electron even with defaultRemote", async () => {
await expect(
resolveConnectionOrigin(
{ connectionOrigin: null },
{ defaultRemote: true },
),
).resolves.toBe("local");
});
it("resolves to local outside Electron regardless of connectionType", async () => {
it("resolves to local outside Electron regardless of the host override", async () => {
await expect(
resolveConnectionOrigin({
connectionType: "ssh",
connectionOrigin: "remote",
}),
).resolves.toBe("local");
@@ -70,13 +68,11 @@ describe("resolveConnectionOrigin", () => {
win.IS_ELECTRON = true;
await expect(
resolveConnectionOrigin({
connectionType: "ssh",
connectionOrigin: "remote",
}),
).resolves.toBe("remote");
await expect(
resolveConnectionOrigin({
connectionType: "ssh",
connectionOrigin: "local",
}),
).resolves.toBe("local");
@@ -94,7 +90,6 @@ describe("resolveConnectionOrigin", () => {
};
await expect(
resolveConnectionOrigin({
connectionType: "ssh",
connectionOrigin: null,
}),
).resolves.toBe("remote");
@@ -109,7 +104,6 @@ describe("resolveConnectionOrigin", () => {
};
await expect(
resolveConnectionOrigin({
connectionType: "ssh",
connectionOrigin: null,
}),
).resolves.toBe("local");
@@ -61,6 +61,21 @@ describe("syncHostPluginsTab", () => {
expect(getHostEditorSection(HOST_PLUGINS_TAB_ID)).toBeDefined();
});
it("registers nothing for a plugin whose host fields are all hidden", () => {
const own = plugin({
contributes: {
settings: {
host: {
fields: [
{ key: "port", type: "number", labelKey: "p", hidden: true },
],
},
},
},
} as Partial<PluginSummary>);
expect(syncHostPluginsTab([own])).toBe(false);
});
it("registers nothing when no plugin contributes", () => {
expect(syncHostPluginsTab([])).toBe(false);
expect(getHostEditorSection(HOST_PLUGINS_TAB_ID)).toBeUndefined();
@@ -101,6 +116,32 @@ describe("syncHostPluginsTab", () => {
});
});
describe("HostPluginSections hidden fields", () => {
it("does not draw a field the plugin edits itself", () => {
const own = plugin({
contributes: {
settings: {
host: {
fields: [
{ key: "shown", type: "string", labelKey: "shown.label" },
{
key: "secretish",
type: "string",
labelKey: "hidden.label",
hidden: true,
},
],
},
},
},
} as Partial<PluginSummary>);
render(
<HostPluginSections plugins={[own]} values={{}} setValue={() => {}} />,
);
expect(screen.queryByText("hidden.label")).toBeNull();
});
});
describe("HostPluginSections", () => {
it("shows the enable switch and hides the fields until it is on", () => {
const setValue = vi.fn();
@@ -36,6 +36,7 @@ vi.mock("react-i18next", () => ({
import { HostAuthOverrideModal } from "../../sidebar/HostAuthOverrideModal";
import { canOverrideHostAuth } from "../../sidebar/host-permissions";
import { registerHostProtocol } from "../../sidebar/host-protocols";
const host = {
id: "42",
@@ -238,9 +239,27 @@ describe("canOverrideHostAuth", () => {
expect(
canOverrideHostAuth({ ...host, enableSsh: false } as Host, "ssh"),
).toBe(false);
const dispose = registerHostProtocol({
id: "rdp",
pluginId: "demo",
settingKey: "enableRdp",
defaultPort: 3389,
titleKey: "rdp",
icon: () => null,
});
expect(
canOverrideHostAuth({ ...host, enableRdp: true } as Host, "rdp"),
canOverrideHostAuth(
{ ...host, pluginSettings: { demo: { enableRdp: true } } } as Host,
"rdp",
),
).toBe(true);
expect(canOverrideHostAuth(host, "rdp")).toBe(false);
dispose();
expect(
canOverrideHostAuth(
{ ...host, pluginSettings: { demo: { enableRdp: true } } } as Host,
"rdp",
),
).toBe(false);
});
});
@@ -55,13 +55,7 @@ function makeHost(overrides: Partial<Host>): Host {
enableTmuxMonitor: false,
quickActions: [],
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 22,
rdpPort: 3389,
vncPort: 5900,
telnetPort: 23,
...overrides,
};
}
@@ -136,7 +130,6 @@ describe("HostCredentialList credential usage", () => {
credentialId: undefined,
rdpCredentialId: "1",
enableSsh: false,
enableRdp: true,
}),
],
});
+30 -20
View File
@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
import { registerHostProtocol } from "../../sidebar/host-protocols";
import {
createHostEditorForm,
buildHostEditorPayload,
@@ -568,53 +569,36 @@ describe("RDP/VNC/Telnet password persistence indicator", () => {
describe("user connection defaults", () => {
const defaults = {
terminal: { fontSize: 18, cursorBlink: false },
rdp: { colorDepth: 24, disableCopy: true },
};
it("shows inherited values without persisting them as host overrides", () => {
const host = {
enableSsh: true,
enableRdp: true,
terminalConfig: { autoTmux: true },
guacamoleConfig: { enableAudioInput: true },
} as Host;
} as unknown as Host;
const form = createHostEditorForm(host, undefined, defaults);
expect(form).toMatchObject({
fontSize: 18,
cursorBlink: false,
inheritTerminalAppearance: true,
inheritRemoteDesktopDefaults: true,
guacamoleConfig: {
colorDepth: 24,
disableCopy: true,
enableAudioInput: true,
},
});
const payload = buildHostEditorPayload(form, {
...sshOnly,
enableRdp: true,
});
const payload = buildHostEditorPayload(form, sshOnly);
expect(payload.terminalConfig).toMatchObject({ autoTmux: true });
expect(payload.terminalConfig).not.toHaveProperty("fontSize");
expect(payload.guacamoleConfig).toEqual({ enableAudioInput: true });
});
it("keeps explicit host overrides above user defaults", () => {
const host = {
enableSsh: true,
enableRdp: true,
terminalConfig: { fontSize: 12 },
guacamoleConfig: { colorDepth: 32 },
} as Host;
} as unknown as Host;
const form = createHostEditorForm(host, undefined, defaults);
expect(form).toMatchObject({
fontSize: 12,
inheritTerminalAppearance: false,
inheritRemoteDesktopDefaults: false,
guacamoleConfig: { colorDepth: 32, disableCopy: true },
});
});
});
@@ -694,3 +678,29 @@ describe("macOS Option character defaults", () => {
expect(createHostEditorForm(host).macOptionIsMeta).toBe(true);
});
});
describe("plugin protocols", () => {
it("uses the first protocol switched on as the host's type and port without SSH", () => {
const dispose = registerHostProtocol({
id: "demo-desktop",
pluginId: "demo",
settingKey: "enableDemo",
portKey: "demoPort",
defaultPort: 3389,
titleKey: "demo",
icon: () => null,
});
const form = {
...createHostEditorForm(null),
pluginSettings: { demo: { demoPort: 3390 } },
};
const payload = buildHostEditorPayload(form, {
enableSsh: false,
enableDemo: true,
});
dispose();
expect(payload.connectionType).toBe("demo-desktop");
expect(payload.port).toBe(3390);
expect(payload.enableSsh).toBe(false);
});
});
@@ -0,0 +1,75 @@
import { afterEach, describe, expect, it } from "vitest";
import {
enabledHostProtocols,
hostProtocolFlags,
protocolPort,
registerHostProtocol,
withProtocolSettings,
type HostProtocolDef,
} from "../../sidebar/host-protocols";
const desktop: HostProtocolDef = {
id: "demo-desktop",
pluginId: "demo",
settingKey: "enableDemo",
portKey: "demoPort",
defaultPort: 3389,
titleKey: "demo:title",
icon: () => null,
};
let dispose: (() => void) | null = null;
afterEach(() => {
dispose?.();
dispose = null;
});
describe("host protocols", () => {
it("reads a protocol's switch from its plugin's host settings", () => {
dispose = registerHostProtocol(desktop);
const host = {
enableSsh: false,
pluginSettings: { demo: { enableDemo: true } },
};
expect(hostProtocolFlags(host)).toEqual({
enableSsh: false,
enableDemo: true,
});
expect(enabledHostProtocols(host).map((p) => p.id)).toEqual([
"demo-desktop",
]);
expect(hostProtocolFlags(null)).toEqual({
enableSsh: true,
enableDemo: false,
});
});
it("falls back to the default port", () => {
expect(protocolPort({ demo: { demoPort: 3390 } }, desktop)).toBe(3390);
expect(protocolPort({ demo: { demoPort: "x" } }, desktop)).toBe(3389);
expect(protocolPort(undefined, desktop)).toBe(3389);
});
it("writes the editor's switches into each owner's settings", () => {
dispose = registerHostProtocol(desktop);
expect(
withProtocolSettings(
{ demo: { demoPort: 3390 }, other: { a: 1 } },
{ enableSsh: true, enableDemo: true },
),
).toEqual({
demo: { demoPort: 3390, enableDemo: true },
other: { a: 1 },
});
});
it("forgets a protocol once its plugin unregisters it", () => {
dispose = registerHostProtocol(desktop);
dispose();
dispose = null;
expect(
enabledHostProtocols({ pluginSettings: { demo: { enableDemo: true } } }),
).toEqual([]);
});
});
+16 -22
View File
@@ -61,46 +61,40 @@ describe("createQuickConnectHost for remote desktop protocols", () => {
expect(isQuickConnectHost(host)).toBe(true);
expect(host).toMatchObject({
enableSsh: true,
enableRdp: false,
sshPort: 2222,
password: "pw",
});
});
it("builds an RDP host that GuacamoleApp can mint a token from", () => {
const desktop = {
id: "demo-desktop",
pluginId: "demo",
settingKey: "enableDemo",
portKey: "demoPort",
defaultPort: 3389,
titleKey: "demo",
icon: () => null,
};
it("builds a plugin protocol host with its switch and port in plugin settings", () => {
const host = createQuickConnectHost({
ip: "10.0.0.2",
port: 3390,
username: "admin",
authType: "password",
password: "pw",
protocol: "rdp",
protocol: desktop,
domain: "CORP",
});
expect(host).toMatchObject({
enableSsh: false,
enableRdp: true,
enableVnc: false,
rdpPort: 3390,
rdpUser: "admin",
rdpPassword: "pw",
pluginSettings: { demo: { enableDemo: true, demoPort: 3390 } },
domain: "CORP",
});
});
it("builds a VNC host with the password on the VNC fields", () => {
const host = createQuickConnectHost({
ip: "10.0.0.3",
port: 5901,
username: "",
authType: "password",
password: "vncpw",
protocol: "vnc",
});
// The login sits on the core fields named after the protocol.
expect(host).toMatchObject({
enableVnc: true,
vncPort: 5901,
vncPassword: "vncpw",
"demo-desktopUser": "admin",
"demo-desktopPassword": "pw",
});
});
});
@@ -29,9 +29,6 @@ function host(id: string, name: string): Host {
enableTunnel: false,
enableFileManager: true,
enableDocker: false,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
quickActions: [],
} as Host;
}