/** * DC-076: Tests for the dashboard WebSocket server */ const http = require('http'); const WebSocket = require('ws'); const EventEmitter = require('events'); const createDashboardWS = require('../../src/websocket/dashboard-ws'); function createMockServer() { return http.createServer((req, res) => { res.writeHead(404); res.end(); }); } describe('DC-076: Dashboard WebSocket', () => { let server, wsServer, port; beforeEach((done) => { server = createMockServer(); server.listen(0, () => { port = server.address().port; const resourceMonitor = new EventEmitter(); const healthChecker = new EventEmitter(); const updateManager = new EventEmitter(); wsServer = createDashboardWS(server, { resourceMonitor, healthChecker, updateManager, log: { info: jest.fn(), error: jest.fn() }, }); done(); }); }); afterEach((done) => { wsServer.close(); server.close(done); }); it('accepts connections at the upgrade path', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('open', () => { ws.close(); }); ws.on('close', () => { done(); }); ws.on('error', done); }); it('sends a connected event on join', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'connected') { expect(msg.data).toHaveProperty('clients'); ws.close(); done(); } }); ws.on('error', done); }); it('responds to ping with pong', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('open', () => { ws.send(JSON.stringify({ type: 'ping' })); }); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'pong') { ws.close(); done(); } }); ws.on('error', done); }); it('responds to subscribe with subscribed confirmation', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('open', () => { ws.send(JSON.stringify({ type: 'subscribe', events: ['resource-alert', 'incident'] })); }); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'subscribed') { expect(msg.events).toEqual(['resource-alert', 'incident']); ws.close(); done(); } }); ws.on('error', done); }); it('responds to client-count request', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('open', () => { ws.send(JSON.stringify({ type: 'client-count' })); }); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'client-count') { expect(msg.count).toBeGreaterThanOrEqual(1); ws.close(); done(); } }); ws.on('error', done); }); it('returns error for invalid JSON', (done) => { const ws = new WebSocket(`ws://localhost:${port}/api/v1/ws`); ws.on('open', () => { ws.send('not json'); }); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'error') { expect(msg.error).toContain('Invalid JSON'); ws.close(); done(); } }); ws.on('error', done); }); it('tracks client count', () => { expect(wsServer.getClientCount()).toBe(0); }); it('broadcast method does not throw with no clients', () => { expect(() => wsServer.broadcast('test', { foo: 'bar' })).not.toThrow(); }); });