Skip to main content
Glama
ooples

MCP Console Automation Server

ssh.test.ts18.2 kB
import { describe, beforeAll, afterAll, beforeEach, afterEach, it, expect, jest } from '@jest/globals'; import { SSHAdapter, SSHOptions } from '../../src/core/SSHAdapter.js'; import { ConsoleManager } from '../../src/core/ConsoleManager.js'; import { SSHServer } from '../mocks/SSHServer.js'; import { readFileSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { writeFileSync, unlinkSync } from 'fs'; // Skip hardware-intensive integration tests in CI const describeIfHardware = process.env.SKIP_HARDWARE_TESTS ? describe.skip : describe; describeIfHardware('SSH Integration Tests', () => { let mockSSHServer: SSHServer; let consoleManager: ConsoleManager; let testKeyPath: string; let testPort: number; beforeAll(async () => { // Start mock SSH server mockSSHServer = new SSHServer(); testPort = await mockSSHServer.start(); // Create test SSH key pair testKeyPath = join(tmpdir(), 'test_ssh_key'); writeFileSync(testKeyPath, `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAFwAAAAdzc2gtcn NhAAAAAwEAAQAAAQEA1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ -----END OPENSSH PRIVATE KEY-----`); consoleManager = new ConsoleManager(); }, 30000); afterAll(async () => { await mockSSHServer.stop(); await consoleManager.destroy(); try { unlinkSync(testKeyPath); } catch (error) { // Ignore cleanup errors } }, 10000); beforeEach(() => { jest.clearAllMocks(); }); describe('SSH Password Authentication', () => { it('should connect with valid password credentials', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).resolves.not.toThrow(); expect(ssh.isActive()).toBe(true); ssh.disconnect(); }); it('should fail with invalid password credentials', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'wrongpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).rejects.toThrow(); }); it('should handle password prompt correctly', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); let passwordPrompted = false; ssh.on('password-prompt', () => { passwordPrompted = true; ssh.sendPassword('testpass'); }); await ssh.connect(options); expect(passwordPrompted).toBe(true); expect(ssh.isActive()).toBe(true); ssh.disconnect(); }); }); describe('SSH Key Authentication', () => { it('should connect with private key authentication', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', privateKey: testKeyPath, timeout: 5000 }; mockSSHServer.setAuthMode('key'); mockSSHServer.addUser('testuser', '', testKeyPath + '.pub'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).resolves.not.toThrow(); expect(ssh.isActive()).toBe(true); ssh.disconnect(); }); it('should fail with invalid private key', async () => { const invalidKeyPath = join(tmpdir(), 'invalid_key'); writeFileSync(invalidKeyPath, 'invalid-key-content'); const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', privateKey: invalidKeyPath, timeout: 5000 }; mockSSHServer.setAuthMode('key'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).rejects.toThrow(); unlinkSync(invalidKeyPath); }); }); describe('SSH Command Execution', () => { let ssh: SSHAdapter; beforeEach(async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); ssh = new SSHAdapter(); await ssh.connect(options); }); afterEach(() => { ssh.disconnect(); }); it('should execute simple commands', async () => { let output = ''; ssh.on('data', (data: string) => { output += data; }); ssh.sendCommand('echo "Hello World"'); // Wait for output await new Promise(resolve => setTimeout(resolve, 1000)); expect(output).toContain('Hello World'); }); it('should handle multiple commands in sequence', async () => { let output = ''; ssh.on('data', (data: string) => { output += data; }); ssh.sendCommand('echo "First"'); ssh.sendCommand('echo "Second"'); ssh.sendCommand('echo "Third"'); // Wait for all outputs await new Promise(resolve => setTimeout(resolve, 2000)); expect(output).toContain('First'); expect(output).toContain('Second'); expect(output).toContain('Third'); }); it('should handle long-running commands', async () => { let output = ''; let dataCount = 0; ssh.on('data', (data: string) => { output += data; dataCount++; }); ssh.sendCommand('for i in {1..5}; do echo "Line $i"; sleep 0.1; done'); // Wait for command completion await new Promise(resolve => setTimeout(resolve, 3000)); expect(dataCount).toBeGreaterThan(0); expect(output).toContain('Line 1'); expect(output).toContain('Line 5'); }); it('should capture stderr output', async () => { let errorOutput = ''; ssh.on('error', (data: string) => { errorOutput += data; }); ssh.sendCommand('echo "Error message" >&2'); // Wait for error output await new Promise(resolve => setTimeout(resolve, 1000)); expect(errorOutput).toContain('Error message'); }); }); describe('SSH Session Persistence', () => { it('should maintain session state across commands', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await ssh.connect(options); let output = ''; ssh.on('data', (data: string) => { output += data; }); // Set environment variable ssh.sendCommand('export TEST_VAR="hello"'); await new Promise(resolve => setTimeout(resolve, 500)); // Check if variable persists ssh.sendCommand('echo $TEST_VAR'); await new Promise(resolve => setTimeout(resolve, 500)); expect(output).toContain('hello'); ssh.disconnect(); }); it('should handle working directory changes', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await ssh.connect(options); let output = ''; ssh.on('data', (data: string) => { output += data; }); // Change directory ssh.sendCommand('cd /tmp'); await new Promise(resolve => setTimeout(resolve, 500)); // Check current directory ssh.sendCommand('pwd'); await new Promise(resolve => setTimeout(resolve, 500)); expect(output).toContain('/tmp'); ssh.disconnect(); }); }); describe('SSH Connection Pooling', () => { it('should reuse existing connections for same host', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh1 = new SSHAdapter(); const ssh2 = new SSHAdapter(); await ssh1.connect(options); await ssh2.connect(options); expect(ssh1.isActive()).toBe(true); expect(ssh2.isActive()).toBe(true); ssh1.disconnect(); ssh2.disconnect(); }); it('should handle connection pool limits', async () => { const connections: SSHAdapter[] = []; const maxConnections = 5; mockSSHServer.setMaxConnections(maxConnections); mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); // Create connections up to the limit for (let i = 0; i < maxConnections; i++) { const ssh = new SSHAdapter(); const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; await ssh.connect(options); connections.push(ssh); } // Try to exceed the limit const extraSsh = new SSHAdapter(); const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 2000 }; await expect(extraSsh.connect(options)).rejects.toThrow(); // Cleanup connections.forEach(ssh => ssh.disconnect()); }); }); describe('SSH Error Handling and Retry', () => { it('should handle connection refused errors', async () => { const options: SSHOptions = { host: 'localhost', port: 9999, // Non-existent port username: 'testuser', password: 'testpass', timeout: 2000 }; const ssh = new SSHAdapter(); let errorReceived = false; ssh.on('connection-refused', () => { errorReceived = true; }); await expect(ssh.connect(options)).rejects.toThrow(); expect(errorReceived).toBe(true); }); it('should handle host unreachable errors', async () => { const options: SSHOptions = { host: '192.0.2.1', // RFC5737 test address port: 22, username: 'testuser', password: 'testpass', timeout: 2000 }; const ssh = new SSHAdapter(); let errorReceived = false; ssh.on('host-unreachable', () => { errorReceived = true; }); await expect(ssh.connect(options)).rejects.toThrow(); }); it('should retry connections on failure', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; // First connection attempt fails mockSSHServer.setFailureMode(true); mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); const connectPromise = ssh.connect(options); // After 2 seconds, enable success mode setTimeout(() => { mockSSHServer.setFailureMode(false); }, 2000); await expect(connectPromise).rejects.toThrow(); // First attempt should fail // Second attempt should succeed await expect(ssh.connect(options)).resolves.not.toThrow(); ssh.disconnect(); }); it('should handle authentication failures gracefully', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'wrongpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); let authFailed = false; ssh.on('auth-failed', () => { authFailed = true; }); await expect(ssh.connect(options)).rejects.toThrow(); expect(authFailed).toBe(true); }); it('should timeout on slow connections', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 1000 // Very short timeout }; mockSSHServer.setSlowMode(true); // Simulate slow connection mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).rejects.toThrow('SSH connection timeout'); mockSSHServer.setSlowMode(false); }); }); describe('SSH with Different Ports and Configs', () => { it('should connect to non-standard SSH port', async () => { const customPort = testPort + 1; const customServer = new SSHServer(); const actualPort = await customServer.start(customPort); const options: SSHOptions = { host: 'localhost', port: actualPort, username: 'testuser', password: 'testpass', timeout: 5000 }; customServer.setAuthMode('password'); customServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).resolves.not.toThrow(); expect(ssh.isActive()).toBe(true); ssh.disconnect(); await customServer.stop(); }); it('should handle strict host key checking disabled', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', strictHostKeyChecking: false, timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await expect(ssh.connect(options)).resolves.not.toThrow(); expect(ssh.isActive()).toBe(true); ssh.disconnect(); }); it('should handle custom connection timeout', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 10000 // Long timeout }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); const startTime = Date.now(); await ssh.connect(options); const connectTime = Date.now() - startTime; expect(connectTime).toBeLessThan(10000); // Should connect quickly expect(ssh.isActive()).toBe(true); ssh.disconnect(); }); it('should handle IPv6 connections', async () => { const options: SSHOptions = { host: '::1', // IPv6 localhost port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); try { await ssh.connect(options); expect(ssh.isActive()).toBe(true); ssh.disconnect(); } catch (error) { // IPv6 might not be available in all test environments expect(error).toBeDefined(); } }); it('should output buffer management', async () => { const options: SSHOptions = { host: 'localhost', port: testPort, username: 'testuser', password: 'testpass', timeout: 5000 }; mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const ssh = new SSHAdapter(); await ssh.connect(options); // Generate large output ssh.sendCommand('for i in {1..100}; do echo "Line $i with some additional text to make it longer"; done'); await new Promise(resolve => setTimeout(resolve, 3000)); const output = ssh.getOutput(); expect(output.length).toBeGreaterThan(0); // Clear output and verify ssh.clearOutput(); expect(ssh.getOutput()).toBe(''); ssh.disconnect(); }); }); describe('ConsoleManager SSH Integration', () => { it('should create SSH sessions through ConsoleManager', async () => { mockSSHServer.setAuthMode('password'); mockSSHServer.addUser('testuser', 'testpass'); const sessionId = await consoleManager.createSession({ command: `ssh testuser@localhost -p ${testPort}`, detectErrors: true, timeout: 10000 }); expect(sessionId).toBeTruthy(); expect(consoleManager.isSessionRunning(sessionId)).toBe(true); await consoleManager.stopSession(sessionId); }); it('should handle SSH session errors in ConsoleManager', async () => { const sessionId = await consoleManager.createSession({ command: 'ssh invaliduser@localhost -p 9999', detectErrors: true, timeout: 5000 }); let errorEventReceived = false; consoleManager.on('console-event', (event) => { if (event.type === 'error' && event.sessionId === sessionId) { errorEventReceived = true; } }); // Wait for error await new Promise(resolve => setTimeout(resolve, 6000)); expect(errorEventReceived).toBe(true); }); }); });

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/ooples/mcp-console-automation'

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