2026-08-19 14:12:06 -05:00
|
|
|
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
2026-10-01 16:20:16 -05:00
|
|
|
|
|
|
|
|
const getLinkedSession = vi.hoisted(() => vi.fn(async () => null));
|
|
|
|
|
vi.mock("@/lib/linked-server", () => ({ getLinkedSession }));
|
2026-08-19 14:12:06 -05:00
|
|
|
import {
|
|
|
|
|
buildOriginWsUrl,
|
|
|
|
|
resolveConnectionOrigin,
|
|
|
|
|
} from "../../lib/connection-origin.js";
|
2026-07-26 18:47:27 -05:00
|
|
|
|
|
|
|
|
const win = window as unknown as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
delete win.IS_ELECTRON;
|
|
|
|
|
delete win.electronAPI;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("resolveConnectionOrigin", () => {
|
2026-09-20 14:56:39 -05:00
|
|
|
// 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.
|
2026-10-01 16:20:16 -05:00
|
|
|
it("resolves to remote with defaultRemote when the host has no override", async () => {
|
2026-09-20 14:56:39 -05:00
|
|
|
win.IS_ELECTRON = true;
|
|
|
|
|
win.electronAPI = {
|
|
|
|
|
invoke: async (channel: string) =>
|
|
|
|
|
channel === "get-desktop-settings"
|
|
|
|
|
? { defaultConnectionOrigin: "local" }
|
|
|
|
|
: null,
|
|
|
|
|
};
|
|
|
|
|
await expect(
|
2026-10-01 16:20:16 -05:00
|
|
|
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 },
|
|
|
|
|
),
|
2026-09-20 14:56:39 -05:00
|
|
|
).resolves.toBe("local");
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-01 16:20:16 -05:00
|
|
|
it("resolves to local outside Electron regardless of the host override", async () => {
|
2026-07-26 18:47:27 -05:00
|
|
|
await expect(
|
|
|
|
|
resolveConnectionOrigin({
|
|
|
|
|
connectionOrigin: "remote",
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBe("local");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("honors a host-level override for ssh in Electron", async () => {
|
|
|
|
|
win.IS_ELECTRON = true;
|
|
|
|
|
await expect(
|
|
|
|
|
resolveConnectionOrigin({
|
|
|
|
|
connectionOrigin: "remote",
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBe("remote");
|
|
|
|
|
await expect(
|
|
|
|
|
resolveConnectionOrigin({
|
|
|
|
|
connectionOrigin: "local",
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBe("local");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("falls back to the desktop-wide default when no host override is set", async () => {
|
|
|
|
|
win.IS_ELECTRON = true;
|
|
|
|
|
win.electronAPI = {
|
|
|
|
|
invoke: async (channel: string) => {
|
|
|
|
|
if (channel === "get-desktop-settings") {
|
|
|
|
|
return { defaultConnectionOrigin: "remote" };
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
await expect(
|
|
|
|
|
resolveConnectionOrigin({
|
|
|
|
|
connectionOrigin: null,
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBe("remote");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("defaults to local when the desktop settings lookup fails", async () => {
|
|
|
|
|
win.IS_ELECTRON = true;
|
|
|
|
|
win.electronAPI = {
|
|
|
|
|
invoke: async () => {
|
|
|
|
|
throw new Error("ipc failed");
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
await expect(
|
|
|
|
|
resolveConnectionOrigin({
|
|
|
|
|
connectionOrigin: null,
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBe("local");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-19 14:12:06 -05:00
|
|
|
|
|
|
|
|
/**
|
2026-10-01 16:20:16 -05:00
|
|
|
* The embedded backend authenticates a local WebSocket from its subprotocol,
|
|
|
|
|
* because the browser WebSocket API cannot set an Authorization header.
|
|
|
|
|
* Electron's main process does inject a JWT cookie, but only on an exact
|
|
|
|
|
* origin match, so a socket has to carry the credential itself.
|
2026-08-19 14:12:06 -05:00
|
|
|
*
|
2026-10-01 16:20:16 -05:00
|
|
|
* This caught a real bug: the Docker console opted out of the token and had no
|
|
|
|
|
* other credential left, so its handshake was closed with 1008 while logs and
|
|
|
|
|
* stats kept working. The fixtures below use the plugin socket routes these
|
|
|
|
|
* channels actually use now; every plugin shares the backend port.
|
2026-08-19 14:12:06 -05:00
|
|
|
*/
|
|
|
|
|
describe("buildOriginWsUrl", () => {
|
|
|
|
|
const store: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
store.jwt = "local-jwt";
|
|
|
|
|
vi.stubGlobal("localStorage", {
|
|
|
|
|
getItem: (k: string) => store[k] ?? null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.unstubAllGlobals();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("carries the local JWT by default", async () => {
|
|
|
|
|
// Every interactive channel on the embedded backend relies on this.
|
2026-09-20 14:56:39 -05:00
|
|
|
const target = await buildOriginWsUrl({
|
2026-08-19 14:12:06 -05:00
|
|
|
origin: "local",
|
2026-10-01 16:20:16 -05:00
|
|
|
localPort: 30001,
|
|
|
|
|
localPath: "/plugin-ws/docker/console",
|
|
|
|
|
remotePath: "/plugin-ws/docker/console",
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
|
2026-09-20 14:56:39 -05:00
|
|
|
expect(target).toEqual({
|
2026-10-01 16:20:16 -05:00
|
|
|
url: "ws://127.0.0.1:30001/plugin-ws/docker/console",
|
2026-09-20 14:56:39 -05:00
|
|
|
protocols: ["termix.jwt.local-jwt"],
|
|
|
|
|
});
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("omits it only when a caller asks", async () => {
|
2026-09-20 14:56:39 -05:00
|
|
|
const target = await buildOriginWsUrl({
|
2026-08-19 14:12:06 -05:00
|
|
|
origin: "local",
|
2026-10-01 16:20:16 -05:00
|
|
|
localPort: 30001,
|
|
|
|
|
localPath: "/plugin-ws/docker/console",
|
|
|
|
|
remotePath: "/plugin-ws/docker/console",
|
2026-09-20 14:56:39 -05:00
|
|
|
includeJwt: false,
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
|
2026-09-20 14:56:39 -05:00
|
|
|
expect(target).toEqual({
|
2026-10-01 16:20:16 -05:00
|
|
|
url: "ws://127.0.0.1:30001/plugin-ws/docker/console",
|
2026-09-20 14:56:39 -05:00
|
|
|
protocols: [],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not duplicate the Guacamole token on remote connections", async () => {
|
2026-10-01 16:20:16 -05:00
|
|
|
getLinkedSession.mockResolvedValueOnce({
|
|
|
|
|
serverUrl: "https://termix.example",
|
|
|
|
|
token: "remote-jwt",
|
|
|
|
|
} as never);
|
2026-09-20 14:56:39 -05:00
|
|
|
|
|
|
|
|
const target = await buildOriginWsUrl({
|
|
|
|
|
origin: "remote",
|
2026-10-01 16:20:16 -05:00
|
|
|
localPort: 30001,
|
|
|
|
|
localPath: "/plugin-ws/remote-desktop/display",
|
|
|
|
|
remotePath: "/plugin-ws/remote-desktop/display",
|
2026-09-20 14:56:39 -05:00
|
|
|
includeJwt: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(target).toEqual({
|
2026-10-01 16:20:16 -05:00
|
|
|
url: "wss://termix.example/plugin-ws/remote-desktop/display",
|
2026-09-20 14:56:39 -05:00
|
|
|
protocols: [],
|
|
|
|
|
});
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("leaves the URL alone when there is no token stored", async () => {
|
|
|
|
|
delete store.jwt;
|
|
|
|
|
|
2026-09-20 14:56:39 -05:00
|
|
|
const target = await buildOriginWsUrl({
|
2026-08-19 14:12:06 -05:00
|
|
|
origin: "local",
|
2026-10-01 16:20:16 -05:00
|
|
|
localPort: 30001,
|
|
|
|
|
localPath: "/plugin-ws/ssh-terminal/terminal",
|
|
|
|
|
remotePath: "/plugin-ws/ssh-terminal/terminal",
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
|
2026-09-20 14:56:39 -05:00
|
|
|
expect(target).toEqual({
|
2026-10-01 16:20:16 -05:00
|
|
|
url: "ws://127.0.0.1:30001/plugin-ws/ssh-terminal/terminal",
|
2026-09-20 14:56:39 -05:00
|
|
|
protocols: [],
|
|
|
|
|
});
|
2026-08-19 14:12:06 -05:00
|
|
|
});
|
|
|
|
|
});
|