/**
* 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();
}
});
});
});