chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Unit Tests for WebSocketServer
|
||||
* Tests real-time communication server
|
||||
*/
|
||||
|
||||
const WebSocketServer = require('../../src/analytics/notifications/WebSocketServer');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
// Mock the WebSocket library
|
||||
jest.mock('ws');
|
||||
|
||||
describe('WebSocketServer', () => {
|
||||
let webSocketServer;
|
||||
let mockHttpServer;
|
||||
let mockWss;
|
||||
let mockWs;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock HTTP server
|
||||
mockHttpServer = {
|
||||
listen: jest.fn(),
|
||||
close: jest.fn()
|
||||
};
|
||||
|
||||
// Mock WebSocket server
|
||||
mockWss = {
|
||||
on: jest.fn(),
|
||||
close: jest.fn(),
|
||||
clients: new Set()
|
||||
};
|
||||
|
||||
// Mock WebSocket connection
|
||||
mockWs = {
|
||||
on: jest.fn(),
|
||||
send: jest.fn(),
|
||||
close: jest.fn(),
|
||||
terminate: jest.fn(),
|
||||
ping: jest.fn(),
|
||||
readyState: WebSocket.OPEN
|
||||
};
|
||||
|
||||
// Mock WebSocket.Server constructor
|
||||
WebSocket.Server.mockImplementation(() => mockWss);
|
||||
|
||||
webSocketServer = new WebSocketServer(mockHttpServer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with default options', () => {
|
||||
expect(webSocketServer.httpServer).toBe(mockHttpServer);
|
||||
expect(webSocketServer.options.port).toBe(3334);
|
||||
expect(webSocketServer.options.path).toBe('/ws');
|
||||
expect(webSocketServer.isRunning).toBe(false);
|
||||
expect(webSocketServer.clients).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should accept custom options', () => {
|
||||
const customServer = new WebSocketServer(mockHttpServer, {
|
||||
port: 4444,
|
||||
path: '/websocket',
|
||||
heartbeatInterval: 60000
|
||||
});
|
||||
|
||||
expect(customServer.options.port).toBe(4444);
|
||||
expect(customServer.options.path).toBe('/websocket');
|
||||
expect(customServer.options.heartbeatInterval).toBe(60000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should create WebSocket server with correct options', async () => {
|
||||
await webSocketServer.initialize();
|
||||
|
||||
expect(WebSocket.Server).toHaveBeenCalledWith({
|
||||
server: mockHttpServer,
|
||||
path: '/ws',
|
||||
clientTracking: true
|
||||
});
|
||||
expect(webSocketServer.isRunning).toBe(true);
|
||||
});
|
||||
|
||||
it('should setup event handlers', async () => {
|
||||
await webSocketServer.initialize();
|
||||
|
||||
expect(mockWss.on).toHaveBeenCalledWith('connection', expect.any(Function));
|
||||
expect(mockWss.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
expect(mockWss.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should handle initialization errors', async () => {
|
||||
WebSocket.Server.mockImplementation(() => {
|
||||
throw new Error('Failed to create WebSocket server');
|
||||
});
|
||||
|
||||
await expect(webSocketServer.initialize()).rejects.toThrow('Failed to create WebSocket server');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleConnection', () => {
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
});
|
||||
|
||||
it('should register new client and setup handlers', () => {
|
||||
const mockRequest = {
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: { 'user-agent': 'test-client' }
|
||||
};
|
||||
|
||||
// Simulate connection event
|
||||
const connectionHandler = mockWss.on.mock.calls.find(call => call[0] === 'connection')[1];
|
||||
connectionHandler(mockWs, mockRequest);
|
||||
|
||||
expect(webSocketServer.clients.size).toBe(1);
|
||||
expect(mockWs.on).toHaveBeenCalledWith('message', expect.any(Function));
|
||||
expect(mockWs.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
expect(mockWs.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
expect(mockWs.on).toHaveBeenCalledWith('pong', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should send welcome message to new client', () => {
|
||||
const mockRequest = {
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: { 'user-agent': 'test-client' }
|
||||
};
|
||||
|
||||
const connectionHandler = mockWss.on.mock.calls.find(call => call[0] === 'connection')[1];
|
||||
connectionHandler(mockWs, mockRequest);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"connection"')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleClientMessage', () => {
|
||||
let clientId;
|
||||
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
|
||||
// Add a mock client
|
||||
clientId = 'test_client_123';
|
||||
webSocketServer.clients.set(clientId, {
|
||||
id: clientId,
|
||||
ws: mockWs,
|
||||
subscriptions: new Set(),
|
||||
isAlive: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle subscribe message', () => {
|
||||
const subscribeMessage = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'conversation_updates'
|
||||
});
|
||||
|
||||
webSocketServer.handleClientMessage(clientId, Buffer.from(subscribeMessage));
|
||||
|
||||
const client = webSocketServer.clients.get(clientId);
|
||||
expect(client.subscriptions.has('conversation_updates')).toBe(true);
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"subscription_confirmed"')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unsubscribe message', () => {
|
||||
// First subscribe
|
||||
const client = webSocketServer.clients.get(clientId);
|
||||
client.subscriptions.add('conversation_updates');
|
||||
|
||||
const unsubscribeMessage = JSON.stringify({
|
||||
type: 'unsubscribe',
|
||||
channel: 'conversation_updates'
|
||||
});
|
||||
|
||||
webSocketServer.handleClientMessage(clientId, Buffer.from(unsubscribeMessage));
|
||||
|
||||
expect(client.subscriptions.has('conversation_updates')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ping message', () => {
|
||||
const pingMessage = JSON.stringify({ type: 'ping' });
|
||||
|
||||
webSocketServer.handleClientMessage(clientId, Buffer.from(pingMessage));
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"pong"')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle malformed JSON gracefully', () => {
|
||||
const invalidMessage = 'invalid json{';
|
||||
|
||||
expect(() => {
|
||||
webSocketServer.handleClientMessage(clientId, Buffer.from(invalidMessage));
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('broadcast', () => {
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
|
||||
// Add mock clients
|
||||
webSocketServer.clients.set('client1', {
|
||||
id: 'client1',
|
||||
ws: { ...mockWs, readyState: WebSocket.OPEN },
|
||||
subscriptions: new Set(['conversation_updates'])
|
||||
});
|
||||
|
||||
webSocketServer.clients.set('client2', {
|
||||
id: 'client2',
|
||||
ws: { ...mockWs, readyState: WebSocket.OPEN, send: jest.fn() },
|
||||
subscriptions: new Set(['data_updates'])
|
||||
});
|
||||
});
|
||||
|
||||
it('should broadcast to all clients when no channel specified', () => {
|
||||
const message = { type: 'test_message', data: 'test' };
|
||||
|
||||
webSocketServer.broadcast(message);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should broadcast only to subscribed clients when channel specified', () => {
|
||||
const message = { type: 'conversation_state_change', data: 'test' };
|
||||
|
||||
webSocketServer.broadcast(message, 'conversation_updates');
|
||||
|
||||
// Only client1 should receive the message (subscribed to conversation_updates)
|
||||
expect(mockWs.send).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle send errors gracefully', () => {
|
||||
const errorWs = {
|
||||
...mockWs,
|
||||
send: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Send failed');
|
||||
}),
|
||||
readyState: WebSocket.OPEN
|
||||
};
|
||||
|
||||
webSocketServer.clients.set('error_client', {
|
||||
id: 'error_client',
|
||||
ws: errorWs,
|
||||
subscriptions: new Set()
|
||||
});
|
||||
|
||||
const message = { type: 'test_message', data: 'test' };
|
||||
|
||||
expect(() => {
|
||||
webSocketServer.broadcast(message);
|
||||
}).not.toThrow();
|
||||
|
||||
// Error client should be removed
|
||||
expect(webSocketServer.clients.has('error_client')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notification methods', () => {
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
jest.spyOn(webSocketServer, 'broadcast');
|
||||
});
|
||||
|
||||
it('should notify conversation state change', () => {
|
||||
webSocketServer.notifyConversationStateChange('conv_123', 'active', { project: 'test' });
|
||||
|
||||
expect(webSocketServer.broadcast).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'conversation_state_change',
|
||||
data: {
|
||||
conversationId: 'conv_123',
|
||||
newState: 'active',
|
||||
project: 'test'
|
||||
}
|
||||
},
|
||||
'conversation_updates'
|
||||
);
|
||||
});
|
||||
|
||||
it('should notify data refresh', () => {
|
||||
const testData = { conversations: [], summary: {} };
|
||||
|
||||
webSocketServer.notifyDataRefresh(testData);
|
||||
|
||||
expect(webSocketServer.broadcast).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'data_refresh',
|
||||
data: testData
|
||||
},
|
||||
'data_updates'
|
||||
);
|
||||
});
|
||||
|
||||
it('should notify system status', () => {
|
||||
const status = { message: 'System healthy', level: 'info' };
|
||||
|
||||
webSocketServer.notifySystemStatus(status);
|
||||
|
||||
expect(webSocketServer.broadcast).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'system_status',
|
||||
data: status
|
||||
},
|
||||
'system_updates'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('heartbeat mechanism', () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
await webSocketServer.initialize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should start heartbeat on initialization', () => {
|
||||
expect(webSocketServer.heartbeatInterval).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should ping clients and remove unresponsive ones', () => {
|
||||
// Add responsive client
|
||||
const responsiveWs = { ...mockWs, ping: jest.fn() };
|
||||
webSocketServer.clients.set('responsive', {
|
||||
id: 'responsive',
|
||||
ws: responsiveWs,
|
||||
isAlive: true
|
||||
});
|
||||
|
||||
// Add unresponsive client
|
||||
const unresponsiveWs = { ...mockWs, terminate: jest.fn() };
|
||||
webSocketServer.clients.set('unresponsive', {
|
||||
id: 'unresponsive',
|
||||
ws: unresponsiveWs,
|
||||
isAlive: false
|
||||
});
|
||||
|
||||
// Trigger heartbeat
|
||||
jest.advanceTimersByTime(30000);
|
||||
|
||||
expect(responsiveWs.ping).toHaveBeenCalled();
|
||||
expect(unresponsiveWs.terminate).toHaveBeenCalled();
|
||||
expect(webSocketServer.clients.has('unresponsive')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
});
|
||||
|
||||
it('should return server statistics', () => {
|
||||
// Add some mock clients
|
||||
webSocketServer.clients.set('client1', {
|
||||
id: 'client1',
|
||||
ip: '127.0.0.1',
|
||||
connectedAt: new Date(),
|
||||
subscriptions: new Set(['test']),
|
||||
isAlive: true
|
||||
});
|
||||
|
||||
const stats = webSocketServer.getStats();
|
||||
|
||||
expect(stats).toMatchObject({
|
||||
isRunning: true,
|
||||
clientCount: 1,
|
||||
queuedMessages: 0,
|
||||
clients: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'client1',
|
||||
ip: '127.0.0.1',
|
||||
subscriptions: ['test'],
|
||||
isAlive: true
|
||||
})
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
beforeEach(async () => {
|
||||
await webSocketServer.initialize();
|
||||
});
|
||||
|
||||
it('should close all connections and stop server', async () => {
|
||||
// Add a mock client
|
||||
webSocketServer.clients.set('client1', {
|
||||
id: 'client1',
|
||||
ws: mockWs
|
||||
});
|
||||
|
||||
await webSocketServer.close();
|
||||
|
||||
expect(mockWs.close).toHaveBeenCalledWith(1000, 'Server shutting down');
|
||||
expect(mockWss.close).toHaveBeenCalled();
|
||||
expect(webSocketServer.isRunning).toBe(false);
|
||||
expect(webSocketServer.clients.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user