- aggiunge test per gameState e utilita server - aggiunge test di integrazione WebSocket - aggiunge test componenti Vue (ControllerPage/DisplayPage) - aggiunge test stress su carico WebSocket - aggiorna configurazione Vitest per includere nuove cartelle e ambiente componenti - aggiorna script npm e dipendenze di test
404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
import { setupWebSocketHandler } from '../../src/websocket-handler.js'
|
|
import { EventEmitter } from 'events'
|
|
|
|
// Mock parziale di una WebSocket e del Server
|
|
class MockWebSocket extends EventEmitter {
|
|
constructor() {
|
|
super()
|
|
this.readyState = 1 // OPEN
|
|
}
|
|
send = vi.fn()
|
|
terminate = vi.fn()
|
|
}
|
|
|
|
class MockWebSocketServer extends EventEmitter {
|
|
clients = new Set()
|
|
}
|
|
|
|
// Helper: connette e registra un client
|
|
function connectAndRegister(wss, role) {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
ws.emit('message', JSON.stringify({ type: 'register', role }))
|
|
ws.send.mockClear()
|
|
return ws
|
|
}
|
|
|
|
// Helper: ultimo messaggio inviato a un ws
|
|
function lastSent(ws) {
|
|
const calls = ws.send.mock.calls
|
|
return JSON.parse(calls[calls.length - 1][0])
|
|
}
|
|
|
|
describe('WebSocket Integration (websocket-handler.js)', () => {
|
|
let wss
|
|
let handler
|
|
|
|
beforeEach(() => {
|
|
wss = new MockWebSocketServer()
|
|
handler = setupWebSocketHandler(wss)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
// =============================================
|
|
// REGISTRAZIONE
|
|
// =============================================
|
|
describe('Registrazione', () => {
|
|
it('dovrebbe registrare un client come "display" e inviare lo stato', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' }))
|
|
|
|
expect(ws.send).toHaveBeenCalled()
|
|
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
|
expect(sentMsg.type).toBe('state')
|
|
expect(sentMsg.state).toBeDefined()
|
|
})
|
|
|
|
it('dovrebbe registrare un client come "controller"', () => {
|
|
connectAndRegister(wss, 'controller')
|
|
expect(handler.getClients().size).toBe(1)
|
|
})
|
|
|
|
it('dovrebbe rifiutare ruolo non valido', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
ws.emit('message', JSON.stringify({ type: 'register', role: 'hacker' }))
|
|
|
|
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
|
expect(sentMsg.type).toBe('error')
|
|
expect(sentMsg.message).toContain('Invalid role')
|
|
})
|
|
|
|
it('dovrebbe usare "display" come ruolo default se mancante', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
ws.emit('message', JSON.stringify({ type: 'register' }))
|
|
|
|
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
|
expect(sentMsg.type).toBe('state')
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// AZIONI
|
|
// =============================================
|
|
describe('Azioni', () => {
|
|
it('dovrebbe permettere al controller di cambiare il punteggio', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
expect(controller.send).toHaveBeenCalled()
|
|
const sentMsg = lastSent(controller)
|
|
expect(sentMsg.type).toBe('state')
|
|
expect(sentMsg.state.sp.punt.home).toBe(1)
|
|
})
|
|
|
|
it('dovrebbe impedire al display di inviare azioni', () => {
|
|
const display = connectAndRegister(wss, 'display')
|
|
|
|
display.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
const sentMsg = lastSent(display)
|
|
expect(sentMsg.type).toBe('error')
|
|
expect(sentMsg.message).toContain('Only controllers')
|
|
})
|
|
|
|
it('dovrebbe impedire azioni da client non registrati', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
|
|
ws.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
|
expect(sentMsg.type).toBe('error')
|
|
expect(sentMsg.message).toContain('Only controllers')
|
|
})
|
|
|
|
it('dovrebbe rifiutare azione con formato invalido (missing action)', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'action'
|
|
}))
|
|
|
|
const sentMsg = lastSent(controller)
|
|
expect(sentMsg.type).toBe('error')
|
|
expect(sentMsg.message).toContain('Invalid action format')
|
|
})
|
|
|
|
it('dovrebbe rifiutare azione con formato invalido (missing action.type)', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { team: 'home' }
|
|
}))
|
|
|
|
const sentMsg = lastSent(controller)
|
|
expect(sentMsg.type).toBe('error')
|
|
expect(sentMsg.message).toContain('Invalid action format')
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// BROADCAST MULTI-CLIENT
|
|
// =============================================
|
|
describe('Broadcast', () => {
|
|
it('dovrebbe inviare lo stato a tutti i client dopo un\'azione', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
const display1 = connectAndRegister(wss, 'display')
|
|
const display2 = connectAndRegister(wss, 'display')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
// Tutti i client nel set dovrebbero aver ricevuto lo stato
|
|
expect(controller.send).toHaveBeenCalled()
|
|
expect(display1.send).toHaveBeenCalled()
|
|
expect(display2.send).toHaveBeenCalled()
|
|
|
|
const msg1 = lastSent(display1)
|
|
const msg2 = lastSent(display2)
|
|
expect(msg1.type).toBe('state')
|
|
expect(msg1.state.sp.punt.home).toBe(1)
|
|
expect(msg2.state.sp.punt.home).toBe(1)
|
|
})
|
|
|
|
it('non dovrebbe inviare a client con readyState != OPEN', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
const closedClient = connectAndRegister(wss, 'display')
|
|
closedClient.readyState = 3 // CLOSED
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
// closedClient non dovrebbe aver ricevuto il broadcast
|
|
expect(closedClient.send).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// SPEAK
|
|
// =============================================
|
|
describe('Speak', () => {
|
|
it('dovrebbe inoltrare il messaggio speak solo ai display', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
const display = connectAndRegister(wss, 'display')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'speak',
|
|
text: 'quindici a dieci'
|
|
}))
|
|
|
|
// Il display riceve il messaggio speak
|
|
expect(display.send).toHaveBeenCalled()
|
|
const msg = lastSent(display)
|
|
expect(msg.type).toBe('speak')
|
|
expect(msg.text).toBe('quindici a dieci')
|
|
})
|
|
|
|
it('non dovrebbe permettere al display di inviare speak', () => {
|
|
const display = connectAndRegister(wss, 'display')
|
|
|
|
display.emit('message', JSON.stringify({
|
|
type: 'speak',
|
|
text: 'test'
|
|
}))
|
|
|
|
const msg = lastSent(display)
|
|
expect(msg.type).toBe('error')
|
|
expect(msg.message).toContain('Only controllers')
|
|
})
|
|
|
|
it('dovrebbe rifiutare speak con testo vuoto', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'speak',
|
|
text: ' '
|
|
}))
|
|
|
|
const msg = lastSent(controller)
|
|
expect(msg.type).toBe('error')
|
|
expect(msg.message).toContain('Invalid speak payload')
|
|
})
|
|
|
|
it('dovrebbe rifiutare speak senza testo', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'speak'
|
|
}))
|
|
|
|
const msg = lastSent(controller)
|
|
expect(msg.type).toBe('error')
|
|
})
|
|
|
|
it('dovrebbe fare trim del testo speak', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
const display = connectAndRegister(wss, 'display')
|
|
|
|
controller.emit('message', JSON.stringify({
|
|
type: 'speak',
|
|
text: ' dieci a otto '
|
|
}))
|
|
|
|
const msg = lastSent(display)
|
|
expect(msg.text).toBe('dieci a otto')
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// MESSAGGI MALFORMATI
|
|
// =============================================
|
|
describe('Messaggi malformati', () => {
|
|
it('dovrebbe gestire JSON non valido senza crash', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
wss.clients.add(ws)
|
|
|
|
expect(() => {
|
|
ws.emit('message', 'questo non è JSON {{{')
|
|
}).not.toThrow()
|
|
|
|
const msg = lastSent(ws)
|
|
expect(msg.type).toBe('error')
|
|
expect(msg.message).toContain('Invalid message format')
|
|
})
|
|
|
|
it('dovrebbe gestire Buffer come input', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
|
|
const buf = Buffer.from(JSON.stringify({
|
|
type: 'action',
|
|
action: { type: 'incPunt', team: 'home' }
|
|
}))
|
|
|
|
controller.emit('message', buf)
|
|
|
|
const msg = lastSent(controller)
|
|
expect(msg.type).toBe('state')
|
|
expect(msg.state.sp.punt.home).toBe(1)
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// DISCONNESSIONE
|
|
// =============================================
|
|
describe('Disconnessione', () => {
|
|
it('dovrebbe rimuovere il client dalla mappa alla disconnessione', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
expect(handler.getClients().size).toBe(1)
|
|
|
|
controller.emit('close')
|
|
|
|
expect(handler.getClients().size).toBe(0)
|
|
})
|
|
|
|
it('i client rimanenti non dovrebbero essere affetti dalla disconnessione', () => {
|
|
const controller = connectAndRegister(wss, 'controller')
|
|
const display = connectAndRegister(wss, 'display')
|
|
expect(handler.getClients().size).toBe(2)
|
|
|
|
controller.emit('close')
|
|
expect(handler.getClients().size).toBe(1)
|
|
|
|
expect(handler.getClients().has(display)).toBe(true)
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// ERRORI WEBSOCKET
|
|
// =============================================
|
|
describe('Errori WebSocket', () => {
|
|
it('dovrebbe terminare la connessione per errore UTF8 invalido', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
|
|
const err = new Error('Invalid UTF8')
|
|
err.code = 'WS_ERR_INVALID_UTF8'
|
|
ws.emit('error', err)
|
|
|
|
expect(ws.terminate).toHaveBeenCalled()
|
|
})
|
|
|
|
it('dovrebbe terminare la connessione per close code invalido', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
|
|
const err = new Error('Invalid close code')
|
|
err.code = 'WS_ERR_INVALID_CLOSE_CODE'
|
|
ws.emit('error', err)
|
|
|
|
expect(ws.terminate).toHaveBeenCalled()
|
|
})
|
|
|
|
it('non dovrebbe terminare per altri errori', () => {
|
|
const ws = new MockWebSocket()
|
|
wss.emit('connection', ws)
|
|
|
|
const err = new Error('Generic error')
|
|
ws.emit('error', err)
|
|
|
|
expect(ws.terminate).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// =============================================
|
|
// API PUBBLICA
|
|
// =============================================
|
|
describe('API pubblica', () => {
|
|
it('getState dovrebbe restituire lo stato corrente', () => {
|
|
const state = handler.getState()
|
|
expect(state.sp.punt.home).toBe(0)
|
|
expect(state.sp.punt.guest).toBe(0)
|
|
})
|
|
|
|
it('setState dovrebbe sovrascrivere lo stato', () => {
|
|
const newState = handler.getState()
|
|
newState.sp.punt.home = 99
|
|
handler.setState(newState)
|
|
expect(handler.getState().sp.punt.home).toBe(99)
|
|
})
|
|
|
|
it('broadcastState dovrebbe inviare a tutti i client', () => {
|
|
const display = connectAndRegister(wss, 'display')
|
|
handler.broadcastState()
|
|
expect(display.send).toHaveBeenCalled()
|
|
const msg = lastSent(display)
|
|
expect(msg.type).toBe('state')
|
|
})
|
|
|
|
it('getClients dovrebbe restituire la mappa dei client', () => {
|
|
expect(handler.getClients()).toBeInstanceOf(Map)
|
|
expect(handler.getClients().size).toBe(0)
|
|
connectAndRegister(wss, 'display')
|
|
expect(handler.getClients().size).toBe(1)
|
|
})
|
|
})
|
|
})
|