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,4 +1,4 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
renderWithApp,
type RenderedPluginApp,
@@ -45,9 +45,39 @@ describe(`${manifest.id} activate`, () => {
expect(app.registered.panels()).toEqual([]);
expect(app.registered.railItems()).toEqual([]);
expect(app.registered.hostActions()).toEqual([]);
expect(app.registered.hostProtocols()).toEqual([]);
expect(app.registered.hostEditorSections()).toEqual([]);
expect(app.registered.dashboardCards()).toEqual([]);
expect(app.registered.settingsComponents()).toEqual([]);
expect(app.registered.actions()).toEqual([]);
});
it("adds RDP, VNC and Telnet as host protocols while turned on", async () => {
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: { get: async () => ({ data: { enabled: true } }) } as never,
});
await vi.waitFor(() =>
expect(rendered!.registered.hostProtocols().sort()).toEqual([
"rdp",
"telnet",
"vnc",
]),
);
expect(rendered.registered.hostActions().map((a) => a.id)).toEqual(
expect.arrayContaining(["rdp", "vnc", "telnet"]),
);
});
it("offers no way in while an admin has turned it off", async () => {
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: { get: async () => ({ data: { enabled: false } }) } as never,
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(rendered.registered.hostProtocols()).toEqual([]);
expect(rendered.registered.hostActions()).toEqual([]);
});
});
@@ -11,22 +11,27 @@ const remoteApiMock = vi.hoisted(() => ({
const isElectronMock = vi.hoisted(() => vi.fn(() => false));
const resolveRemoteHostIdMock = vi.hoisted(() => vi.fn());
vi.mock("@/main-axios", () => ({
authApi: authApiMock,
getRemoteGuacamoleApi: () => remoteApiMock,
vi.mock("@termix/plugin-sdk/ui", () => ({
isElectron: isElectronMock,
handleApiError: (error: unknown) => error,
}));
vi.mock("@/lib/remote-server-api", () => ({
resolveRemoteHostId: resolveRemoteHostIdMock,
}));
import {
setRemoteDesktopApp,
getGuacdStatus,
getGuacamoleConnectionId,
getGuacamoleTokenFromHost,
} from "../../src/frontend/guacamole-api";
// Mirrors app.apiFor: the remote server only for a remote origin in the
// desktop app, the ordinary client otherwise.
setRemoteDesktopApp({
apiFor: (origin) =>
(origin === "remote" && isElectronMock()
? remoteApiMock
: authApiMock) as never,
});
beforeEach(() => {
authApiMock.get.mockClear();
authApiMock.post.mockClear();
@@ -42,9 +47,7 @@ describe("guacamole API origin", () => {
await getGuacdStatus("remote");
await getGuacamoleTokenFromHost(9, "remote", "vnc");
expect(authApiMock.get).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/status",
);
expect(authApiMock.get).toHaveBeenCalledWith("/status");
expect(authApiMock.post).toHaveBeenCalledOnce();
expect(remoteApiMock.get).not.toHaveBeenCalled();
expect(remoteApiMock.post).not.toHaveBeenCalled();
@@ -60,9 +63,7 @@ describe("guacamole API origin", () => {
expect(status.guacd.status).toBe("connected");
expect(token.token).toBe("remote-token");
expect(remoteApiMock.get).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/status",
);
expect(remoteApiMock.get).toHaveBeenCalledWith("/status");
expect(remoteApiMock.post).toHaveBeenCalledOnce();
expect(authApiMock.get).not.toHaveBeenCalled();
expect(authApiMock.post).not.toHaveBeenCalled();
@@ -84,15 +85,12 @@ describe("guacamole API origin", () => {
"host-sync-id",
);
expect(remoteApiMock.post).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/connect-host/41",
{
protocol: "rdp",
promptedUsername: "admin",
promptedPassword: "secret",
promptedDomain: "EXAMPLE",
},
);
expect(remoteApiMock.post).toHaveBeenCalledWith("/connect-host/41", {
protocol: "rdp",
promptedUsername: "admin",
promptedPassword: "secret",
promptedDomain: "EXAMPLE",
});
expect(resolveRemoteHostIdMock).toHaveBeenCalledWith("host-sync-id");
});
@@ -105,15 +103,12 @@ describe("guacamole API origin", () => {
domain: "",
});
expect(authApiMock.post).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/connect-host/9",
{
protocol: "rdp",
promptedUsername: "local-admin",
promptedPassword: "secret",
promptedDomain: "",
},
);
expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", {
protocol: "rdp",
promptedUsername: "local-admin",
promptedPassword: "secret",
promptedDomain: "",
});
});
// Support#1240: a host set to "This Device" is served by the embedded
@@ -125,12 +120,9 @@ describe("guacamole API origin", () => {
const token = await getGuacamoleTokenFromHost(9, "local", "rdp");
expect(token.token).toBe("local-token");
expect(authApiMock.post).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/connect-host/9",
{
protocol: "rdp",
},
);
expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", {
protocol: "rdp",
});
expect(remoteApiMock.post).not.toHaveBeenCalled();
expect(resolveRemoteHostIdMock).not.toHaveBeenCalled();
});
@@ -141,9 +133,7 @@ describe("guacamole API origin", () => {
const status = await getGuacdStatus("local");
expect(status.guacd.status).toBe("disconnected");
expect(authApiMock.get).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/status",
);
expect(authApiMock.get).toHaveBeenCalledWith("/status");
expect(remoteApiMock.get).not.toHaveBeenCalled();
});
@@ -177,9 +167,8 @@ it("looks up the session on the backend that issued its token", async () => {
isElectronMock.mockReturnValue(true);
const signal = new AbortController().signal;
await getGuacamoleConnectionId("connect/id", "remote", signal);
expect(remoteApiMock.get).toHaveBeenCalledWith(
"/plugin-api/remote-desktop/connection/connect%2Fid",
{ signal },
);
expect(remoteApiMock.get).toHaveBeenCalledWith("/connection/connect%2Fid", {
signal,
});
expect(authApiMock.get).not.toHaveBeenCalled();
});
@@ -1,36 +0,0 @@
import { createRequire } from "node:module";
import { describe, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const { buildRdpFile, validateNativeRdpOptions } =
require("../../../../electron/native-rdp.cjs") as {
buildRdpFile: (options: Record<string, unknown>) => string;
validateNativeRdpOptions: (options: Record<string, unknown>) => unknown;
};
describe("native Windows RDP", () => {
it("creates a credential-prompting RDP file without a password", () => {
const result = buildRdpFile({
host: "server.example.com",
port: 3390,
username: "alice",
domain: "EXAMPLE",
password: "must-not-be-written",
});
expect(result).toContain("full address:s:server.example.com:3390");
expect(result).toContain("username:s:EXAMPLE\\alice");
expect(result).toContain("prompt for credentials:i:1");
expect(result).not.toContain("must-not-be-written");
});
it("rejects unsafe host and port values", () => {
expect(() =>
validateNativeRdpOptions({ host: "bad\r\nvalue" }),
).not.toThrow();
expect(() => validateNativeRdpOptions({ host: "server/path" })).toThrow();
expect(() =>
validateNativeRdpOptions({ host: "server", port: 70000 }),
).toThrow();
});
});
@@ -1,41 +1,51 @@
import { describe, expect, it } from "vitest";
import { createQuickConnectHost } from "@/sidebar/quick-connect-host";
import { quickConnectGuacHost } from "../../src/frontend/quick-connect-guac-host";
// The shape core's Quick Connect builds for a plugin protocol: the switch and
// port in this plugin's host settings, the login on the protocol's fields.
function quickHost(protocol: "rdp" | "vnc", extra: Record<string, unknown>) {
return {
id: "quick-connect-1",
name: "admin@10.0.0.2",
ip: "10.0.0.2",
pluginSettings: {
"remote-desktop": {
[protocol === "rdp" ? "enableRdp" : "enableVnc"]: true,
[protocol === "rdp" ? "rdpPort" : "vncPort"]:
protocol === "rdp" ? 3390 : 5901,
},
},
...extra,
};
}
describe("quickConnectGuacHost", () => {
it("carries an RDP quick connect's address and credentials", () => {
const host = createQuickConnectHost({
ip: "10.0.0.2",
port: 3390,
username: "admin",
authType: "password",
password: "pw",
protocol: "rdp",
domain: "CORP",
});
expect(quickConnectGuacHost(host)).toMatchObject({
expect(
quickConnectGuacHost(
quickHost("rdp", {
rdpUser: "admin",
rdpPassword: "pw",
domain: "CORP",
}),
),
).toMatchObject({
ip: "10.0.0.2",
connectionType: "rdp",
rdpPort: 3390,
rdpUser: "admin",
rdpPassword: "pw",
port: 3390,
username: "admin",
password: "pw",
domain: "CORP",
});
});
it("puts a VNC quick connect's password on the VNC fields", () => {
const host = createQuickConnectHost({
ip: "10.0.0.3",
port: 5901,
username: "",
authType: "password",
password: "vncpw",
protocol: "vnc",
});
expect(quickConnectGuacHost(host)).toMatchObject({
it("takes a VNC quick connect's login from the VNC fields", () => {
expect(
quickConnectGuacHost(quickHost("vnc", { vncPassword: "vncpw" })),
).toMatchObject({
connectionType: "vnc",
vncPort: 5901,
vncPassword: "vncpw",
port: 5901,
password: "vncpw",
});
});
});