mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the remote-desktop plugin (B14)
This commit is contained in:
1 parent
0af2c94983
commit
14eb558b85
215 files changed
+30029
-3921
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user