@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
395 lines (337 loc) • 12.8 kB
text/typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DashboardServer } from '../server.js';
import { Server } from 'http';
import { io as ioClient, Socket } from 'socket.io-client';
import request from 'supertest';
// Mock the modules we depend on
vi.mock('../../utils/performance-monitor.js', () => ({
PerformanceMonitor: {
getInstance: vi.fn(() => ({
getSystemMetrics: vi.fn().mockResolvedValue({ cpu: 50, memory: 60 }),
getToolMetrics: vi.fn().mockResolvedValue([]),
getQualityMetrics: vi.fn().mockResolvedValue({ testCoverage: 80 }),
getPerformanceAlerts: vi.fn().mockResolvedValue([])
}))
}
}));
vi.mock('../../utils/security-manager.js', () => ({
SecurityManager: {
getInstance: vi.fn(() => ({
getSecurityStatus: vi.fn().mockResolvedValue({ level: 'medium' }),
getSecurityEvents: vi.fn().mockResolvedValue([]),
generateSecurityMetrics: vi.fn().mockResolvedValue({ totalEvents: 0 })
}))
}
}));
vi.mock('../../utils/error-handler.js', () => ({
ErrorHandler: vi.fn().mockImplementation(() => ({
getErrorTimeline: vi.fn().mockResolvedValue([]),
analyzeErrorPatterns: vi.fn().mockResolvedValue({ patterns: [] })
}))
}));
vi.mock('../../modules/agile-management/agile-manager.js', () => ({
AgileManager: vi.fn().mockImplementation(() => ({
getAllSprints: vi.fn().mockResolvedValue([]),
getAllStories: vi.fn().mockResolvedValue([]),
getAllEpics: vi.fn().mockResolvedValue([])
}))
}));
// Mock the SQLite manager to prevent database initialization issues
vi.mock('../../storage/sqlite-manager.js', () => ({
getSQLiteManager: vi.fn(() => ({
query: vi.fn().mockResolvedValue({ success: true, data: [] }),
get: vi.fn().mockResolvedValue({ success: true, data: null }),
run: vi.fn().mockResolvedValue({ success: true, data: { changes: 1 } }),
initialize: vi.fn().mockResolvedValue(true),
close: vi.fn().mockResolvedValue(true)
})),
ensureDatabaseReady: vi.fn().mockResolvedValue({
query: vi.fn().mockResolvedValue({ success: true, data: [] }),
get: vi.fn().mockResolvedValue({ success: true, data: null }),
run: vi.fn().mockResolvedValue({ success: true, data: { changes: 1 } }),
initialize: vi.fn().mockResolvedValue(true),
close: vi.fn().mockResolvedValue(true),
getConnectionInfo: vi.fn().mockReturnValue({
isInitialized: true,
dbPath: ':memory:'
})
})
}));
describe('DashboardServer', () => {
let dashboardServer: DashboardServer;
let httpServer: Server;
let clientSocket: Socket;
let testPort: number;
let mockPerformanceMonitor: any;
let mockSecurityManager: any;
let mockErrorHandler: any;
let mockAgileManager: any;
// Generate a unique port for each test run
const getRandomPort = () => Math.floor(Math.random() * 10000) + 40000;
beforeEach(async () => {
// Generate a unique port for this test
testPort = getRandomPort();
// Create mocks for dependencies
mockPerformanceMonitor = {
getSystemMetrics: vi.fn().mockResolvedValue({ cpu: 50, memory: 60 }),
getToolMetrics: vi.fn().mockResolvedValue([]),
getQualityMetrics: vi.fn().mockResolvedValue({ testCoverage: 80 }),
getPerformanceAlerts: vi.fn().mockResolvedValue([])
};
mockSecurityManager = {
getSecurityStatus: vi.fn().mockResolvedValue({ level: 'medium' }),
getSecurityEvents: vi.fn().mockResolvedValue([]),
generateSecurityMetrics: vi.fn().mockResolvedValue({ totalEvents: 0 })
};
mockErrorHandler = {
getErrorTimeline: vi.fn().mockResolvedValue([]),
analyzeErrorPatterns: vi.fn().mockResolvedValue({ patterns: [] })
};
mockAgileManager = {
getAllSprints: vi.fn().mockResolvedValue([]),
getAllStories: vi.fn().mockResolvedValue([]),
getAllEpics: vi.fn().mockResolvedValue([])
};
const config = {
enabled: true,
port: testPort,
host: 'localhost',
autoOpen: false,
features: {
performance: true,
security: true,
agile: true,
errors: true
},
realTimeUpdates: true,
exportEnabled: true
};
dashboardServer = new DashboardServer(
config,
mockPerformanceMonitor,
mockSecurityManager,
mockErrorHandler,
mockAgileManager
);
await dashboardServer.start();
httpServer = dashboardServer['server']; // Access private server property for testing
// Give the server time to start
await new Promise(resolve => setTimeout(resolve, 100));
});
afterEach(async () => {
if (clientSocket && clientSocket.connected) {
clientSocket.disconnect();
clientSocket = null;
}
if (dashboardServer) {
await dashboardServer.stop();
// Add a small delay to ensure port is released
await new Promise(resolve => setTimeout(resolve, 100));
}
});
describe('Server Lifecycle', () => {
it('should start the server on specified port', async () => {
const response = await request(`http://localhost:${testPort}`).get('/');
expect(response.status).toBe(200);
expect(response.type).toBe('text/html');
});
it('should serve static files', async () => {
const response = await request(`http://localhost:${testPort}`).get('/css/dashboard.css');
expect(response.status).toBe(200);
expect(response.type).toBe('text/css');
});
it('should handle health check endpoint', async () => {
const response = await request(`http://localhost:${testPort}`).get('/api/health');
expect(response.status).toBe(200);
expect(response.body).toEqual({
status: 'healthy',
timestamp: expect.any(String),
features: expect.any(Object),
version: expect.any(String),
database: expect.any(Object)
});
});
it('should stop gracefully', async () => {
// Create a separate instance for this test
const stopTestPort = getRandomPort();
const stopTestServer = new DashboardServer(
{
enabled: true,
port: stopTestPort,
host: 'localhost',
autoOpen: false,
features: {
performance: true,
security: true,
agile: true,
errors: true
},
realTimeUpdates: false,
exportEnabled: true
},
mockPerformanceMonitor,
mockSecurityManager,
mockErrorHandler,
mockAgileManager
);
await stopTestServer.start();
await stopTestServer.stop();
// Try to connect - should fail
try {
await request(`http://localhost:${stopTestPort}`).get('/');
expect.fail('Server should be stopped');
} catch (error) {
// Expected - server is stopped
expect(error).toBeDefined();
}
});
});
describe('WebSocket Communication', () => {
beforeEach((done) => {
clientSocket = ioClient(`http://localhost:${testPort}`, {
transports: ['websocket'],
autoConnect: true
});
clientSocket.on('connect', done);
});
it('should accept websocket connections', () => {
// The beforeEach already establishes the connection via done callback
// So if we reach this test, the connection was successful
expect(clientSocket).toBeDefined();
expect(clientSocket.io).toBeDefined();
});
it('should send initial data on connection', (done) => {
clientSocket.on('initial-data', (data: any) => {
expect(data).toHaveProperty('metrics');
expect(data).toHaveProperty('security');
expect(data).toHaveProperty('errors');
expect(data).toHaveProperty('agile');
done();
});
});
it('should broadcast dashboard updates', (done) => {
clientSocket.on('dashboard-update', (data: any) => {
expect(data).toHaveProperty('metrics');
expect(data).toHaveProperty('timestamp');
done();
});
// Trigger an update by waiting for the interval
setTimeout(() => {
// Update should have been broadcast by now
}, 2000);
});
it('should handle client disconnection', (done) => {
clientSocket.on('disconnect', () => {
expect(clientSocket.connected).toBe(false);
done();
});
clientSocket.disconnect();
});
it('should handle request-data event', (done) => {
clientSocket.emit('request-data', { type: 'metrics' });
clientSocket.on('data-response', (data: any) => {
expect(data.type).toBe('metrics');
expect(data.data).toBeDefined();
done();
});
});
});
describe('API Integration', () => {
it('should serve metrics API', async () => {
const response = await request(`http://localhost:${testPort}`)
.get('/api/metrics/overview');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('cpu');
});
it('should serve security API', async () => {
const response = await request(`http://localhost:${testPort}`)
.get('/api/security/status');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('level');
});
it('should serve agile API', async () => {
const response = await request(`http://localhost:${testPort}`)
.get('/api/agile/sprints');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// The API returns data object with sprints array
expect(response.body.data).toBeDefined();
expect(response.body.data.sprints).toBeInstanceOf(Array);
});
it('should serve errors API', async () => {
// Add a small delay to ensure server is ready
await new Promise(resolve => setTimeout(resolve, 100));
const response = await request(`http://localhost:${testPort}`)
.get('/api/errors/timeline')
.timeout(5000); // Add explicit timeout
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('timeline');
}, 10000); // Increase test timeout
});
describe('Error Handling', () => {
it('should handle 404 for unknown routes', async () => {
const response = await request(`http://localhost:${testPort}`)
.get('/unknown/route');
expect(response.status).toBe(404);
});
it('should handle API errors gracefully', async () => {
// Force an error by mocking a failure
mockPerformanceMonitor.getSystemMetrics.mockRejectedValueOnce(new Error('Test error'));
const response = await request(`http://localhost:${testPort}`)
.get('/api/metrics/overview');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to fetch system metrics');
});
});
describe('Data Collection', () => {
it('should collect data periodically', async () => {
const initialDataSpy = vi.fn();
// Connect a client to capture updates
const testClient = ioClient(`http://localhost:${testPort}`, {
transports: ['websocket']
});
testClient.on('performance_update', initialDataSpy);
// Wait for at least one update cycle (6 seconds to be safe)
await new Promise(resolve => setTimeout(resolve, 6000));
expect(initialDataSpy).toHaveBeenCalled();
testClient.disconnect();
}, 15000); // Set timeout to 15 seconds
});
describe('Configuration', () => {
it('should use custom configuration when provided', async () => {
const customPort = getRandomPort();
const customConfig = {
enabled: true,
port: customPort,
host: 'localhost',
autoOpen: false,
features: {
performance: true,
security: true,
agile: true,
errors: true
},
realTimeUpdates: true,
exportEnabled: true,
updateInterval: 5000,
maxDataPoints: 200
};
const customDashboard = new DashboardServer(
customConfig,
mockPerformanceMonitor,
mockSecurityManager,
mockErrorHandler,
mockAgileManager
);
await customDashboard.start();
// Verify server started with custom config
const response = await request(`http://localhost:${customPort}`).get('/api/health');
expect(response.status).toBe(200);
await customDashboard.stop();
});
});
});