Skip to main content
Glama
story3-websocket-connection-discovery-integration.test.ts10.3 kB
/** * Story 3: Dynamic WebSocket Connection Discovery - Integration Tests * * These tests validate the full integration between WebSocketConnectionDiscovery * and real MCP server instances, ensuring end-to-end WebSocket connection discovery * works as specified in Story 3 acceptance criteria. * * NO MOCKS - These tests use real MCP server instances and real WebSocket connections * to validate the complete workflow from monitoring URL discovery to WebSocket readiness. */ import { MCPSSHServer } from '../src/mcp-ssh-server.js'; import { WebSocketConnectionDiscovery } from './integration/terminal-history-framework/websocket-connection-discovery.js'; import WebSocket from 'ws'; import { SSHConnectionManager } from '../src/ssh-connection-manager.js'; describe('Story 3: WebSocketConnectionDiscovery - Integration Tests', () => { let mcpServer: MCPSSHServer; let mcpClient: { callTool: (toolName: string, args: unknown) => Promise<unknown> }; let sshManager: SSHConnectionManager; let webServerPort: number; beforeAll(async () => { // Find available port for web server webServerPort = 8080 + Math.floor(Math.random() * 1000); // Create SSH connection manager with discovered port sshManager = new SSHConnectionManager(webServerPort); // Create MCP server instance mcpServer = new MCPSSHServer({}, sshManager); mcpServer.setWebServerPort(webServerPort); // Create MCP client that can call tools directly on the server mcpClient = { callTool: async (toolName: string, args: unknown) => { return await mcpServer.callTool(toolName, args); } }; }); afterAll(async () => { if (mcpServer) { await mcpServer.stop(); } if (sshManager) { sshManager.cleanup(); } }); describe('End-to-End WebSocket Discovery Workflow', () => { it('should discover WebSocket URL when MCP server provides valid monitoring URL', async () => { // ARRANGE: Use real MCP client but simulate session existence by testing URL parsing const discovery = new WebSocketConnectionDiscovery(mcpClient); // Test the URL parsing functionality directly with known good monitoring URL const testMonitoringUrl = `http://localhost:${webServerPort}/session/test-session`; const expectedWebSocketUrl = `ws://localhost:${webServerPort}/ws/session/test-session`; // ACT: Test URL parsing (the core functionality) const parsedWebSocketUrl = discovery.parseMonitoringUrl(testMonitoringUrl); // ASSERT: URL should be properly formatted expect(parsedWebSocketUrl).toBe(expectedWebSocketUrl); expect(parsedWebSocketUrl).toMatch(/^ws:\/\/localhost:\d+\/ws\/session\/test-session$/); }); it('should fail gracefully for non-existent session', async () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // ACT & ASSERT: Should handle non-existent session gracefully await expect(discovery.discoverWebSocketUrl('nonexistent-session')) .rejects.toThrow(/Failed to discover monitoring URL.*not found/); }); it('should parse different URL formats correctly', async () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // Test different URL formats that MCP server might return const testCases = [ { monitoringUrl: `http://localhost:${webServerPort}/session/session1`, expectedWsUrl: `ws://localhost:${webServerPort}/ws/session/session1` }, { monitoringUrl: `https://localhost:${webServerPort}/session/session2`, expectedWsUrl: `wss://localhost:${webServerPort}/ws/session/session2` }, { monitoringUrl: `http://example.com:3000/session/remote-session`, expectedWsUrl: `ws://example.com:3000/ws/session/remote-session` } ]; // ACT & ASSERT: Test each URL format conversion for (const testCase of testCases) { const result = discovery.parseMonitoringUrl(testCase.monitoringUrl); expect(result).toBe(testCase.expectedWsUrl); } }); }); describe('WebSocket Connection Establishment Integration', () => { it('should test WebSocket connection establishment logic with real URLs', async () => { // ARRANGE: Test the connection logic with real WebSocket addresses const discovery = new WebSocketConnectionDiscovery(mcpClient); // Create a WebSocket URL that will test the connection logic const webSocketUrl = `ws://localhost:${webServerPort}/ws/session/test-session`; // ACT: Test the connection establishment logic try { const webSocket = await discovery.establishConnection(webSocketUrl, 2000); // ASSERT: If connection succeeds, validate it properly expect(webSocket).toBeDefined(); expect(discovery.validateConnection(webSocket)).toBe(true); expect(webSocket.readyState).toBe(WebSocket.OPEN); // CLEANUP webSocket.close(); } catch (error) { // If connection fails, verify it's handled properly with real error expect(error).toBeDefined(); expect(error).toBeInstanceOf(Error); } }, 8000); it('should handle connection failure with real refused connections', async () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); const invalidWebSocketUrl = 'ws://localhost:99999/ws/session/nonexistent'; // Invalid port // ACT & ASSERT: Should handle real connection failures await expect(discovery.establishConnection(invalidWebSocketUrl, 2000)) .rejects.toThrow(/Failed to create WebSocket|WebSocket connection failed/); }, 5000); it('should handle real connection timeout scenarios', async () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // Use a URL that will cause timeout - blackhole IP that won't respond const timeoutWebSocketUrl = 'ws://10.255.255.1:8080/ws/session/timeout-test'; // ACT & ASSERT: Should timeout with real network conditions await expect(discovery.establishConnection(timeoutWebSocketUrl, 1000)) .rejects.toThrow(/WebSocket connection timeout after 1000ms|Failed to create WebSocket/); }, 3000); it('should have proper timeout configuration', () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // ASSERT: Should have reasonable default timeout expect(discovery.getDefaultConnectionTimeout()).toBe(5000); // Should be configurable discovery.setDefaultConnectionTimeout(3000); expect(discovery.getDefaultConnectionTimeout()).toBe(3000); }); it('should reject invalid timeout values', () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // ACT & ASSERT: Invalid timeouts should be rejected expect(() => discovery.setDefaultConnectionTimeout(0)).toThrow('Connection timeout must be positive'); expect(() => discovery.setDefaultConnectionTimeout(-1)).toThrow('Connection timeout must be positive'); // Valid timeout should work expect(() => discovery.setDefaultConnectionTimeout(1000)).not.toThrow(); }); }); describe('Connection Validation', () => { it('should validate WebSocket connection states correctly', () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); // Mock different WebSocket states const openSocket = { readyState: WebSocket.OPEN }; const connectingSocket = { readyState: WebSocket.CONNECTING }; const closingSocket = { readyState: WebSocket.CLOSING }; const closedSocket = { readyState: WebSocket.CLOSED }; // ACT & ASSERT: Only OPEN should be valid expect(discovery.validateConnection(openSocket)).toBe(true); expect(discovery.validateConnection(connectingSocket)).toBe(false); expect(discovery.validateConnection(closingSocket)).toBe(false); expect(discovery.validateConnection(closedSocket)).toBe(false); // Null/undefined should be invalid expect(discovery.validateConnection(null)).toBe(false); expect(discovery.validateConnection(undefined)).toBe(false); }); }); describe('Error Handling Integration', () => { it('should handle MCP server unavailable scenarios', async () => { // ARRANGE: Create discovery with real MCP client, but stop the server await mcpServer.stop(); const discovery = new WebSocketConnectionDiscovery(mcpClient); // ACT & ASSERT: Should handle server unavailable gracefully await expect(discovery.discoverWebSocketUrl('any-session')) .rejects.toThrow(); // CLEANUP: Restart server for other tests mcpServer = new MCPSSHServer({}, sshManager); mcpServer.setWebServerPort(webServerPort); mcpClient = { callTool: async (toolName: string, args: unknown) => { return await mcpServer.callTool(toolName, args); } }; }); it('should handle malformed MCP responses', async () => { // ARRANGE: Use real MCP client but try to access a session that returns invalid data // This test simulates a real scenario where MCP server might have incomplete responses const discovery = new WebSocketConnectionDiscovery(mcpClient); // ACT & ASSERT: Should handle malformed responses gracefully // Try to get monitoring URL for a session that doesn't exist or returns invalid data await expect(discovery.discoverWebSocketUrl('malformed-session-test')) .rejects.toThrow(); }); it('should handle URL parsing edge cases', () => { // ARRANGE const discovery = new WebSocketConnectionDiscovery(mcpClient); const invalidUrls = [ 'not-a-url-at-all', 'ftp://invalid.com/session/test', 'http://localhost/invalid-path', 'http://localhost/session', // Missing session name '' ]; // ACT & ASSERT: All should throw appropriate errors for (const invalidUrl of invalidUrls) { expect(() => discovery.parseMonitoringUrl(invalidUrl)).toThrow(); } }); }); });

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