test(vitest): amplia la suite con test unitari, integrazione, componenti e stress
- 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
This commit is contained in:
@@ -16,68 +16,388 @@ 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
|
||||
let ws
|
||||
|
||||
beforeEach(() => {
|
||||
wss = new MockWebSocketServer()
|
||||
handler = setupWebSocketHandler(wss)
|
||||
ws = new MockWebSocket()
|
||||
// Simuliamo la connessione
|
||||
wss.emit('connection', ws)
|
||||
// Aggiungiamo il client al set del server (come farebbe 'ws' realmente)
|
||||
wss.clients.add(ws)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('dovrebbe registrare un client come "display" e inviare lo stato', () => {
|
||||
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' }))
|
||||
// =============================================
|
||||
// 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' }))
|
||||
|
||||
// Verifica che abbia inviato lo stato iniziale
|
||||
expect(ws.send).toHaveBeenCalled()
|
||||
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
||||
expect(sentMsg.type).toBe('state')
|
||||
expect(sentMsg.state).toBeDefined()
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
it('dovrebbe permettere al controller di cambiare il punteggio', () => {
|
||||
// 1. Registra Controller
|
||||
ws.emit('message', JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
ws.send.mockClear() // pulisco chiamate precedenti
|
||||
// =============================================
|
||||
// AZIONI
|
||||
// =============================================
|
||||
describe('Azioni', () => {
|
||||
it('dovrebbe permettere al controller di cambiare il punteggio', () => {
|
||||
const controller = connectAndRegister(wss, 'controller')
|
||||
|
||||
// 2. Invia Azione
|
||||
ws.emit('message', JSON.stringify({
|
||||
type: 'action',
|
||||
action: { type: 'incPunt', team: 'home' }
|
||||
}))
|
||||
controller.emit('message', JSON.stringify({
|
||||
type: 'action',
|
||||
action: { type: 'incPunt', team: 'home' }
|
||||
}))
|
||||
|
||||
// 3. Verifica Broadcast del nuovo stato
|
||||
expect(ws.send).toHaveBeenCalled()
|
||||
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
||||
expect(sentMsg.type).toBe('state')
|
||||
expect(sentMsg.state.sp.punt.home).toBe(1)
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
it('dovrebbe impedire al display di inviare azioni', () => {
|
||||
// 1. Registra Display
|
||||
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' }))
|
||||
ws.send.mockClear()
|
||||
// =============================================
|
||||
// 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')
|
||||
|
||||
// 2. Tenta Azione
|
||||
ws.emit('message', JSON.stringify({
|
||||
type: 'action',
|
||||
action: { type: 'incPunt', team: 'home' }
|
||||
}))
|
||||
controller.emit('message', JSON.stringify({
|
||||
type: 'action',
|
||||
action: { type: 'incPunt', team: 'home' }
|
||||
}))
|
||||
|
||||
// 3. Verifica Errore
|
||||
expect(ws.send).toHaveBeenCalled()
|
||||
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
|
||||
expect(sentMsg.type).toBe('error')
|
||||
expect(sentMsg.message).toContain('Only controllers')
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user