import { describe, expect, it, beforeEach, afterEach, spyOn } from 'bun:test';
import { VyOSClient } from '../src/vyos-client';
import type { VyOSAuth } from '../src/schemas/vyos-schemas';
/**
* @fileoverview Comprehensive test suite for VyOSClient using Bun test runner.
*
* Tests all VyOS API client functionality including:
* - Authentication and connection
* - Configuration management
* - Operational commands
* - System management
* - Network diagnostics
* - Error handling
*
* @author VyOS MCP Server Tests
* @version 1.0.0
* @since 2025-01-13
*/
describe('VyOSClient', () => {
let client: VyOSClient;
let mockAuth: VyOSAuth;
let fetchSpy: ReturnType<typeof spyOn>;
beforeEach(() => {
mockAuth = {
host: 'https://vyos.test.local',
apiKey: 'test-api-key',
timeout: 5000,
verifySSL: false,
};
client = new VyOSClient(mockAuth);
// Mock global fetch
fetchSpy = spyOn(global, 'fetch');
});
afterEach(() => {
fetchSpy.mockRestore();
});
describe('constructor', () => {
it('should create VyOSClient instance with correct properties', () => {
expect(client).toBeInstanceOf(VyOSClient);
});
it('should use default values for optional parameters', () => {
const minimalAuth = {
host: 'https://vyos.minimal.test',
apiKey: 'minimal-key',
};
const minimalClient = new VyOSClient(minimalAuth);
expect(minimalClient).toBeInstanceOf(VyOSClient);
});
});
describe('makeRequest', () => {
it('should make successful API request', async () => {
const mockResponse = { success: true, data: { version: '1.4' } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
const result = await client.getSystemInfo();
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/info',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
})
);
expect(result).toEqual(mockResponse);
});
it('should handle HTTP errors', async () => {
fetchSpy.mockResolvedValue(new Response('Not Found', {
status: 404,
statusText: 'Not Found',
}));
await expect(client.getSystemInfo()).rejects.toThrow('VyOS API request failed: HTTP 404: Not Found');
});
it('should handle VyOS API errors', async () => {
const errorResponse = { error: 'Invalid API key' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(errorResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
await expect(client.getSystemInfo()).rejects.toThrow('VyOS API request failed: VyOS API Error: Invalid API key');
});
it('should handle network errors', async () => {
fetchSpy.mockRejectedValue(new Error('Network error'));
await expect(client.getSystemInfo()).rejects.toThrow('VyOS API request failed: Network error');
});
it('should handle timeout', async () => {
fetchSpy.mockRejectedValue(new DOMException('The operation was aborted.', 'AbortError'));
await expect(client.getSystemInfo()).rejects.toThrow('VyOS API request failed');
});
});
describe('system information', () => {
it('should get system info', async () => {
const mockSystemInfo = {
version: '1.4-rolling-202501130317',
hostname: 'vyos-router',
uptime: '1 day, 2 hours',
};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockSystemInfo)));
const result = await client.getSystemInfo();
expect(result).toEqual(mockSystemInfo);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/info',
expect.objectContaining({
method: 'POST',
})
);
});
it('should get version', async () => {
const mockVersion = { version: '1.4-rolling' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockVersion)));
await client.getVersion();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get uptime', async () => {
const mockUptime = { uptime: '1 day, 2:30:45' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockUptime)));
await client.getUptime();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
});
describe('configuration management', () => {
it('should show configuration', async () => {
const mockConfig = { interfaces: { ethernet: { eth0: { address: '192.168.1.1/24' } } } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockConfig)));
const result = await client.showConfig(['interfaces', 'ethernet', 'eth0']);
expect(result).toEqual(mockConfig);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/retrieve',
expect.objectContaining({
method: 'POST',
})
);
});
it('should show full configuration when no path provided', async () => {
const mockConfig = { system: {}, interfaces: {} };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockConfig)));
await client.showConfig();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/retrieve',
expect.any(Object)
);
});
it('should set configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.setConfig(['interfaces', 'ethernet', 'eth0', 'address'], '192.168.1.1/24');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/configure',
expect.objectContaining({
method: 'POST',
})
);
});
it('should set valueless configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.setConfig(['service', 'ssh']);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/configure',
expect.any(Object)
);
});
it('should delete configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.deleteConfig(['interfaces', 'ethernet', 'eth1']);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/configure',
expect.objectContaining({
method: 'POST',
})
);
});
it('should check if configuration exists', async () => {
const mockResponse = { exists: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
const exists = await client.configExists(['protocols', 'bgp']);
expect(exists).toBe(true);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/retrieve',
expect.any(Object)
);
});
it('should return configuration values', async () => {
const mockResponse = { values: ['eth0', 'eth1', 'eth2'] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
const values = await client.returnValues(['interfaces', 'ethernet']);
expect(values).toEqual(['eth0', 'eth1', 'eth2']);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/retrieve',
expect.any(Object)
);
});
it('should handle empty values response', async () => {
const mockResponse = {};
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
const values = await client.returnValues(['nonexistent']);
expect(values).toEqual([]);
});
it('should commit configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.commit('Test commit', 5);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.any(Object)
);
});
it('should commit without comment and timeout', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.commit();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.any(Object)
);
});
it('should save configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.save();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.objectContaining({
method: 'POST',
})
);
});
it('should load configuration', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.load('/config/backup.boot');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.any(Object)
);
});
it('should load default configuration file', async () => {
const mockResponse = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResponse)));
await client.load();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/config-file',
expect.any(Object)
);
});
});
describe('operational commands', () => {
it('should execute show command', async () => {
const mockResult = { interfaces: { eth0: { statistics: { rx_bytes: 1000 } } } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
const result = await client.show(['interfaces', 'ethernet', 'statistics']);
expect(result).toEqual(mockResult);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should execute reset command', async () => {
const mockResult = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.reset(['interfaces', 'ethernet', 'eth0', 'counters']);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/reset',
expect.any(Object)
);
});
it('should execute generate command', async () => {
const mockResult = { privateKey: 'wg-private-key', publicKey: 'wg-public-key' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
const result = await client.generate(['wireguard', 'keypair']);
expect(result).toEqual(mockResult);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/generate',
expect.any(Object)
);
});
});
describe('system management', () => {
it('should reboot system', async () => {
const mockResult = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.reboot();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/reboot',
expect.objectContaining({
method: 'POST',
})
);
});
it('should poweroff system', async () => {
const mockResult = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.poweroff();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/poweroff',
expect.any(Object)
);
});
it('should add system image', async () => {
const mockResult = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.addImage('vyos-1.4-rolling');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/image',
expect.any(Object)
);
});
it('should delete system image', async () => {
const mockResult = { success: true };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.deleteImage('old-vyos-image');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/image',
expect.any(Object)
);
});
});
describe('network diagnostics', () => {
it('should execute ping with all options', async () => {
const mockResult = { packets_transmitted: 3, packets_received: 3, packet_loss: 0 };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
const options = {
count: 3,
interval: 1,
timeout: 5,
size: 64,
source: '192.168.1.1',
};
await client.ping('8.8.8.8', options);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should execute ping with minimal options', async () => {
const mockResult = { packets_transmitted: 1, packets_received: 1 };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.ping('8.8.8.8');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should execute traceroute with options', async () => {
const mockResult = { hops: [{ hop: 1, address: '192.168.1.1' }] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
const options = {
maxHops: 15,
timeout: 3,
source: '192.168.1.100',
};
await client.traceroute('8.8.8.8', options);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should execute traceroute with minimal options', async () => {
const mockResult = { hops: [] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResult)));
await client.traceroute('1.1.1.1');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
});
describe('monitoring and statistics', () => {
it('should get interfaces', async () => {
const mockInterfaces = { ethernet: { eth0: { state: 'up' } } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockInterfaces)));
await client.getInterfaces();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get interface statistics for specific interface', async () => {
const mockStats = { rx_bytes: 1000, tx_bytes: 500 };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockStats)));
await client.getInterfaceStatistics('eth0');
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get all interface statistics', async () => {
const mockStats = { eth0: { rx_bytes: 1000 }, eth1: { rx_bytes: 2000 } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockStats)));
await client.getInterfaceStatistics();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get routes', async () => {
const mockRoutes = [{ destination: '0.0.0.0/0', gateway: '192.168.1.1' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockRoutes)));
await client.getRoutes();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get BGP summary', async () => {
const mockBGP = { router_id: '1.1.1.1', as_number: 65001 };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockBGP)));
await client.getBGPSummary();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get BGP neighbors', async () => {
const mockNeighbors = [{ address: '192.168.1.2', state: 'Established' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockNeighbors)));
await client.getBGPNeighbors();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get OSPF neighbors', async () => {
const mockOSPF = [{ neighbor: '192.168.1.3', state: 'Full' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockOSPF)));
await client.getOSPFNeighbors();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get VPN status', async () => {
const mockVPN = [{ tunnel: 'peer-192.168.2.1', state: 'up' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockVPN)));
await client.getVPNStatus();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get firewall rules', async () => {
const mockFirewall = { name: { LAN_LOCAL: { rule: { 10: { action: 'accept' } } } } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockFirewall)));
await client.getFirewallRules();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get NAT rules', async () => {
const mockNAT = { source: { rule: { 100: { outbound_interface: 'eth0' } } } };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockNAT)));
await client.getNATRules();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get DHCP leases', async () => {
const mockLeases = [{ ip: '192.168.1.100', mac: '00:11:22:33:44:55' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockLeases)));
await client.getDHCPLeases();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get connection tracking', async () => {
const mockConntrack = [{ protocol: 'tcp', src: '192.168.1.100', dst: '8.8.8.8' }];
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockConntrack)));
await client.getConntrack();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get log with default lines', async () => {
const mockLog = { lines: ['Jan 13 12:00:00 vyos sshd: Connection established'] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockLog)));
await client.getLog();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get log with custom line count', async () => {
const mockLog = { lines: [] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockLog)));
await client.getLog(50);
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get system resources', async () => {
const mockResources = { cpu_usage: 15, load_average: [0.5, 0.4, 0.3] };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockResources)));
await client.getSystemResources();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get system memory', async () => {
const mockMemory = { total: 2048, used: 512, free: 1536 };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockMemory)));
await client.getSystemMemory();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
it('should get system storage', async () => {
const mockStorage = { filesystem: '/', size: '10G', used: '2.5G', available: '7.5G' };
fetchSpy.mockResolvedValue(new Response(JSON.stringify(mockStorage)));
await client.getSystemStorage();
expect(fetchSpy).toHaveBeenCalledWith(
'https://vyos.test.local/show',
expect.any(Object)
);
});
});
});