mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 23:55:23 +00:00
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const state = vi.hoisted(() => ({
|
|
hosts: [] as Record<string, unknown>[],
|
|
credentials: [] as Record<string, unknown>[],
|
|
resolve: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../../database/repositories/factory.js", () => ({
|
|
createCurrentHostResolutionRepository: () => ({
|
|
findHostsByUserId: async () => state.hosts,
|
|
findHostByIdForUser: async (id: number) =>
|
|
state.hosts.find((host) => host.id === id) ?? null,
|
|
}),
|
|
createCurrentCredentialRepository: () => ({
|
|
listDecryptedByUserId: async () => state.credentials,
|
|
}),
|
|
}));
|
|
vi.mock("../../../database/routes/host-normalizers.js", () => ({
|
|
hostTerminalExport: () => ({ terminalConfig: null, sshOptions: null }),
|
|
}));
|
|
vi.mock("../../../database/routes/host-plugin-settings.js", () => ({
|
|
loadHostPluginSettings: async () => new Map(),
|
|
}));
|
|
vi.mock("../../../hosts/protocol-auth/protocol-auth.js", () => ({
|
|
listProtocolLogins: async () => [],
|
|
toPortableLogins: () => ({
|
|
rdp: {
|
|
authType: "credential",
|
|
credentialId: 4,
|
|
username: "bob",
|
|
password: "rdp-secret",
|
|
fields: { domain: "corp", token: "t" },
|
|
},
|
|
}),
|
|
}));
|
|
vi.mock("../../../hosts/protocol-auth/registry.js", () => ({
|
|
findHostProtocol: () => ({
|
|
credentialFields: [{ key: "token", secret: true }, { key: "domain" }],
|
|
}),
|
|
}));
|
|
vi.mock("../../../hosts/resolve-host-credentials.js", () => ({
|
|
resolveHostCredentials: state.resolve,
|
|
}));
|
|
|
|
import { exportAllHosts } from "../../../hosts/import/host-export.js";
|
|
|
|
const sshHost = {
|
|
id: 1,
|
|
connectionType: "ssh",
|
|
name: "web",
|
|
ip: "192.0.2.1",
|
|
port: 22,
|
|
username: "alice",
|
|
password: "pw",
|
|
key: "KEY",
|
|
keyPassword: "kp",
|
|
sudoPassword: "sp",
|
|
socks5Password: "s5",
|
|
authType: "credential",
|
|
credentialId: 7,
|
|
folder: "Prod",
|
|
tags: "a,b",
|
|
jumpHosts: JSON.stringify([{ hostId: 2 }]),
|
|
forceKeyboardInteractive: "true",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
state.hosts = [{ ...sshHost }];
|
|
state.credentials = [
|
|
{ id: 7, name: "deploy", authType: "key", username: "alice", tags: "x" },
|
|
];
|
|
state.resolve.mockReset().mockImplementation(async (host) => ({
|
|
...host,
|
|
password: "resolved-pw",
|
|
key: "RESOLVED-KEY",
|
|
}));
|
|
});
|
|
|
|
describe("exportAllHosts", () => {
|
|
it("includes decrypted secrets without share mode", async () => {
|
|
const result = await exportAllHosts("u1", false);
|
|
expect(result.credentials).toBeUndefined();
|
|
expect(result.hosts[0]).toMatchObject({
|
|
exportId: 1,
|
|
password: "resolved-pw",
|
|
key: "RESOLVED-KEY",
|
|
sudoPassword: "sp",
|
|
credentialId: 7,
|
|
tags: ["a", "b"],
|
|
jumpHosts: [{ hostId: 2 }],
|
|
forceKeyboardInteractive: true,
|
|
});
|
|
expect(state.resolve).toHaveBeenCalled();
|
|
});
|
|
|
|
it("leaves out every secret and credential id in share mode", async () => {
|
|
const result = await exportAllHosts("u1", true);
|
|
const [host] = result.hosts;
|
|
expect(state.resolve).not.toHaveBeenCalled();
|
|
expect(host).toMatchObject({
|
|
password: null,
|
|
key: null,
|
|
keyPassword: null,
|
|
sudoPassword: null,
|
|
socks5Password: null,
|
|
credentialAlias: "deploy",
|
|
});
|
|
expect(host).not.toHaveProperty("credentialId");
|
|
expect(result.credentials).toEqual([
|
|
expect.objectContaining({ alias: "deploy", authType: "key" }),
|
|
]);
|
|
expect(JSON.stringify(result)).not.toMatch(/rdp-secret|"t"/);
|
|
expect(host.protocolAuth).toMatchObject({
|
|
rdp: {
|
|
authType: "direct",
|
|
credentialId: null,
|
|
password: null,
|
|
fields: { domain: "corp" },
|
|
},
|
|
});
|
|
expect(result.version).toBe("1");
|
|
});
|
|
|
|
it("exports a remote desktop host without SSH fields", async () => {
|
|
state.hosts = [{ ...sshHost, connectionType: "rdp" }];
|
|
const result = await exportAllHosts("u1", false);
|
|
expect(result.hosts[0]).not.toHaveProperty("authType");
|
|
expect(result.hosts[0]).toHaveProperty("protocolAuth");
|
|
});
|
|
});
|
|
|
|
describe("exportAllHosts with hostIds", () => {
|
|
it("exports only the asked hosts the user owns", async () => {
|
|
state.hosts = [
|
|
{ ...sshHost, id: 1 },
|
|
{ ...sshHost, id: 2 },
|
|
];
|
|
const result = await exportAllHosts("u1", false, [2, 99]);
|
|
expect(result.hosts.map((host) => host.exportId)).toEqual([2]);
|
|
});
|
|
|
|
it("returns nothing for an id the user does not own", async () => {
|
|
const result = await exportAllHosts("u1", false, [99]);
|
|
expect(result.hosts).toEqual([]);
|
|
});
|
|
});
|