diff --git a/tests/integration/websocket-handler-errori.test.js b/tests/integration/websocket-handler-errori.test.js new file mode 100644 index 0000000..d415b98 --- /dev/null +++ b/tests/integration/websocket-handler-errori.test.js @@ -0,0 +1,134 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { EventEmitter } from 'events' + +// Mock parziale di una WebSocket e del Server, analogo a websocket.test.js ma +// con send/terminate che possono essere forzati a lanciare per esercitare i +// rami di gestione errori altrimenti irraggiungibili. +class MockWebSocket extends EventEmitter { + constructor() { + super() + this.readyState = 1 // OPEN + } + send = vi.fn() + terminate = vi.fn() +} + +class MockWebSocketServer extends EventEmitter { + clients = new Set() +} + +describe('websocket-handler.js — rami di errore', () => { + afterEach(() => { + vi.restoreAllMocks() + vi.resetModules() + vi.doUnmock('../../src/gameState.js') + }) + + describe('errori di invio (ws.send che lancia)', () => { + let wss, setupWebSocketHandler, consoleSpy + + beforeEach(async () => { + vi.resetModules() + ;({ setupWebSocketHandler } = await import('../../src/websocket-handler.js')) + wss = new MockWebSocketServer() + consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('handleRegister: se ws.send lancia durante l\'invio dello stato iniziale, non deve propagare l\'errore', () => { + setupWebSocketHandler(wss) + const ws = new MockWebSocket() + ws.send.mockImplementation(() => { throw new Error('socket chiuso a metà') }) + wss.emit('connection', ws) + wss.clients.add(ws) + + expect(() => { + ws.emit('message', JSON.stringify({ type: 'register', role: 'display' })) + }).not.toThrow() + expect(consoleSpy).toHaveBeenCalledWith('Error sending initial state:', expect.any(Error)) + }) + + it('sendError: se ws.send lancia rispondendo a un JSON non valido, non deve propagare l\'errore', () => { + setupWebSocketHandler(wss) + const ws = new MockWebSocket() + ws.send.mockImplementation(() => { throw new Error('socket chiuso a metà') }) + wss.emit('connection', ws) + wss.clients.add(ws) + + // JSON non valido → handleMessage va nel catch → sendError prova a rispondere → ws.send lancia + expect(() => { + ws.emit('message', 'non è JSON {{{') + }).not.toThrow() + expect(consoleSpy).toHaveBeenCalledWith('Failed to send error message:', expect.any(Error)) + }) + + it('sendError: se ws.send lancia rispondendo a un\'azione invalida, non deve propagare l\'errore', () => { + setupWebSocketHandler(wss) + const controller = new MockWebSocket() + wss.emit('connection', controller) + wss.clients.add(controller) + controller.emit('message', JSON.stringify({ type: 'register', role: 'controller' })) + + controller.send.mockImplementation(() => { throw new Error('socket chiuso a metà') }) + expect(() => { + controller.emit('message', JSON.stringify({ type: 'action' })) + }).not.toThrow() + expect(consoleSpy).toHaveBeenCalledWith('Failed to send error message:', expect.any(Error)) + }) + }) + + describe('handleError: ws.terminate che lancia', () => { + it('non deve propagare l\'errore se la chiusura forzata fallisce', async () => { + vi.resetModules() + const { setupWebSocketHandler } = await import('../../src/websocket-handler.js') + const wss = new MockWebSocketServer() + setupWebSocketHandler(wss) + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const ws = new MockWebSocket() + ws.terminate.mockImplementation(() => { throw new Error('già chiusa') }) + wss.emit('connection', ws) + + const err = new Error('Invalid UTF8') + err.code = 'WS_ERR_INVALID_UTF8' + expect(() => ws.emit('error', err)).not.toThrow() + expect(consoleSpy).toHaveBeenCalledWith('Error closing connection:', expect.any(Error)) + }) + }) + + describe('handleAction: applyAction che lancia', () => { + it('non deve propagare l\'errore, ripristina lo stato precedente e notifica il client', async () => { + vi.resetModules() + vi.doMock('../../src/gameState.js', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + applyAction: vi.fn((state, action) => { + if (action.type === 'boom') throw new Error('regola di gioco rotta') + return actual.applyAction(state, action) + }), + } + }) + const { setupWebSocketHandler } = await import('../../src/websocket-handler.js') + const wss = new MockWebSocketServer() + const handler = setupWebSocketHandler(wss) + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const controller = new MockWebSocket() + wss.emit('connection', controller) + wss.clients.add(controller) + controller.emit('message', JSON.stringify({ type: 'register', role: 'controller' })) + const statoPrecedente = handler.getState() + controller.send.mockClear() + + expect(() => { + controller.emit('message', JSON.stringify({ type: 'action', action: { type: 'boom' } })) + }).not.toThrow() + + expect(consoleSpy).toHaveBeenCalledWith('Error applying action:', expect.any(Error)) + expect(handler.getState()).toEqual(statoPrecedente) + const msg = JSON.parse(controller.send.mock.calls.at(-1)[0]) + expect(msg.type).toBe('error') + expect(msg.message).toContain('Failed to apply action') + }) + }) +})