import { afterEach, describe, it, expect } from "vitest"; import { setHostProtocolSource } from "../../../hosts/protocol-auth/registry.js"; import { applyHostKeyTypeUpdate, containsOwnerPrivateAuthUpdate, impliedSshAuthType, isNonEmptyString, keepsStoredSshAuth, isOptionalBoolean, isValidPort, normalizeImportedHost, normalizeProtocolEnableFields, renameFolderPath, sanitizeHostForRecipient, stripSensitiveFields, transformHostResponse, } from "../../../database/routes/host-normalizers.js"; describe("applyHostKeyTypeUpdate", () => { it("clears an existing key type when auto-detect sends null", () => { const update: Record = { keyType: "ssh-ed25519" }; applyHostKeyTypeUpdate(update, null); expect(update.keyType).toBeNull(); }); it("does not change the key type when the field is omitted", () => { const update: Record = { keyType: "ssh-ed25519" }; applyHostKeyTypeUpdate(update, undefined); expect(update.keyType).toBe("ssh-ed25519"); }); }); describe("containsOwnerPrivateAuthUpdate", () => { it("detects owner-only SSH auth fields, including explicit clears", () => { expect(containsOwnerPrivateAuthUpdate({ password: null }, "ssh")).toBe( true, ); expect( containsOwnerPrivateAuthUpdate({ credentialId: undefined }, "ssh"), ).toBe(true); expect( containsOwnerPrivateAuthUpdate({ authType: "password" }, "ssh"), ).toBe(true); expect(containsOwnerPrivateAuthUpdate({ shareSshAuth: true }, "ssh")).toBe( true, ); }); it("leaves plugin protocol logins to their own guard", () => { expect( containsOwnerPrivateAuthUpdate( { protocolAuth: { spice: { credentialId: 7 } } }, "ssh", ), ).toBe(false); }); it("allows shared editors to update non-authentication host settings", () => { expect( containsOwnerPrivateAuthUpdate( { name: "renamed", ip: "10.0.0.5", notes: "updated", }, "ssh", ), ).toBe(false); }); }); describe("isNonEmptyString", () => { it("accepts non-blank strings", () => { expect(isNonEmptyString("hello")).toBe(true); expect(isNonEmptyString(" x ")).toBe(true); }); it("rejects blank strings and non-strings", () => { expect(isNonEmptyString("")).toBe(false); expect(isNonEmptyString(" ")).toBe(false); expect(isNonEmptyString(123)).toBe(false); expect(isNonEmptyString(null)).toBe(false); expect(isNonEmptyString(undefined)).toBe(false); }); }); describe("isOptionalBoolean", () => { it("accepts booleans and an omitted value", () => { expect(isOptionalBoolean(true)).toBe(true); expect(isOptionalBoolean(false)).toBe(true); expect(isOptionalBoolean(undefined)).toBe(true); }); it("rejects truthy string and numeric lookalikes", () => { expect(isOptionalBoolean("false")).toBe(false); expect(isOptionalBoolean("0")).toBe(false); expect(isOptionalBoolean(1)).toBe(false); expect(isOptionalBoolean(null)).toBe(false); }); }); describe("normalizeProtocolEnableFields", () => { it("omits unspecified protocol fields so database defaults are preserved", () => { expect(normalizeProtocolEnableFields({ name: "server" })).toEqual({}); }); it("converts an explicitly provided SSH switch to a database integer", () => { expect( normalizeProtocolEnableFields({ enableSsh: true, enableRdp: false }), ).toEqual({ enableSsh: 1 }); }); }); describe("renameFolderPath", () => { it("renames an exact folder match", () => { expect(renameFolderPath("Production", "Production", "Prod")).toBe("Prod"); }); it("re-paths nested children under the renamed ancestor", () => { expect(renameFolderPath("Production / Web", "Production", "Prod")).toBe( "Prod / Web", ); expect( renameFolderPath("Production / Web / app01", "Production", "Prod"), ).toBe("Prod / Web / app01"); }); it("renames a nested folder itself and keeps its parent", () => { expect( renameFolderPath("Production / Web", "Production / Web", "Frontend"), ).toBe("Frontend"); expect( renameFolderPath( "Production / Web / app01", "Production / Web", "Production / Frontend", ), ).toBe("Production / Frontend / app01"); }); it("returns null for unrelated folders", () => { expect(renameFolderPath("Staging", "Production", "Prod")).toBeNull(); expect(renameFolderPath("Production2", "Production", "Prod")).toBeNull(); expect( renameFolderPath("ProductionExtra / Web", "Production", "Prod"), ).toBeNull(); }); }); describe("isValidPort", () => { it("accepts ports in range", () => { expect(isValidPort(1)).toBe(true); expect(isValidPort(22)).toBe(true); expect(isValidPort(65535)).toBe(true); }); it("rejects out-of-range or non-number ports", () => { expect(isValidPort(0)).toBe(false); expect(isValidPort(65536)).toBe(false); expect(isValidPort(-1)).toBe(false); expect(isValidPort("22")).toBe(false); }); }); describe("normalizeImportedHost", () => { it("defaults connectionType to ssh with port 22", () => { const host = normalizeImportedHost({ ip: "10.0.0.1" }); expect(host.connectionType).toBe("ssh"); expect(host.port).toBe(22); expect(host.enableSsh).toBe(true); }); describe("with a plugin protocol declared", () => { afterEach(() => setHostProtocolSource(() => [])); function declareSpice() { setHostProtocolSource(() => [ { id: "spice", defaultPort: 5930, pluginId: "spice-plugin", pluginName: "Spice", }, ]); } it("infers the protocol from its enable flag and uses its default port", () => { declareSpice(); const host = normalizeImportedHost({ enableSpice: true, ip: "10.0.0.2" }); expect(host.connectionType).toBe("spice"); expect(host.port).toBe(5930); expect(host.enableSsh).toBe(false); }); it("reads the protocol's own port field", () => { declareSpice(); const host = normalizeImportedHost({ connectionType: "spice", spicePort: 5999, }); expect(host.port).toBe(5999); }); it("ignores the flag of a protocol nobody declares", () => { const host = normalizeImportedHost({ enableSpice: true }); expect(host.connectionType).toBe("ssh"); }); }); it("honors an explicit port over protocol defaults", () => { const host = normalizeImportedHost({ connectionType: "ssh", port: 2222, }); expect(host.port).toBe(2222); }); it("resolves ip from common aliases", () => { expect(normalizeImportedHost({ address: "a.example" }).ip).toBe( "a.example", ); expect(normalizeImportedHost({ hostname: "h.example" }).ip).toBe( "h.example", ); }); it("normalizes tags from a comma string", () => { const host = normalizeImportedHost({ tags: "prod, db , , web" }); expect(host.tags).toEqual(["prod", "db", "web"]); }); it("normalizes tags from an array", () => { const host = normalizeImportedHost({ tags: ["a", " b ", "", "c"] }); expect(host.tags).toEqual(["a", "b", "c"]); }); it("infers authType credential when credentialId present", () => { const host = normalizeImportedHost({ credentialId: 7 }); expect(host.credentialId).toBe(7); expect(host.authType).toBe("credential"); }); it("infers credential auth from share aliases", () => { const aliasHost = normalizeImportedHost({ credentialAlias: "prod-admin" }); expect(aliasHost.credentialAlias).toBe("prod-admin"); expect(aliasHost.authType).toBe("credential"); const nameHost = normalizeImportedHost({ credentialName: "ops-key" }); expect(nameHost.credentialAlias).toBe("ops-key"); expect(nameHost.authType).toBe("credential"); }); }); describe("stripSensitiveFields", () => { it("removes secret fields and adds boolean presence flags", () => { const result = stripSensitiveFields({ name: "web", password: "secret", key: "PRIVATE KEY", keyPassword: "kp", sudoPassword: "sp", terminalConfig: { theme: "termix", sudoPassword: "nested-sudo", }, }); expect(result.password).toBeUndefined(); expect(result.key).toBeUndefined(); expect(result.keyPassword).toBeUndefined(); expect(result.sudoPassword).toBeUndefined(); expect(result.terminalConfig).toEqual({ theme: "termix" }); expect(result.hasPassword).toBe(true); expect(result.hasKey).toBe(true); expect(result.hasKeyPassword).toBe(true); expect(result.hasSudoPassword).toBe(true); expect(result.name).toBe("web"); }); it("marks presence flags false when secrets are absent", () => { const result = stripSensitiveFields({ name: "web" }); expect(result.hasPassword).toBe(false); expect(result.hasKey).toBe(false); }); it("strips a sudo password nested in terminalConfig", () => { const result = stripSensitiveFields({ name: "web", terminalConfig: { theme: "termix", sudoPassword: "nested-only-sudo", }, }); expect(result.hasSudoPassword).toBe(false); expect( (result.terminalConfig as Record).sudoPassword, ).toBeUndefined(); }); it("leaves protocol login summaries alone for the owner", () => { const protocolAuth = { spice: { authType: "direct", hasPassword: true, secretFieldKeys: [] }, }; expect(stripSensitiveFields({ protocolAuth }).protocolAuth).toEqual( protocolAuth, ); }); }); describe("transformHostResponse", () => { it("parses tags and coerces flags to booleans", () => { const result = transformHostResponse({ tags: "a,b,c", shareSshAuth: 1, pin: 1, }); expect(result.tags).toEqual(["a", "b", "c"]); expect(result.shareSshAuth).toBe(true); expect(result.pin).toBe(true); }); it("parses JSON array fields and defaults them to []", () => { const result = transformHostResponse({ jumpHosts: '[{"hostId":8}]', portKnockSequence: null, }); expect(result.jumpHosts).toEqual([{ hostId: 8 }]); expect(result.portKnockSequence).toEqual([]); }); it("leaves the 2.8 quick_actions column out, for its plugin to put back", () => { const result = transformHostResponse({ quickActions: '[{"name":"x","snippetId":1}]', }); expect(result.quickActions).toBeUndefined(); }); it("passes the stored SSH switch through", () => { // The remote desktop migration corrects connection_type-only hosts once. expect(transformHostResponse({ enableSsh: false }).enableSsh).toBe(false); }); it("applies the default SSH port", () => { const result = transformHostResponse({ port: 22 }); expect(result.sshPort).toBe(22); expect(result).not.toHaveProperty("rdpPort"); }); }); describe("sanitizeHostForRecipient", () => { const sharedHost = { id: 42, userId: "owner", ownerUsername: "owner", isShared: true, permissionLevel: "view", name: "prod", ip: "10.0.0.42", port: 22, username: "root", folder: "servers", parentHostId: 17, tags: ["linux"], notes: "secret runbook", quickActions: [{ name: "restart", snippetId: "1" }], credentialId: 7, shareSshAuth: true, overrideCredentialUsername: true, password: "hunter2", key: "PRIVATE", sudoPassword: "sudo", socks5Password: "socks", protocolAuth: { spice: { authType: "direct", credentialId: null, username: "viewer", fields: { display: "0" }, hasPassword: true, secretFieldKeys: ["ticket"], }, }, enableSsh: true, enableRdp: true, sshPort: 22, rdpPort: 3389, defaultPath: "/srv", terminalConfig: { theme: "termix", sudoPassword: "nested-sudo", agentSocketPath: "/run/user/1000/ssh-agent.sock", }, }; it("always strips secrets for recipients", () => { const result = sanitizeHostForRecipient({ ...sharedHost }, "view"); expect(result.password).toBeUndefined(); expect(result.key).toBeUndefined(); expect(result.sudoPassword).toBeUndefined(); expect(result.socks5Password).toBeUndefined(); expect(result.protocolAuth).toEqual({ spice: { authType: "direct", credentialId: null, username: "viewer", fields: { display: "0" }, }, }); expect(result.credentialId).toBeUndefined(); expect(result.overrideCredentialUsername).toBeUndefined(); expect(result.terminalConfig).toEqual({ theme: "termix" }); expect(result.shareSshAuth).toBe(true); expect(result.hasPassword).toBe(false); expect(result.hasKey).toBe(false); // view keeps configuration fields expect(result.notes).toBe("secret runbook"); expect(result.quickActions).toEqual(sharedHost.quickActions); }); it("never exposes parentHostId to a recipient, at any permission level", () => { // A recipient generally can't see (or share-permission on) the owner's // parent host row, so sub-host tree structure is never leaked -- a // shared host always renders at root for its recipient. expect( sanitizeHostForRecipient({ ...sharedHost }, "view").parentHostId, ).toBeUndefined(); expect( sanitizeHostForRecipient({ ...sharedHost }, "manage").parentHostId, ).toBeUndefined(); expect( sanitizeHostForRecipient({ ...sharedHost }, "connect").parentHostId, ).toBeUndefined(); }); it("reduces connect-level hosts to connection essentials", () => { const result = sanitizeHostForRecipient( { ...sharedHost, permissionLevel: "connect", authOverrides: { ssh: { credentialId: 9, required: false, ownerAuthShared: true, }, }, }, "connect", ); expect(result.name).toBe("prod"); expect(result.ip).toBe("10.0.0.42"); expect(result.protocolAuth).toEqual({ spice: { authType: "direct" } }); expect(result.permissionLevel).toBe("connect"); expect(result.shareSshAuth).toBe(true); expect(result.authOverrides).toEqual({ ssh: { credentialId: 9, required: false, ownerAuthShared: true, }, }); expect(result.notes).toBeUndefined(); expect(result.quickActions).toBeUndefined(); expect(result.password).toBeUndefined(); }); }); describe("transformHostResponse terminal fields", () => { const row = (extra: Record) => transformHostResponse({ id: 1, tags: "", ...extra }); it("sends the SSH options and core's own terminalConfig keys only", () => { const host = row({ sshOptions: JSON.stringify({ keepaliveInterval: 20 }), terminalConfig: JSON.stringify({ startupSnippetId: 3, theme: "nord", keepaliveInterval: 5, }), }); expect(host.sshOptions).toEqual({ keepaliveInterval: 20 }); expect(host.terminalConfig).toEqual({ keepaliveInterval: 20 }); }); it("hides the owner's agent socket from a shared recipient", () => { const shared = sanitizeHostForRecipient( row({ sshOptions: JSON.stringify({ agentSocketPath: "/run/agent", keepaliveInterval: 9, }), }), "edit", ); expect(shared.sshOptions).toEqual({ keepaliveInterval: 9 }); expect(shared.terminalConfig).toEqual({ keepaliveInterval: 9 }); }); }); it("preserves a shared host sync identity for connect-only recipients", () => { const result = sanitizeHostForRecipient( { id: 9, syncId: "remote-host-41", password: "secret", notes: "private" }, "connect", ); expect(result).toEqual({ id: 9, syncId: "remote-host-41" }); }); describe("keepsStoredSshAuth", () => { it("keeps stored secrets only when no auth type is sent", () => { expect(keepsStoredSshAuth(undefined)).toBe(true); expect(keepsStoredSshAuth(null)).toBe(true); expect(keepsStoredSshAuth("password")).toBe(false); expect(keepsStoredSshAuth("none")).toBe(false); }); }); describe("impliedSshAuthType", () => { it("reads the auth type from the secret a create body carries", () => { expect(impliedSshAuthType({ credentialId: 2, password: "pw" })).toBe( "credential", ); expect(impliedSshAuthType({ key: "-----BEGIN", password: "pw" })).toBe( "key", ); expect(impliedSshAuthType({ password: "pw" })).toBe("password"); expect(impliedSshAuthType({})).toBeUndefined(); }); });