import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { join } from "path"; import { getSSHConfigPath, parseSSHConfig, getHostConfig, clearCache, parseConfigContent, } from "../sshConfig"; // Mock os module vi.mock("node:os", async () => { const actual = await vi.importActual("node:os"); return { ...actual, homedir: vi.fn(() => "/home/user"), }; }); // Mock fs module // Define state variables that will be accessed by mocks let mockFileExists = false; let mockFileContent = ""; let mockFileStats: { mtimeMs: number } | null = null; // Create mock functions inside the factory to avoid hoisting issues vi.mock("node:fs", async () => { const actual = await vi.importActual("node:fs"); const mockExistsSync = vi.fn(() => mockFileExists); const mockReadFileSync = vi.fn(() => mockFileContent); const mockStatSync = vi.fn(() => { if (!mockFileStats) { throw new Error("File not found"); } return mockFileStats as any; }); return { ...actual, existsSync: mockExistsSync, readFileSync: mockReadFileSync, statSync: mockStatSync, }; }); // Import fs after mocking to get the mocked functions import * as fs from "node:fs"; describe("sshConfig", () => { beforeEach(() => { vi.clearAllMocks(); clearCache(); mockFileExists = false; mockFileContent = ""; mockFileStats = null; vi.mocked(fs.existsSync).mockClear(); vi.mocked(fs.readFileSync).mockClear(); vi.mocked(fs.statSync).mockClear(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("getSSHConfigPath", () => { it("should return the correct SSH config path", () => { const path = getSSHConfigPath(); expect(path).toBe(join("/home/user", ".ssh", "config")); }); }); describe("parseConfigContent", () => { it("should parse simple host configuration", () => { const content = ` Host myserver HostName example.com User myuser Port 22 `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(1); expect(hosts[0]).toMatchObject({ host: "myserver", hostName: "example.com", user: "myuser", port: 22, }); }); it("should parse multiple hosts", () => { const content = ` Host server1 HostName server1.example.com User user1 Host server2 HostName server2.example.com User user2 Port 2222 `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(2); expect(hosts[0].host).toBe("server1"); expect(hosts[1].host).toBe("server2"); expect(hosts[1].port).toBe(2222); }); it("should skip wildcard hosts", () => { const content = ` Host * User default Host myserver HostName example.com `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(1); expect(hosts[0].host).toBe("myserver"); }); it("should handle multiple host aliases", () => { const content = ` Host server1 server2 server3 HostName example.com User myuser `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(3); expect(hosts.map((h) => h.host)).toEqual([ "server1", "server2", "server3", ]); }); it("should parse identity file and expand ~", () => { const content = ` Host myserver HostName example.com IdentityFile ~/.ssh/id_rsa `; const hosts = parseConfigContent(content); expect(hosts[0].identityFile).toBe("/home/user/.ssh/id_rsa"); }); it("should parse proxy jump", () => { const content = ` Host myserver HostName example.com ProxyJump jumpbox `; const hosts = parseConfigContent(content); expect(hosts[0].proxyJump).toBe("jumpbox"); }); it("should skip comments and empty lines", () => { const content = ` # This is a comment Host myserver # Another comment HostName example.com User myuser Host server2 HostName server2.com `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(2); }); it("should handle global configuration", () => { // Note: The current implementation doesn't support global configuration // This test documents the expected behavior if global config support is added const content = ` User globaluser Port 2222 Host myserver HostName example.com `; const hosts = parseConfigContent(content); // Global config is not currently supported, so user and port will be undefined // This test verifies the current behavior expect(hosts[0].hostName).toBe("example.com"); // If global config support is added, uncomment these: // expect(hosts[0].user).toBe("globaluser"); // expect(hosts[0].port).toBe(2222); }); it("should handle case-insensitive properties", () => { const content = ` Host myserver HOSTNAME example.com USER myuser PORT 2222 `; const hosts = parseConfigContent(content); expect(hosts[0].hostName).toBe("example.com"); expect(hosts[0].user).toBe("myuser"); expect(hosts[0].port).toBe(2222); }); it("should handle invalid port gracefully", () => { const content = ` Host myserver HostName example.com Port invalid `; const hosts = parseConfigContent(content); expect(hosts[0].port).toBeUndefined(); }); it("should return empty array for empty config", () => { const hosts = parseConfigContent(""); expect(hosts).toHaveLength(0); }); it("should handle tabs and spaces for indentation", () => { const content = ` Host myserver HostName example.com User myuser `; const hosts = parseConfigContent(content); expect(hosts).toHaveLength(1); expect(hosts[0].hostName).toBe("example.com"); expect(hosts[0].user).toBe("myuser"); }); }); describe("parseSSHConfig", () => { it("should return empty array if config file does not exist", () => { mockFileExists = false; clearCache(); const hosts = parseSSHConfig(); expect(hosts).toHaveLength(0); }); it("should parse existing config file", () => { const configContent = ` Host myserver HostName example.com User myuser `; clearCache(); mockFileExists = true; mockFileContent = configContent; mockFileStats = { mtimeMs: 1234567890 }; // Reset the mock implementation to return the content vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); const hosts = parseSSHConfig(); expect(hosts).toHaveLength(1); }); it("should cache parsed config", () => { const configContent = ` Host myserver HostName example.com `; clearCache(); mockFileExists = true; mockFileContent = configContent; mockFileStats = { mtimeMs: 1234567890 }; vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); vi.mocked(fs.readFileSync).mockClear(); parseSSHConfig(); parseSSHConfig(); // Should only read file once due to caching expect(vi.mocked(fs.readFileSync)).toHaveBeenCalledTimes(1); }); it("should handle read errors gracefully", () => { // Suppress console.error for this test const consoleSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); clearCache(); mockFileExists = true; mockFileStats = { mtimeMs: 1234567890 }; vi.mocked(fs.readFileSync).mockImplementation(() => { throw new Error("Permission denied"); }); const hosts = parseSSHConfig(); expect(hosts).toHaveLength(0); consoleSpy.mockRestore(); }); }); describe("getHostConfig", () => { it("should return host config by alias", () => { const configContent = ` Host myserver HostName example.com User myuser `; clearCache(); mockFileExists = true; mockFileContent = configContent; mockFileStats = { mtimeMs: 1234567890 }; // Reset the mock implementation to return the content vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); const host = getHostConfig("myserver"); expect(host).toBeDefined(); expect(host?.host).toBe("myserver"); expect(host?.hostName).toBe("example.com"); }); it("should return null for non-existent host", () => { const configContent = ` Host myserver HostName example.com `; clearCache(); mockFileExists = true; mockFileContent = configContent; mockFileStats = { mtimeMs: 1234567890 }; vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); const host = getHostConfig("nonexistent"); expect(host).toBeNull(); }); }); describe("clearCache", () => { it("should clear the cache", () => { const configContent = ` Host myserver HostName example.com `; clearCache(); mockFileExists = true; mockFileContent = configContent; mockFileStats = { mtimeMs: 1234567890 }; vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); vi.mocked(fs.readFileSync).mockClear(); parseSSHConfig(); clearCache(); parseSSHConfig(); // After clearing cache, should read file again expect(vi.mocked(fs.readFileSync)).toHaveBeenCalledTimes(2); }); }); });