import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { generateKeyPairSync } from "crypto"; import ssh2Pkg, { type ParsedKey } from "ssh2"; const mockAccess = vi.fn(); vi.mock("fs/promises", () => ({ access: mockAccess, })); import { FilteredAgent, resolveAgentSocket, } from "../../hosts/terminal-auth-helpers.js"; describe("resolveAgentSocket", () => { const originalEnv = process.env.SSH_AUTH_SOCK; const originalPlatform = process.platform; beforeEach(() => { mockAccess.mockReset(); delete process.env.SSH_AUTH_SOCK; }); afterEach(() => { if (originalEnv !== undefined) { process.env.SSH_AUTH_SOCK = originalEnv; } else { delete process.env.SSH_AUTH_SOCK; } Object.defineProperty(process, "platform", { value: originalPlatform }); }); it("uses explicit socket path from terminalConfig over SSH_AUTH_SOCK", async () => { Object.defineProperty(process, "platform", { value: "linux" }); process.env.SSH_AUTH_SOCK = "/tmp/ssh-env/agent.123"; mockAccess.mockResolvedValue(undefined); const result = await resolveAgentSocket({ agentSocketPath: "/run/user/1000/gnupg/S.gpg-agent.ssh", }); expect(result).toEqual({ socketPath: "/run/user/1000/gnupg/S.gpg-agent.ssh", }); expect(mockAccess).toHaveBeenCalledWith( "/run/user/1000/gnupg/S.gpg-agent.ssh", ); }); it("falls back to SSH_AUTH_SOCK when no explicit path is provided", async () => { Object.defineProperty(process, "platform", { value: "linux" }); process.env.SSH_AUTH_SOCK = "/tmp/ssh-XXXX/agent.456"; mockAccess.mockResolvedValue(undefined); const result = await resolveAgentSocket({}); expect(result).toEqual({ socketPath: "/tmp/ssh-XXXX/agent.456" }); }); it("falls back to SSH_AUTH_SOCK when agentSocketPath is empty string", async () => { Object.defineProperty(process, "platform", { value: "linux" }); process.env.SSH_AUTH_SOCK = "/tmp/ssh-XXXX/agent.789"; mockAccess.mockResolvedValue(undefined); const result = await resolveAgentSocket({ agentSocketPath: " " }); expect(result).toEqual({ socketPath: "/tmp/ssh-XXXX/agent.789" }); }); it("returns error when neither SSH_AUTH_SOCK nor explicit path is set", async () => { const result = await resolveAgentSocket({}); expect(result).toHaveProperty("error"); expect((result as { error: string }).error).toContain("SSH_AUTH_SOCK"); }); it("returns error when terminalConfig is undefined and SSH_AUTH_SOCK is not set", async () => { const result = await resolveAgentSocket(undefined); expect(result).toHaveProperty("error"); }); it("returns error on non-Windows when socket file is missing", async () => { Object.defineProperty(process, "platform", { value: "linux" }); process.env.SSH_AUTH_SOCK = "/tmp/missing-agent.sock"; mockAccess.mockRejectedValue(new Error("ENOENT")); const result = await resolveAgentSocket({}); expect(result).toHaveProperty("error"); expect((result as { error: string }).error).toContain( "/tmp/missing-agent.sock", ); }); it("skips file existence check on Windows", async () => { Object.defineProperty(process, "platform", { value: "win32" }); process.env.SSH_AUTH_SOCK = "\\\\.\\pipe\\openssh-ssh-agent"; const result = await resolveAgentSocket({}); expect(result).toEqual({ socketPath: "\\\\.\\pipe\\openssh-ssh-agent", }); expect(mockAccess).not.toHaveBeenCalled(); }); }); describe("FilteredAgent", () => { it("only returns identities matching the configured public key", async () => { const { privateKey: keyA } = generateKeyPairSync("rsa", { modulusLength: 2048, }); const { privateKey: keyB } = generateKeyPairSync("rsa", { modulusLength: 2048, }); const parsedA = ssh2Pkg.utils.parseKey( keyA.export({ type: "pkcs1", format: "pem" }), ) as ParsedKey; const parsedB = ssh2Pkg.utils.parseKey( keyB.export({ type: "pkcs1", format: "pem" }), ) as ParsedKey; const inner = { getIdentities: (cb: (err: Error | null, keys: ParsedKey[]) => void) => cb(null, [parsedA, parsedB]), getStream: vi.fn(), sign: vi.fn(), }; const filtered = new FilteredAgent( inner as unknown as ConstructorParameters[0], parsedB.getPublicSSH(), ); const identities = await new Promise((resolve, reject) => { filtered.getIdentities((err, keys) => { if (err || !keys) reject(err ?? new Error("Missing identities")); else resolve(keys); }); }); expect(identities).toHaveLength(1); expect(identities[0].getPublicSSH()).toEqual(parsedB.getPublicSSH()); }); it("returns no identities when nothing matches", async () => { const { privateKey: keyA } = generateKeyPairSync("rsa", { modulusLength: 2048, }); const { privateKey: keyB } = generateKeyPairSync("rsa", { modulusLength: 2048, }); const parsedA = ssh2Pkg.utils.parseKey( keyA.export({ type: "pkcs1", format: "pem" }), ) as ParsedKey; const parsedB = ssh2Pkg.utils.parseKey( keyB.export({ type: "pkcs1", format: "pem" }), ) as ParsedKey; const inner = { getIdentities: (cb: (err: Error | null, keys: ParsedKey[]) => void) => cb(null, [parsedA]), getStream: vi.fn(), sign: vi.fn(), }; const filtered = new FilteredAgent( inner as unknown as ConstructorParameters[0], parsedB.getPublicSSH(), ); const identities = await new Promise((resolve, reject) => { filtered.getIdentities((err, keys) => { if (err || !keys) reject(err ?? new Error("Missing identities")); else resolve(keys); }); }); expect(identities).toHaveLength(0); }); });