import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
FirewallaAPI,
FirewallaMCPServer,
FirewallaConfig,
FlowRecord,
Device,
ENV_FIREWALLA_URL,
ENV_FIREWALLA_TOKEN,
ENV_FIREWALLA_ID,
ENV_LOG_LEVEL,
FirewallaAPIError,
FirewallaAuthError,
FirewallaNetworkError,
FirewallaRateLimitError,
logger,
} from './server.js';
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Mock stderr.write to capture log output
const mockStderrWrite = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
describe('FirewallaAPI', () => {
const testConfig: FirewallaConfig = {
baseUrl: 'https://my.firewalla.com',
bearerToken: 'test-token',
firewallId: 'test-device-id',
};
beforeEach(() => {
mockFetch.mockReset();
mockStderrWrite.mockClear();
});
describe('constructor', () => {
it('should create an instance with valid config', () => {
const api = new FirewallaAPI(testConfig);
expect(api).toBeInstanceOf(FirewallaAPI);
});
});
describe('queryFlows', () => {
it('should make POST request to /v1/flows/query', async () => {
const mockFlows: FlowRecord[] = [
{
ts: 1234567890,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 443,
devicePort: 54321,
ip: '1.2.3.4',
deviceIP: '192.168.1.100',
device: 'AA:BB:CC:DD:EE:FF',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const api = new FirewallaAPI(testConfig);
const result = await api.queryFlows({
start: 1234567800,
end: 1234567900,
});
expect(mockFetch).toHaveBeenCalledWith(
'https://my.firewalla.com/v1/flows/query',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json; charset=utf-8',
Authorization: 'Bearer test-token',
'X-Firewalla-ID': 'test-device-id',
}),
})
);
expect(result).toEqual(mockFlows);
});
it('should throw FirewallaAuthError on 401', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
});
const api = new FirewallaAPI(testConfig);
await expect(api.queryFlows({ start: 1234567800, end: 1234567900 })).rejects.toThrow(
FirewallaAuthError
);
});
it('should throw FirewallaAuthError on 403', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 403,
statusText: 'Forbidden',
});
const api = new FirewallaAPI(testConfig);
await expect(api.queryFlows({ start: 1234567800, end: 1234567900 })).rejects.toThrow(
FirewallaAuthError
);
});
});
describe('listDevices', () => {
it('should make GET request to /v1/device/list', async () => {
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const api = new FirewallaAPI(testConfig);
const result = await api.listDevices();
expect(mockFetch).toHaveBeenCalledWith(
'https://my.firewalla.com/v1/device/list',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
})
);
expect(result).toEqual(mockDevices);
});
});
describe('getDevice', () => {
it('should find device by MAC address', async () => {
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const api = new FirewallaAPI(testConfig);
const result = await api.getDevice('AA:BB:CC:DD:EE:FF');
expect(result.mac).toBe('AA:BB:CC:DD:EE:FF');
expect(result.name).toBe('iPhone');
});
it('should throw error if device not found', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const api = new FirewallaAPI(testConfig);
await expect(api.getDevice('XX:XX:XX:XX:XX:XX')).rejects.toThrow(
'Device with MAC XX:XX:XX:XX:XX:XX not found'
);
});
});
describe('error recovery', () => {
it('should retry on 500 server error and succeed', async () => {
// First call fails with 500, second succeeds
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
})
.mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 2 });
const result = await api.listDevices();
expect(result).toEqual([]);
expect(mockFetch).toHaveBeenCalledTimes(2);
// Verify structured log was written
expect(mockStderrWrite).toHaveBeenCalled();
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.level).toBe('warn');
expect(logEntry.message).toContain('retrying');
});
it('should retry on 503 and eventually fail after max retries', async () => {
// All calls fail with 503
mockFetch.mockResolvedValue({
ok: false,
status: 503,
statusText: 'Service Unavailable',
});
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 2 });
await expect(api.listDevices()).rejects.toThrow(FirewallaAPIError);
expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries
});
it('should not retry on 400 client error', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
statusText: 'Bad Request',
});
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 3 });
await expect(api.listDevices()).rejects.toThrow(FirewallaAPIError);
expect(mockFetch).toHaveBeenCalledTimes(1); // No retries
});
it('should not retry on authentication errors', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
});
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 3 });
await expect(api.listDevices()).rejects.toThrow(FirewallaAuthError);
expect(mockFetch).toHaveBeenCalledTimes(1); // No retries for auth errors
});
it('should handle rate limiting with Retry-After header', async () => {
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 2 });
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 429,
statusText: 'Too Many Requests',
headers: { get: () => '1' },
})
.mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const result = await api.listDevices();
expect(result).toEqual([]);
// Verify rate limit warning was logged
expect(mockStderrWrite).toHaveBeenCalled();
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.level).toBe('warn');
expect(logEntry.message).toContain('Rate limited');
});
it('should handle network errors and retry', async () => {
// First call throws network error, second succeeds
mockFetch.mockRejectedValueOnce(new TypeError('Failed to fetch')).mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 2 });
const result = await api.listDevices();
expect(result).toEqual([]);
expect(mockFetch).toHaveBeenCalledTimes(2);
// Verify network error was logged
expect(mockStderrWrite).toHaveBeenCalled();
});
it('should throw FirewallaNetworkError after all network retries fail', async () => {
mockFetch.mockRejectedValue(new TypeError('Failed to fetch'));
const api = new FirewallaAPI(testConfig, { baseDelayMs: 10, maxRetries: 2 });
await expect(api.listDevices()).rejects.toThrow(FirewallaNetworkError);
expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries
});
it('should accept custom retry configuration', () => {
const api = new FirewallaAPI(testConfig, {
maxRetries: 5,
baseDelayMs: 500,
maxDelayMs: 5000,
timeoutMs: 60000,
});
expect(api).toBeInstanceOf(FirewallaAPI);
});
});
});
describe('Error classes', () => {
describe('FirewallaAPIError', () => {
it('should create error with all properties', () => {
const error = new FirewallaAPIError('Test error', 500, '/test', true);
expect(error.message).toBe('Test error');
expect(error.statusCode).toBe(500);
expect(error.endpoint).toBe('/test');
expect(error.retryable).toBe(true);
expect(error.name).toBe('FirewallaAPIError');
});
it('should default retryable to false', () => {
const error = new FirewallaAPIError('Test error');
expect(error.retryable).toBe(false);
});
});
describe('FirewallaAuthError', () => {
it('should set correct properties', () => {
const error = new FirewallaAuthError('Auth failed', '/login');
expect(error.statusCode).toBe(401);
expect(error.retryable).toBe(false);
expect(error.name).toBe('FirewallaAuthError');
});
});
describe('FirewallaNetworkError', () => {
it('should be retryable', () => {
const error = new FirewallaNetworkError('Network failed', '/api');
expect(error.retryable).toBe(true);
expect(error.name).toBe('FirewallaNetworkError');
});
});
describe('FirewallaRateLimitError', () => {
it('should include retry-after value', () => {
const error = new FirewallaRateLimitError('Rate limited', 60, '/api');
expect(error.statusCode).toBe(429);
expect(error.retryAfter).toBe(60);
expect(error.retryable).toBe(true);
expect(error.name).toBe('FirewallaRateLimitError');
});
});
});
describe('FirewallaMCPServer', () => {
const originalEnv = process.env;
beforeEach(() => {
// Reset environment variables
process.env = { ...originalEnv };
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
mockFetch.mockReset();
mockStderrWrite.mockClear();
});
afterEach(() => {
process.env = originalEnv;
});
describe('constructor', () => {
it('should create an instance without env vars', () => {
const server = new FirewallaMCPServer();
expect(server).toBeInstanceOf(FirewallaMCPServer);
});
it('should auto-configure from environment variables', () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-device-id';
const server = new FirewallaMCPServer();
expect(server).toBeInstanceOf(FirewallaMCPServer);
// Verify that config message was logged via structured logger
expect(mockStderrWrite).toHaveBeenCalled();
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.level).toBe('info');
expect(logEntry.message).toContain('configured from environment variables');
});
it('should warn about partial configuration', () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
// Missing TOKEN and ID
new FirewallaMCPServer();
// Verify warning was logged
expect(mockStderrWrite).toHaveBeenCalled();
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.level).toBe('warn');
expect(logEntry.message).toContain('Partial Firewalla config');
});
});
});
describe('Logger', () => {
beforeEach(() => {
mockStderrWrite.mockClear();
});
it('should output structured JSON logs', () => {
logger.info('Test message', { key: 'value' });
expect(mockStderrWrite).toHaveBeenCalled();
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.service).toBe('firewalla-mcp');
expect(logEntry.level).toBe('info');
expect(logEntry.message).toBe('Test message');
expect(logEntry.context).toEqual({ key: 'value' });
expect(logEntry.timestamp).toBeDefined();
});
it('should support different log levels', () => {
logger.setLevel('debug');
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warn message');
logger.error('Error message');
expect(mockStderrWrite).toHaveBeenCalledTimes(4);
// Reset to default
logger.setLevel('info');
});
it('should filter logs below current level', () => {
logger.setLevel('warn');
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warn message');
logger.error('Error message');
expect(mockStderrWrite).toHaveBeenCalledTimes(2); // Only warn and error
// Reset to default
logger.setLevel('info');
});
it('should get current log level', () => {
const level = logger.getLevel();
expect(['debug', 'info', 'warn', 'error']).toContain(level);
});
it('should omit context when empty', () => {
logger.info('Test message');
const logOutput = mockStderrWrite.mock.calls[0][0] as string;
const logEntry = JSON.parse(logOutput);
expect(logEntry.context).toBeUndefined();
});
});
describe('Environment variable constants', () => {
it('should export correct environment variable names', () => {
expect(ENV_FIREWALLA_URL).toBe('FIREWALLA_URL');
expect(ENV_FIREWALLA_TOKEN).toBe('FIREWALLA_TOKEN');
expect(ENV_FIREWALLA_ID).toBe('FIREWALLA_ID');
expect(ENV_LOG_LEVEL).toBe('LOG_LEVEL');
});
});
describe('Type exports', () => {
it('should export FirewallaConfig interface', () => {
const config: FirewallaConfig = {
baseUrl: 'https://example.com',
bearerToken: 'token',
firewallId: 'id',
};
expect(config.baseUrl).toBe('https://example.com');
});
it('should export FlowRecord interface', () => {
const flow: FlowRecord = {
ts: 1234567890,
fd: 'in',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 443,
devicePort: 12345,
ip: '1.2.3.4',
deviceIP: '192.168.1.1',
device: 'AA:BB:CC:DD:EE:FF',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
};
expect(flow.protocol).toBe('tcp');
});
it('should export Device interface', () => {
const device: Device = {
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
};
expect(device.online).toBe(true);
});
});
describe('FirewallaMCPServer tool handlers', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
mockFetch.mockReset();
mockStderrWrite.mockClear();
});
afterEach(() => {
process.env = originalEnv;
});
describe('configureFirewalla', () => {
it('should configure API and return success message', async () => {
const server = new FirewallaMCPServer();
const result = await (server as any).configureFirewalla({
baseUrl: 'https://my.firewalla.com',
bearerToken: 'test-token',
firewallId: 'test-id',
});
expect(result.content[0].text).toContain('configured successfully');
});
it('should indicate when overriding env config', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'env-token';
process.env[ENV_FIREWALLA_ID] = 'env-id';
const server = new FirewallaMCPServer();
const result = await (server as any).configureFirewalla({
baseUrl: 'https://my.firewalla.com',
bearerToken: 'new-token',
firewallId: 'new-id',
});
expect(result.content[0].text).toContain('reconfigured');
expect(result.content[0].text).toContain('overriding');
});
});
describe('getConfigStatus', () => {
it('should return not configured status when no config', async () => {
const server = new FirewallaMCPServer();
const result = await (server as any).getConfigStatus();
expect(result.content[0].text).toContain('not configured');
});
it('should return configured status with env source', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const server = new FirewallaMCPServer();
const result = await (server as any).getConfigStatus();
expect(result.content[0].text).toContain('configured');
expect(result.content[0].text).toContain('environment variables');
});
it('should show environment variable status', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
const server = new FirewallaMCPServer();
const result = await (server as any).getConfigStatus();
const statusJson = JSON.parse(result.content[1].text);
expect(statusJson.environmentVariables.FIREWALLA_URL).toBe('set');
expect(statusJson.environmentVariables.FIREWALLA_TOKEN).toBe('not set');
});
});
describe('queryNetworkFlows', () => {
it('should throw error when API not configured', async () => {
const server = new FirewallaMCPServer();
await expect(
(server as any).queryNetworkFlows({
startTime: 1234567800,
endTime: 1234567900,
})
).rejects.toThrow('not configured');
});
it('should return flows when configured', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const mockFlows: FlowRecord[] = [
{
ts: 1234567890,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 443,
devicePort: 54321,
ip: '1.2.3.4',
deviceIP: '192.168.1.100',
device: 'AA:BB:CC:DD:EE:FF',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).queryNetworkFlows({
startTime: 1234567800,
endTime: 1234567900,
});
expect(result.content[0].text).toContain('Found 1 network flows');
});
});
describe('listDevices', () => {
it('should throw error when API not configured', async () => {
const server = new FirewallaMCPServer();
await expect((server as any).listDevices()).rejects.toThrow('not configured');
});
it('should return device list when configured', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const server = new FirewallaMCPServer();
const result = await (server as any).listDevices();
expect(result.content[0].text).toContain('Found 1 devices');
});
});
describe('getDeviceDetails', () => {
it('should throw error when API not configured', async () => {
const server = new FirewallaMCPServer();
await expect((server as any).getDeviceDetails({ mac: 'AA:BB:CC:DD:EE:FF' })).rejects.toThrow(
'not configured'
);
});
it('should return device details when found', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getDeviceDetails({ mac: 'AA:BB:CC:DD:EE:FF' });
expect(result.content[0].text).toContain('Device details for iPhone');
});
});
describe('searchDevices', () => {
it('should throw error when API not configured', async () => {
const server = new FirewallaMCPServer();
await expect((server as any).searchDevices({ query: 'iphone' })).rejects.toThrow(
'not configured'
);
});
it('should search devices by name', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
{
ip: '192.168.1.101',
mac: 'BB:CC:DD:EE:FF:AA',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Samsung',
bname: 'Galaxy',
names: ['Galaxy'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'Galaxy',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 500,
totalDownload: 1000,
gid: 'gid-1',
online: false,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const server = new FirewallaMCPServer();
const result = await (server as any).searchDevices({ query: 'iphone' });
expect(result.content[0].text).toContain('Found 1 device');
});
it('should filter by online status', async () => {
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
{
ip: '192.168.1.101',
mac: 'BB:CC:DD:EE:FF:AA',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPad',
names: ['iPad'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPad',
deviceType: 'tablet',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 500,
totalDownload: 1000,
gid: 'gid-1',
online: false,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
});
const server = new FirewallaMCPServer();
// Search for 'phone' which matches deviceType, filter to online only
const result = await (server as any).searchDevices({ query: 'phone', online: true });
expect(result.content[0].text).toContain('Found 1 device');
});
});
});
describe('Traffic analysis helper methods', () => {
let server: FirewallaMCPServer;
beforeEach(() => {
mockStderrWrite.mockClear();
server = new FirewallaMCPServer();
});
const sampleFlows: FlowRecord[] = [
{
ts: 1234567890,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 443,
devicePort: 54321,
ip: '1.2.3.4',
host: 'google.com',
deviceIP: '192.168.1.100',
device: 'AA:BB:CC:DD:EE:FF',
deviceName: 'iPhone',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 1000,
download: 5000,
},
{
ts: 1234567891,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'udp',
port: 53,
devicePort: 54322,
ip: '8.8.8.8',
host: 'dns.google',
deviceIP: '192.168.1.100',
device: 'AA:BB:CC:DD:EE:FF',
deviceName: 'iPhone',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 100,
download: 200,
},
{
ts: 1234567892,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 80,
devicePort: 54323,
ip: '5.6.7.8',
host: 'malware.com',
deviceIP: '192.168.1.101',
device: 'BB:CC:DD:EE:FF:AA',
deviceName: 'Galaxy',
country: 'RU',
tags: ['security'],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 500,
download: 1000,
blocked: true,
blockType: 'threat',
category: 'intel',
},
{
ts: 1234567893,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 22,
devicePort: 54324,
ip: '10.0.0.1',
deviceIP: '192.168.1.102',
device: 'CC:DD:EE:FF:AA:BB',
deviceName: 'Server',
country: 'CN',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 2000,
download: 3000,
},
];
describe('generateTrafficSummary', () => {
it('should calculate total traffic correctly', () => {
const result = (server as any).generateTrafficSummary(sampleFlows);
expect(result.totalFlows).toBe(4);
expect(result.totalUpload).toBe(3600); // 1000 + 100 + 500 + 2000
expect(result.totalDownload).toBe(9200); // 5000 + 200 + 1000 + 3000
expect(result.totalTraffic).toBe(12800);
});
it('should count unique devices', () => {
const result = (server as any).generateTrafficSummary(sampleFlows);
expect(result.uniqueDevices).toBe(3);
});
it('should count blocked flows', () => {
const result = (server as any).generateTrafficSummary(sampleFlows);
expect(result.blockedFlows).toBe(1);
});
it('should group flows by protocol', () => {
const result = (server as any).generateTrafficSummary(sampleFlows);
expect(result.protocolDistribution.tcp).toBe(3);
expect(result.protocolDistribution.udp).toBe(1);
});
});
describe('getTopTalkers', () => {
it('should rank devices by total traffic', () => {
const result = (server as any).getTopTalkers(sampleFlows);
expect(result[0].mac).toBe('AA:BB:CC:DD:EE:FF'); // 6300 bytes total
expect(result[0].total).toBe(6300);
});
it('should respect limit parameter', () => {
const result = (server as any).getTopTalkers(sampleFlows, 2);
expect(result.length).toBe(2);
});
it('should aggregate traffic per device', () => {
const result = (server as any).getTopTalkers(sampleFlows);
const iphone = result.find((d: any) => d.mac === 'AA:BB:CC:DD:EE:FF');
expect(iphone.upload).toBe(1100); // 1000 + 100
expect(iphone.download).toBe(5200); // 5000 + 200
});
});
describe('getBlockedTraffic', () => {
it('should count total blocked flows', () => {
const result = (server as any).getBlockedTraffic(sampleFlows);
expect(result.totalBlocked).toBe(1);
});
it('should group blocked by type', () => {
const result = (server as any).getBlockedTraffic(sampleFlows);
expect(result.byType.threat).toBe(1);
});
it('should list top blocked destinations', () => {
const result = (server as any).getBlockedTraffic(sampleFlows);
expect(result.topBlockedDestinations[0].destination).toBe('malware.com');
expect(result.topBlockedDestinations[0].count).toBe(1);
});
});
describe('getSecurityEvents', () => {
it('should count security events', () => {
const result = (server as any).getSecurityEvents(sampleFlows);
expect(result.totalEvents).toBeGreaterThanOrEqual(1);
});
it('should count blocked connections', () => {
const result = (server as any).getSecurityEvents(sampleFlows);
expect(result.blockedConnections).toBe(1);
});
it('should count threat intel events', () => {
const result = (server as any).getSecurityEvents(sampleFlows);
expect(result.threatIntelEvents).toBe(1);
});
it('should identify suspicious ports in security events', () => {
// Add a blocked flow on a suspicious port
const flowsWithSuspiciousPort: FlowRecord[] = [
...sampleFlows,
{
ts: 1234567894,
fd: 'in',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 22,
devicePort: 54325,
ip: '10.0.0.2',
deviceIP: '192.168.1.103',
device: 'DD:EE:FF:AA:BB:CC',
deviceName: 'Attacker',
country: 'CN',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 100,
download: 100,
blocked: true,
blockType: 'threat',
},
];
const result = (server as any).getSecurityEvents(flowsWithSuspiciousPort);
// Port 22 should now be detected as it's in a security event
expect(result.suspiciousPorts[22]).toBe(1);
});
});
describe('groupFlowsByProtocol', () => {
it('should count flows per protocol', () => {
const result = (server as any).groupFlowsByProtocol(sampleFlows);
expect(result.tcp).toBe(3);
expect(result.udp).toBe(1);
});
});
describe('groupFlowsByCountry', () => {
it('should count flows per country', () => {
const result = (server as any).groupFlowsByCountry(sampleFlows);
expect(result.US).toBe(2);
expect(result.RU).toBe(1);
expect(result.CN).toBe(1);
});
});
describe('getTopDestinations', () => {
it('should rank destinations by connection count', () => {
const result = (server as any).getTopDestinations(sampleFlows);
expect(result.length).toBeGreaterThan(0);
expect(result[0]).toHaveProperty('destination');
expect(result[0]).toHaveProperty('count');
expect(result[0]).toHaveProperty('upload');
expect(result[0]).toHaveProperty('download');
});
it('should respect limit parameter', () => {
const result = (server as any).getTopDestinations(sampleFlows, 2);
expect(result.length).toBeLessThanOrEqual(2);
});
});
describe('getSuspiciousPorts', () => {
it('should detect suspicious port activity', () => {
const result = (server as any).getSuspiciousPorts(sampleFlows);
expect(result[22]).toBe(1); // SSH port
});
it('should not count non-suspicious ports', () => {
const result = (server as any).getSuspiciousPorts(sampleFlows);
expect(result[443]).toBeUndefined();
expect(result[80]).toBeUndefined();
});
});
describe('groupDevicesByType', () => {
const devices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
{
ip: '192.168.1.101',
mac: 'BB:CC:DD:EE:FF:AA',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPad',
names: ['iPad'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPad',
deviceType: 'tablet',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 500,
totalDownload: 1000,
gid: 'gid-1',
online: true,
},
{
ip: '192.168.1.102',
mac: 'CC:DD:EE:FF:AA:BB',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone2',
names: ['iPhone2'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone2',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 500,
totalDownload: 1000,
gid: 'gid-1',
online: true,
},
];
it('should group devices by type', () => {
const result = (server as any).groupDevicesByType(devices);
expect(result.phone).toBe(2);
expect(result.tablet).toBe(1);
});
it('should handle unknown device types', () => {
const devicesWithUnknown = [
...devices,
{
ip: '192.168.1.103',
mac: 'DD:EE:FF:AA:BB:CC',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Unknown',
bname: 'Unknown',
names: ['Unknown'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'Unknown',
deviceType: undefined,
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 0,
totalDownload: 0,
gid: 'gid-1',
online: false,
},
];
const result = (server as any).groupDevicesByType(devicesWithUnknown);
expect(result.unknown).toBe(1);
});
});
});
describe('FirewallaMCPServer additional handlers', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
mockFetch.mockReset();
mockStderrWrite.mockClear();
});
afterEach(() => {
process.env = originalEnv;
});
describe('getNetworkOverview', () => {
it('should throw error when API not configured', async () => {
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
const server = new FirewallaMCPServer();
await expect((server as any).getNetworkOverview()).rejects.toThrow('not configured');
});
it('should return network overview when configured', async () => {
const mockNetworkTargets = {
devices: [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
],
networkGroups: [
{
meta: { type: 'lan', name: 'LAN', uuid: 'uuid-1' },
enabled: true,
ipv4: '192.168.1.0/24',
uuid: 'uuid-1',
name: 'LAN',
gid: 'gid-1',
devices: [],
},
],
deviceTags: [
{
uid: 'tag-1',
name: 'IoT Devices',
createTs: 1234567800,
policy: {},
gid: 'gid-1',
devices: [],
},
],
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockNetworkTargets,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getNetworkOverview();
expect(result.content[0].text).toContain('Network Overview');
const overview = JSON.parse(result.content[1].text);
expect(overview.summary.totalDevices).toBe(1);
expect(overview.summary.onlineDevices).toBe(1);
expect(overview.summary.totalNetworkGroups).toBe(1);
});
});
describe('getCloudRules', () => {
it('should throw error when API not configured', async () => {
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
const server = new FirewallaMCPServer();
await expect((server as any).getCloudRules()).rejects.toThrow('not configured');
});
it('should return cloud rules when configured', async () => {
const mockCloudRules = {
'rule-1': {
id: 'rule-1',
type: 'category',
name: 'Block Ads',
source: 'cloud',
scope: 'all',
beta: false,
disabled: false,
last_updated: 1234567890,
rules: [],
notes: 'Block advertising',
count: 5,
dnsmasq_only: true,
},
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockCloudRules,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getCloudRules();
expect(result.content[0].text).toContain('Found 1 cloud security rules');
});
});
describe('getTrafficTrends', () => {
it('should throw error when API not configured', async () => {
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
const server = new FirewallaMCPServer();
await expect((server as any).getTrafficTrends({ period: '24h' })).rejects.toThrow(
'not configured'
);
});
it('should return traffic trends for 24h', async () => {
const mockTrendData = {
upload: { '1234567800': 1000, '1234567900': 2000 },
download: { '1234567800': 5000, '1234567900': 6000 },
totalUpload: 3000,
totalDownload: 11000,
block: {
'1234567800': { percent: 5, total: 100, blocked: 5 },
'1234567900': { percent: 10, total: 200, blocked: 20 },
},
totalConn: 300,
totalIpB: 10,
totalDnsB: 15,
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTrendData,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getTrafficTrends({ period: '24h' });
expect(result.content[0].text).toContain('Traffic trends analysis for 24h');
const analysis = JSON.parse(result.content[1].text);
expect(analysis.summary.totalUpload).toBe(3000);
expect(analysis.summary.totalDownload).toBe(11000);
});
it('should default to 24h period when not specified', async () => {
const mockTrendData = {
upload: {},
download: {},
totalUpload: 0,
totalDownload: 0,
block: {},
totalConn: 0,
totalIpB: 0,
totalDnsB: 0,
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTrendData,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getTrafficTrends({});
expect(result.content[0].text).toContain('24h');
});
});
describe('getFirewallaStatus', () => {
it('should throw error when API not configured', async () => {
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
const server = new FirewallaMCPServer();
await expect((server as any).getFirewallaStatus()).rejects.toThrow('not configured');
});
it('should return Firewalla device status', async () => {
const mockBoxes = [
{
name: 'Firewalla Gold',
model: 'gold',
gid: 'gid-1',
eid: 'eid-1',
status: true,
activeTs: 1234567890,
syncTs: 1234567880,
lokiEnabled: true,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockBoxes,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getFirewallaStatus();
expect(result.content[0].text).toContain('Firewalla device status (1 device)');
const status = JSON.parse(result.content[1].text);
expect(status[0].name).toBe('Firewalla Gold');
expect(status[0].status).toBe('Online');
});
it('should handle multiple devices', async () => {
const mockBoxes = [
{
name: 'Firewalla Gold',
model: 'gold',
gid: 'gid-1',
eid: 'eid-1',
status: true,
activeTs: 1234567890,
syncTs: 1234567880,
lokiEnabled: true,
},
{
name: 'Firewalla Purple',
model: 'purple',
gid: 'gid-2',
eid: 'eid-2',
status: false,
activeTs: 1234567800,
syncTs: 1234567790,
lokiEnabled: false,
},
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockBoxes,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getFirewallaStatus();
expect(result.content[0].text).toContain('2 devices');
const status = JSON.parse(result.content[1].text);
expect(status[1].status).toBe('Offline');
});
});
describe('updateDeviceRules', () => {
it('should throw error when API not configured', async () => {
delete process.env[ENV_FIREWALLA_URL];
delete process.env[ENV_FIREWALLA_TOKEN];
delete process.env[ENV_FIREWALLA_ID];
const server = new FirewallaMCPServer();
await expect(
(server as any).updateDeviceRules({
deviceMac: 'AA:BB:CC:DD:EE:FF',
action: 'block',
target: 'youtube',
})
).rejects.toThrow('not configured');
});
it('should update device rules successfully', async () => {
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
const mockBoxes = [
{
name: 'Firewalla Gold',
model: 'gold',
gid: 'gid-1',
eid: 'eid-1',
status: true,
activeTs: 1234567890,
syncTs: 1234567880,
lokiEnabled: true,
},
];
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockBoxes,
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const server = new FirewallaMCPServer();
const result = await (server as any).updateDeviceRules({
deviceMac: 'AA:BB:CC:DD:EE:FF',
action: 'block',
target: 'youtube',
type: 'category',
});
expect(result.content[0].text).toContain('Successfully blocked youtube');
expect(result.content[0].text).toContain('iPhone');
});
it('should throw error when no Firewalla devices found', async () => {
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
})
.mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const server = new FirewallaMCPServer();
await expect(
(server as any).updateDeviceRules({
deviceMac: 'AA:BB:CC:DD:EE:FF',
action: 'allow',
target: 'netflix',
})
).rejects.toThrow('No Firewalla devices found');
});
});
});
describe('Traffic trend helper methods', () => {
let server: FirewallaMCPServer;
beforeEach(() => {
mockStderrWrite.mockClear();
server = new FirewallaMCPServer();
});
describe('findTrafficPeaks', () => {
it('should find peak upload and download times', () => {
const trendData = {
upload: { '1234567800': 1000, '1234567900': 5000, '1234568000': 2000 },
download: { '1234567800': 3000, '1234567900': 2000, '1234568000': 8000 },
totalUpload: 8000,
totalDownload: 13000,
block: {},
totalConn: 0,
totalIpB: 0,
totalDnsB: 0,
};
const result = (server as any).findTrafficPeaks(trendData);
expect(result.peakUpload.bytes).toBe(5000);
expect(result.peakDownload.bytes).toBe(8000);
});
it('should handle empty trend data', () => {
const trendData = {
upload: {},
download: {},
totalUpload: 0,
totalDownload: 0,
block: {},
totalConn: 0,
totalIpB: 0,
totalDnsB: 0,
};
const result = (server as any).findTrafficPeaks(trendData);
expect(result.peakUpload.bytes).toBe(0);
expect(result.peakDownload.bytes).toBe(0);
});
});
describe('calculateBlockingEfficiency', () => {
it('should calculate blocking efficiency correctly', () => {
const trendData = {
upload: {},
download: {},
totalUpload: 0,
totalDownload: 0,
block: {
'1234567800': { percent: 10, total: 100, blocked: 10 },
'1234567900': { percent: 20, total: 200, blocked: 40 },
},
totalConn: 300,
totalIpB: 0,
totalDnsB: 0,
};
const result = (server as any).calculateBlockingEfficiency(trendData);
expect(result.totalConnections).toBe(300);
expect(result.totalBlocked).toBe(50);
expect(parseFloat(result.efficiencyPercent)).toBeCloseTo(16.67, 1);
expect(parseFloat(result.averageBlockRate)).toBeCloseTo(15, 0);
});
it('should handle zero connections', () => {
const trendData = {
upload: {},
download: {},
totalUpload: 0,
totalDownload: 0,
block: {},
totalConn: 0,
totalIpB: 0,
totalDnsB: 0,
};
const result = (server as any).calculateBlockingEfficiency(trendData);
expect(result.totalConnections).toBe(0);
expect(result.efficiencyPercent).toBe('0.00');
expect(result.averageBlockRate).toBe('0.00');
});
});
});
describe('FirewallaMCPServer analyze traffic', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
process.env[ENV_FIREWALLA_URL] = 'https://my.firewalla.com';
process.env[ENV_FIREWALLA_TOKEN] = 'test-token';
process.env[ENV_FIREWALLA_ID] = 'test-id';
mockFetch.mockReset();
mockStderrWrite.mockClear();
});
afterEach(() => {
process.env = originalEnv;
});
const mockFlows: FlowRecord[] = [
{
ts: 1234567890,
fd: 'out',
count: 1,
intf: 'eth0',
protocol: 'tcp',
port: 443,
devicePort: 54321,
ip: '1.2.3.4',
deviceIP: '192.168.1.100',
device: 'AA:BB:CC:DD:EE:FF',
country: 'US',
tags: [],
tagIds: [],
networkName: 'LAN',
onWan: true,
upload: 1000,
download: 5000,
},
];
describe('analyzeNetworkTraffic', () => {
it('should analyze traffic summary', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).analyzeNetworkTraffic({
startTime: 1234567800,
endTime: 1234567900,
analysisType: 'summary',
});
expect(result.content[0].text).toContain('Network traffic analysis');
expect(result.content[0].text).toContain('summary');
});
it('should analyze top talkers', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).analyzeNetworkTraffic({
startTime: 1234567800,
endTime: 1234567900,
analysisType: 'top_talkers',
});
expect(result.content[0].text).toContain('top_talkers');
});
it('should analyze blocked traffic', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).analyzeNetworkTraffic({
startTime: 1234567800,
endTime: 1234567900,
analysisType: 'blocked_traffic',
});
expect(result.content[0].text).toContain('blocked_traffic');
});
it('should analyze security events', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).analyzeNetworkTraffic({
startTime: 1234567800,
endTime: 1234567900,
analysisType: 'security_events',
});
expect(result.content[0].text).toContain('security_events');
});
});
describe('getDeviceTraffic', () => {
it('should return device traffic analysis', async () => {
const mockDevices: Device[] = [
{
ip: '192.168.1.100',
mac: 'AA:BB:CC:DD:EE:FF',
lastActive: 1234567890,
firstFound: 1234567800,
macVendor: 'Apple',
bname: 'iPhone',
names: ['iPhone'],
policy: { deviceTags: [], userTags: [], tags: [], ssidTags: [] },
name: 'iPhone',
deviceType: 'phone',
intf: { uuid: 'uuid-1', name: 'LAN' },
totalUpload: 1000,
totalDownload: 2000,
gid: 'gid-1',
online: true,
},
];
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockDevices,
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockFlows,
});
const server = new FirewallaMCPServer();
const result = await (server as any).getDeviceTraffic({
mac: 'AA:BB:CC:DD:EE:FF',
startTime: 1234567800,
endTime: 1234567900,
});
expect(result.content[0].text).toContain('Traffic analysis for device iPhone');
});
});
});