import type { SSHCredentials } from 'n8n-workflow'; import { Client } from 'ssh2'; import { SSHClientsManager } from '@/SSHClientsManager'; describe('SSHClientsManager', () => { const credentials: SSHCredentials = { sshAuthenticateWith: 'password', sshHost: 'example.com', sshPort: 22, sshUser: 'username', sshPassword: 'password', }; let sshClientsManager: SSHClientsManager; const connectSpy = jest.spyOn(Client.prototype, 'connect'); const endSpy = jest.spyOn(Client.prototype, 'end'); beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); sshClientsManager = new SSHClientsManager(); connectSpy.mockImplementation(function (this: Client) { this.emit('ready'); return this; }); }); it('should create a new SSH client', async () => { const client = await sshClientsManager.getClient(credentials); expect(client).toBeInstanceOf(Client); }); it('should not create a new SSH client when connect fails', async () => { connectSpy.mockImplementation(function (this: Client) { throw new Error('Failed to connect'); }); await expect(sshClientsManager.getClient(credentials)).rejects.toThrow('Failed to connect'); }); it('should reuse an existing SSH client', async () => { const client1 = await sshClientsManager.getClient(credentials); const client2 = await sshClientsManager.getClient(credentials); expect(client1).toBe(client2); }); it('should close all SSH connections on process exit', async () => { await sshClientsManager.getClient(credentials); sshClientsManager.onShutdown(); expect(endSpy).toHaveBeenCalledTimes(1); }); it('should cleanup stale SSH connections', async () => { await sshClientsManager.getClient({ ...credentials, sshHost: 'host1' }); await sshClientsManager.getClient({ ...credentials, sshHost: 'host2' }); await sshClientsManager.getClient({ ...credentials, sshHost: 'host3' }); jest.advanceTimersByTime(6 * 60 * 1000); sshClientsManager.cleanupStaleConnections(); expect(endSpy).toHaveBeenCalledTimes(3); expect(sshClientsManager.clients.size).toBe(0); }); });