Skip to main content
Glama
ssh-key-file-integration.test.ts13.1 kB
/** * Integration Tests for SSH Key File Enhancement * Tests complete workflow with various key file scenarios */ import { SSHConnectionManager } from "../src/ssh-connection-manager.js"; import { MCPSSHServer } from "../src/mcp-ssh-server.js"; import { SSHConnectionConfig } from "../src/types.js"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; describe("SSH Key File Integration Tests", () => { let connectionManager: SSHConnectionManager; let mcpServer: MCPSSHServer; const testKeyDir = path.join(os.tmpdir(), "ssh-key-integration-test"); // Test RSA key that's properly formatted but still for testing only const validTestRSAKey = `-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA4f6wHWw1mKtP7Vm7rF8vWmEBGkI2vNKPfZ2bE3Sd8Q9Hb2yH YNMqCmKlV5l5Q3aZ9z7K3D3k1J7W2q9B9v4A8F7H3E1QQ6oV5J2b3c4d5E6F7G8H 9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3G4H5I6J7K8L9M0N 1O2P3Q4R5S6T7U8V9W0X1Y2Z3A4B5C6D7E8F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T 3U4V5W6X7Y8Z9A0B1C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z 5A6B7C8D9E0F1G2H3I4J5K6L7M8N9O0P1Q2R3S4T5U6V7W8X9Y0Z1A2B3C4D5E6F 7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3G4H5I6J7K8L QIDAQABAoIBAQCVqJ6mUF1Tt/2VlkV2aZK3uHZ4z8g9C2N3dPH6V2bT9L5N6Z4R 5M3H8I2K9C4J7E6L1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0G1H2I3J4K 5L6M7N8O9P0Q1R2S3T4U5V6W7X8Y9Z0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q 7R8S9T0U1V2W3X4Y5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O1P2Q3R4S5T6U7V8W 9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3L4M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0C 1D2E3F4G5H6I7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0G1H2I J3K4L5M6NAoGBAP5TmVhKiV4p2t5aYP6oI3tFG4qK5r6K7j8K9l0K1m2K3n4K5o6K p7q8K9r0L1s2L3t4L5u6L7v8L9w0M1x2M3y4M5z6M7a8M9b0N1c2N3d4N5e6N7f8N 9g0O1h2O3i4O5j6O7k8O9l0P1m2P3n4P5o6P7p8P9q0Q1r2Q3s4Q5t6Q7u8Q9v0R AoGBAOT9U0VgLGi6v0u7nJ8zYo0qC9I4J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z 4A5B6C7D8E9F0G1H2I3J4K5L6M7N8O9P0Q1R2S3T4U5V6W7X8Y9Z0A1B2C3D4E5F6G 7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N 0O1P2Q3R4S5T6U7V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3L4M5N6O7P8Q9R0S1AoG BALz9T0VhKiV4p2t5aYP6oI3tFG4qK5r6K7j8K9l0K1m2K3n4K5o6Kp7q8K9r0L1s2L 3t4L5u6L7v8L9w0M1x2M3y4M5z6M7a8M9b0N1c2N3d4N5e6N7f8N9g0O1h2O3i4O5j6 O7k8O9l0P1m2P3n4P5o6P7p8P9q0Q1r2Q3s4Q5t6Q7u8Q9v0RAoGBAMj8K9L0M1N2O 3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0G1H2I3J4K5L6M7N8O9P0Q1R2S3T4U5V6 W7X8Y9Z0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D 0E1F2G3H4I5J6K7L8M9N0O1P2Q3R4S5T6U7V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K -----END RSA PRIVATE KEY-----`; // Encrypted test key (for testing encrypted key detection) const encryptedTestKey = `-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,1234567890ABCDEF1234567890ABCDEF MIIEpAIBAAKCAQEA4f6wHWw1mKtP7Vm7rF8vWmEBGkI2vNKPfZ2bE3Sd8Q9Hb2yH Encrypted content here - this is a test encrypted key NOT A REAL ENCRYPTED KEY - FOR TESTING ONLY -----END RSA PRIVATE KEY-----`; beforeEach(() => { connectionManager = new SSHConnectionManager(); mcpServer = new MCPSSHServer(); // Create test directory if (!fs.existsSync(testKeyDir)) { fs.mkdirSync(testKeyDir, { recursive: true }); } }); afterEach(async () => { connectionManager.cleanup(); if (mcpServer) { await mcpServer.stop(); } // Clean up test directory if (fs.existsSync(testKeyDir)) { fs.rmSync(testKeyDir, { recursive: true, force: true }); } }); describe("End-to-End Key File Workflow", () => { it("should successfully read unencrypted key file and attempt connection", async () => { const testKeyPath = path.join(testKeyDir, "test_key"); fs.writeFileSync(testKeyPath, validTestRSAKey); const config: SSHConnectionConfig = { name: "e2e-test-keyfile", host: "localhost", username: "testuser", keyFilePath: testKeyPath }; // Should read the key file and attempt connection // Will fail with connection error (not file error) because localhost SSH isn't set up try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; // Should get connection-related error, not file-related error expect( errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("connection") || errorMessage.includes("All configured authentication methods failed") || errorMessage.includes("Cannot parse privateKey") ).toBe(true); // Should NOT be a file-related error expect(errorMessage).not.toContain("Key file not found"); expect(errorMessage).not.toContain("Cannot read key file"); } }, 15000); it("should handle tilde path expansion correctly", async () => { // Create a test key in actual home directory for this test const homeDir = os.homedir(); const testKeyInHome = path.join(homeDir, ".test-ssh-key-temp"); try { fs.writeFileSync(testKeyInHome, validTestRSAKey); const config: SSHConnectionConfig = { name: "e2e-tilde-test", host: "localhost", username: "testuser", keyFilePath: "~/.test-ssh-key-temp" // Should expand to home directory }; try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; // Should get connection-related error, proving tilde expansion worked expect( errorMessage.includes("ECONNREFUSED") || errorMessage.includes("connection") || errorMessage.includes("All configured authentication methods failed") || errorMessage.includes("Cannot parse privateKey") ).toBe(true); // Should NOT get file not found (proving tilde expansion worked) expect(errorMessage).not.toContain("Key file not found"); } } finally { // Clean up test key from home directory if (fs.existsSync(testKeyInHome)) { fs.unlinkSync(testKeyInHome); } } }, 15000); it("should prioritize privateKey over keyFilePath", async () => { const testKeyPath = path.join(testKeyDir, "ignored_key"); fs.writeFileSync(testKeyPath, validTestRSAKey); const config: SSHConnectionConfig = { name: "e2e-priority-test", host: "localhost", username: "testuser", privateKey: validTestRSAKey, // Should be used keyFilePath: testKeyPath // Should be ignored }; // Since privateKey is provided, keyFilePath should be ignored try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; // Should get connection-related error using privateKey content expect( errorMessage.includes("ECONNREFUSED") || errorMessage.includes("connection") || errorMessage.includes("All configured authentication methods failed") || errorMessage.includes("Cannot parse privateKey") ).toBe(true); } }, 15000); it("should handle encrypted key detection correctly", async () => { const testKeyPath = path.join(testKeyDir, "encrypted_key"); fs.writeFileSync(testKeyPath, encryptedTestKey); const config: SSHConnectionConfig = { name: "e2e-encrypted-test", host: "localhost", username: "testuser", keyFilePath: testKeyPath, passphrase: "testpassword" }; // Should detect encrypted key and pass passphrase to SSH2 try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; // Log for debugging: errorMessage // Should get connection attempt (not key decryption error) // because SSH2 handles encrypted key decryption expect( errorMessage.includes("ECONNREFUSED") || errorMessage.includes("connection") || errorMessage.includes("All configured authentication methods failed") || errorMessage.includes("Cannot parse privateKey") || errorMessage.includes("Error while parsing") || errorMessage.includes("getPrivatePEM is not a function") || errorMessage.includes("key") ).toBe(true); } }, 15000); it("should fail when encrypted key provided without passphrase", async () => { const testKeyPath = path.join(testKeyDir, "encrypted_key_no_pass"); fs.writeFileSync(testKeyPath, encryptedTestKey); const config: SSHConnectionConfig = { name: "e2e-encrypted-no-pass", host: "localhost", username: "testuser", keyFilePath: testKeyPath // No passphrase provided }; // Should fail with specific error about missing passphrase try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; expect(errorMessage).toContain("Key is encrypted but no passphrase provided"); } }, 15000); }); describe("MCP Server Integration", () => { it("should handle keyFilePath via MCP callTool interface", async () => { const testKeyPath = path.join(testKeyDir, "mcp_test_key"); fs.writeFileSync(testKeyPath, validTestRSAKey); const result = await mcpServer.callTool("ssh_connect", { name: "mcp-integration-test", host: "localhost", username: "testuser", keyFilePath: testKeyPath }) as any; // Should fail at connection level, not schema/file level expect(result.success).toBe(false); expect( result.error.includes("ECONNREFUSED") || result.error.includes("connection") || result.error.includes("All configured authentication methods failed") || result.error.includes("Cannot parse privateKey") ).toBe(true); // Should NOT be file-related errors expect(result.error).not.toContain("Key file not found"); }, 15000); it("should handle both keyFilePath and passphrase via MCP interface", async () => { const testKeyPath = path.join(testKeyDir, "mcp_encrypted_key"); fs.writeFileSync(testKeyPath, encryptedTestKey); const result = await mcpServer.callTool("ssh_connect", { name: "mcp-encrypted-test", host: "localhost", username: "testuser", keyFilePath: testKeyPath, passphrase: "testpassword" }) as any; // Should fail at connection level with encrypted key handling expect(result.success).toBe(false); // Log for debugging: result.error expect( result.error.includes("ECONNREFUSED") || result.error.includes("connection") || result.error.includes("All configured authentication methods failed") || result.error.includes("Cannot parse privateKey") || result.error.includes("Error while parsing") || result.error.includes("getPrivatePEM is not a function") || result.error.includes("key") ).toBe(true); }, 15000); }); describe("Error Scenarios", () => { it("should provide clear error for non-existent key file", async () => { const config: SSHConnectionConfig = { name: "error-test-notfound", host: "localhost", username: "testuser", keyFilePath: "/this/path/does/not/exist/key.pem" }; try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; expect(errorMessage).toContain("Key file not accessible"); // Path should not be exposed for security reasons - check it was sanitized expect(errorMessage).not.toContain("/this/path/does/not/exist/key.pem"); } }); it("should require at least one authentication method", async () => { const config: SSHConnectionConfig = { name: "error-test-no-auth", host: "localhost", username: "testuser" // No password, privateKey, or keyFilePath }; try { await connectionManager.createConnection(config); expect(true).toBe(false); // Should not succeed } catch (error) { const errorMessage = (error as Error).message; expect( errorMessage.includes("Either password, privateKey, or keyFilePath must be provided") || errorMessage.includes("All configured authentication methods failed") ).toBe(true); } }, 15000); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LightspeedDMS/ssh-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server