import { describe, expect, it, beforeEach, afterEach, spyOn } from 'bun:test';
import server from '../src/index';
import { VyOSClient } from '../src/vyos-client';
/**
* @fileoverview End-to-end integration tests for the VyOS MCP server.
*
* Tests complete workflows including:
* - VyOSClient integration with mocked VyOS API
* - MCP server functionality
* - Network configuration scenarios (mocked)
* - Error recovery and resilience
* - Performance and reliability
*
* @author VyOS MCP Server Tests
* @version 1.0.0
* @since 2025-01-13
*/
describe('VyOS MCP Server Integration Tests', () => {
let fetchSpy: ReturnType<typeof spyOn>;
let vyosClient: VyOSClient;
beforeEach(() => {
fetchSpy = spyOn(global, 'fetch');
// Create VyOSClient instance for testing
vyosClient = new VyOSClient({
host: 'https://vyos.test.local',
apiKey: 'test-api-key',
timeout: 5000,
verifySSL: false,
});
});
afterEach(() => {
fetchSpy.mockRestore();
});
describe('MCP Server Integration', () => {
it('should provide working SSE endpoint', async () => {
// Test SSE endpoint
const sseResponse = await server.request('/sse', {
method: 'GET',
headers: { 'Accept': 'text/event-stream' },
});
expect(sseResponse.status).toBe(200);
expect(sseResponse.headers.get('Content-Type')).toContain('text/event-stream');
// Test that messages endpoint requires proper setup
const msgResponse = await server.request('/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'ping',
}),
});
// Should return 500 since transport isn't properly initialized in test environment
expect(msgResponse.status).toBe(500);
});
it('should handle CORS for cross-origin MCP clients', async () => {
const response = await server.request('/sse', {
method: 'GET',
headers: {
'Origin': 'https://claude.ai',
'Accept': 'text/event-stream',
},
});
expect(response.headers.get('Access-Control-Allow-Origin')).toBeTruthy();
});
});
describe('VyOSClient Integration Tests', () => {
describe('Authentication and Connection', () => {
it('should handle successful VyOS authentication', async () => {
const mockResponse = {
success: true,
data: { authenticated: true },
};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
const result = await vyosClient.getSystemInfo();
expect(result).toEqual(mockResponse);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/info',
expect.objectContaining({
method: 'POST',
})
);
});
it('should handle VyOS authentication failures', async () => {
fetchSpy.mockResolvedValue(new Response('Unauthorized', {
status: 401,
statusText: 'Unauthorized',
}));
await expect(vyosClient.getSystemInfo()).rejects.toThrow('VyOS API request failed: HTTP 401: Unauthorized');
});
it('should handle network connectivity issues', async () => {
fetchSpy.mockRejectedValue(new Error('Network error: Connection refused'));
await expect(vyosClient.getSystemInfo()).rejects.toThrow('VyOS API request failed: Network error: Connection refused');
});
});
describe('Configuration Management Workflow', () => {
it('should complete interface configuration workflow', async () => {
// Mock successful responses for the workflow - create new Response for each call
fetchSpy.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({ success: true }))));
// Step 1: Set interface configuration
await vyosClient.setConfig(['interfaces', 'ethernet', 'eth0', 'address'], '192.168.1.1/24');
// Step 2: Set interface description
await vyosClient.setConfig(['interfaces', 'ethernet', 'eth0', 'description'], 'LAN Interface');
// Step 3: Commit configuration
await vyosClient.commit('Configure LAN interface');
// Step 4: Save configuration
await vyosClient.save();
// Verify all API calls were made
expect(fetchSpy).toHaveBeenCalledTimes(4);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/configure',
expect.objectContaining({ method: 'POST' })
);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.objectContaining({ method: 'POST' })
);
});
it('should handle configuration rollback scenario', async () => {
// Mock successful responses - create new Response for each call
fetchSpy.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({ success: true }))));
// Make risky configuration change
await vyosClient.setConfig(['interfaces', 'ethernet', 'eth0', 'address'], '10.0.1.1/24');
// Commit with confirm timeout for safety
await vyosClient.commit('Risky IP change - needs confirmation', 5);
// In real scenario, if connectivity fails, commit would auto-rollback
// Here we simulate manual rollback
await vyosClient.setConfig(['interfaces', 'ethernet', 'eth0', 'address'], '192.168.1.1/24');
await vyosClient.commit('Rollback to working configuration');
expect(fetchSpy).toHaveBeenCalledTimes(4);
});
});
describe('Network Diagnostics Integration', () => {
it('should perform comprehensive network diagnostics', async () => {
// Mock ping result
const mockPingResult = {
packets_transmitted: 3,
packets_received: 3,
packet_loss: 0,
rtt_avg: 1.5,
};
// Mock traceroute result
const mockTraceResult = {
hops: [
{ hop: 1, address: '192.168.1.1', rtt: [1.2, 1.1, 1.3] },
{ hop: 2, address: '8.8.8.8', rtt: [8.1, 8.3, 8.0] },
],
};
fetchSpy
.mockResolvedValueOnce(new Response(JSON.stringify(mockPingResult)))
.mockResolvedValueOnce(new Response(JSON.stringify(mockTraceResult)));
// Test connectivity
const pingResult = await vyosClient.ping('8.8.8.8', { count: 3 });
expect(pingResult).toEqual(mockPingResult);
// Trace network path
const traceResult = await vyosClient.traceroute('8.8.8.8', { maxHops: 10 });
expect(traceResult).toEqual(mockTraceResult);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('should handle network diagnostic failures gracefully', async () => {
// Mock failed ping
const mockFailedPing = {
packets_transmitted: 3,
packets_received: 0,
packet_loss: 100,
};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockFailedPing)));
const result = await vyosClient.ping('192.168.99.1', { count: 3 });
expect(result).toEqual(mockFailedPing);
expect(result).toHaveProperty('packet_loss', 100);
});
});
describe('System Management Integration', () => {
it('should retrieve system information and status', async () => {
const mockSystemInfo = {
version: '1.4-rolling-202501130317',
hostname: 'vyos-router',
uptime: '1 day, 2 hours, 30 minutes',
load_average: [0.15, 0.10, 0.08],
};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockSystemInfo)));
const info = await vyosClient.getSystemInfo();
expect(info).toEqual(mockSystemInfo);
expect(info).toHaveProperty('version');
expect(info).toHaveProperty('hostname');
});
it('should handle system management operations', async () => {
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ success: true })));
// Test reboot command (would be dangerous in real scenario)
await vyosClient.reboot();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/reboot',
expect.objectContaining({ method: 'POST' })
);
});
});
describe('Monitoring and Statistics Integration', () => {
it('should retrieve interface statistics', async () => {
const mockInterfaceStats = {
eth0: {
rx_bytes: 1000000,
tx_bytes: 500000,
rx_packets: 1000,
tx_packets: 800,
rx_errors: 0,
tx_errors: 0,
},
};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockInterfaceStats)));
const stats = await vyosClient.getInterfaceStatistics();
expect(stats).toEqual(mockInterfaceStats);
expect(stats.eth0).toHaveProperty('rx_bytes');
expect(stats.eth0.rx_bytes).toBeGreaterThan(0);
});
it('should retrieve routing information', async () => {
const mockRoutes = [
{
destination: '0.0.0.0/0',
gateway: '192.168.1.1',
interface: 'eth0',
metric: 1,
},
{
destination: '192.168.1.0/24',
gateway: '0.0.0.0',
interface: 'eth0',
metric: 0,
},
];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockRoutes)));
const routes = await vyosClient.getRoutes();
expect(routes).toEqual(mockRoutes);
expect(Array.isArray(routes)).toBe(true);
expect(routes.length).toBeGreaterThan(0);
});
});
});
describe('Error Recovery Scenarios', () => {
it('should handle VyOS API timeouts gracefully', async () => {
fetchSpy.mockRejectedValue(new DOMException('The operation was aborted.', 'AbortError'));
await expect(vyosClient.getSystemInfo()).rejects.toThrow('VyOS API request failed');
});
it('should handle invalid VyOS API responses', async () => {
fetchSpy.mockResolvedValue(new Response('Internal Server Error', {
status: 500,
statusText: 'Internal Server Error',
}));
await expect(vyosClient.getSystemInfo()).rejects.toThrow('VyOS API request failed: HTTP 500: Internal Server Error');
});
it('should handle VyOS API errors in response', async () => {
const errorResponse = { error: 'Invalid command syntax' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(errorResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
await expect(vyosClient.getSystemInfo()).rejects.toThrow('VyOS API request failed: VyOS API Error: Invalid command syntax');
});
});
describe('Performance and Reliability', () => {
it('should handle multiple concurrent VyOS API requests', async () => {
const mockResponse = { success: true, data: {} };
fetchSpy.mockImplementation(() => Promise.resolve(new Response(JSON.stringify(mockResponse))));
// Create multiple concurrent requests
const requests = [
vyosClient.getSystemInfo(),
vyosClient.getInterfaces(),
vyosClient.getRoutes(),
vyosClient.showConfig(['system']),
];
const responses = await Promise.all(requests);
// All requests should succeed
for (const response of responses) {
expect(response).toEqual(mockResponse);
}
// Should have made 4 concurrent fetch calls
expect(fetchSpy).toHaveBeenCalledTimes(4);
});
it('should handle large configuration responses', async () => {
// Generate large mock configuration
const largeConfig = {
interfaces: {},
protocols: {},
system: {},
};
// Generate 50 interfaces for large response test
for (let i = 0; i < 50; i++) {
largeConfig.interfaces[`eth${i}`] = {
address: `192.168.${Math.floor(i / 254)}.${i % 254 + 1}/24`,
description: `Interface ${i}`,
mtu: 1500,
};
}
fetchSpy.mockResolvedValue(new Response(JSON.stringify(largeConfig)));
const result = await vyosClient.showConfig();
expect(result).toEqual(largeConfig);
expect(Object.keys(result.interfaces)).toHaveLength(50);
});
});
});