Add socket message factory tests - TEST-020 complete (20 tests)

Quick win #2: Test coverage for WebSocket message factory functions

Tests cover:
- generateMessageId() produces valid UUIDs
- createJoinGameMessage() with/without last_event_id
- createActionMessage() with various action types
- createResignMessage() structure
- createHeartbeatMessage() structure
- Message ID uniqueness across all factories
- Integration tests validating message structure

Results:
- 20 new tests, all passing
- Socket/types.ts factory functions: 0% → 100% coverage
- WebSocket message reliability improved
- Message tracking validation established

All tests pass (1045/1045 total, +20 from previous)
This commit is contained in:
Cal Corum 2026-02-02 15:39:11 -06:00
parent c45fae8c57
commit 56de143397

View File

@ -0,0 +1,367 @@
/**
* Tests for WebSocket message types and factory functions.
*
* Verifies message creation, structure, and unique ID generation.
*/
import { describe, it, expect } from 'vitest'
import {
generateMessageId,
createJoinGameMessage,
createActionMessage,
createResignMessage,
createHeartbeatMessage,
} from './types'
import type { GameAction } from './types'
describe('Socket Message Types', () => {
describe('generateMessageId', () => {
it('generates a unique message ID', () => {
/**
* Test that generateMessageId returns a UUID.
*
* Message IDs must be unique for tracking and deduplication.
*/
const id = generateMessageId()
expect(id).toBeTruthy()
expect(typeof id).toBe('string')
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
})
it('generates different IDs on successive calls', () => {
/**
* Test that generateMessageId produces unique values.
*
* Each message must have a unique ID for proper tracking.
*/
const id1 = generateMessageId()
const id2 = generateMessageId()
const id3 = generateMessageId()
expect(id1).not.toBe(id2)
expect(id2).not.toBe(id3)
expect(id1).not.toBe(id3)
})
})
describe('createJoinGameMessage', () => {
it('creates a join game message with game ID', () => {
/**
* Test createJoinGameMessage basic structure.
*
* Join messages must include type, message_id, and game_id.
*/
const gameId = 'game-123'
const message = createJoinGameMessage(gameId)
expect(message.type).toBe('join_game')
expect(message.game_id).toBe(gameId)
expect(message.message_id).toBeTruthy()
expect(message.message_id).toMatch(/^[0-9a-f-]+$/i)
})
it('includes last_event_id when provided', () => {
/**
* Test createJoinGameMessage with last_event_id for reconnection.
*
* When reconnecting, clients send the last event ID they received
* to resume from that point.
*/
const gameId = 'game-123'
const lastEventId = 'event-456'
const message = createJoinGameMessage(gameId, lastEventId)
expect(message.type).toBe('join_game')
expect(message.game_id).toBe(gameId)
expect(message.last_event_id).toBe(lastEventId)
})
it('omits last_event_id when not provided', () => {
/**
* Test createJoinGameMessage without last_event_id for new connections.
*
* Initial joins don't have a last_event_id.
*/
const gameId = 'game-123'
const message = createJoinGameMessage(gameId)
expect(message.last_event_id).toBeUndefined()
})
it('generates unique message IDs', () => {
/**
* Test that each join message has a unique ID.
*
* Message IDs must be unique even for the same game.
*/
const gameId = 'game-123'
const message1 = createJoinGameMessage(gameId)
const message2 = createJoinGameMessage(gameId)
expect(message1.message_id).not.toBe(message2.message_id)
})
})
describe('createActionMessage', () => {
it('creates an action message with game ID and action', () => {
/**
* Test createActionMessage basic structure.
*
* Action messages must include type, message_id, game_id, and action.
*/
const gameId = 'game-123'
const action: GameAction = {
action_type: 'play_card',
card_id: 'card-456',
}
const message = createActionMessage(gameId, action)
expect(message.type).toBe('action')
expect(message.game_id).toBe(gameId)
expect(message.action).toEqual(action)
expect(message.message_id).toBeTruthy()
})
it('preserves action data structure', () => {
/**
* Test that action data is preserved without modification.
*
* The action object should be included as-is in the message.
*/
const gameId = 'game-123'
const action: GameAction = {
action_type: 'attack',
attacker_id: 'attacker-1',
defender_id: 'defender-2',
attack_index: 0,
}
const message = createActionMessage(gameId, action)
expect(message.action).toEqual(action)
expect(message.action.action_type).toBe('attack')
expect(message.action.attacker_id).toBe('attacker-1')
expect(message.action.defender_id).toBe('defender-2')
expect(message.action.attack_index).toBe(0)
})
it('handles different action types', () => {
/**
* Test createActionMessage with various action types.
*
* Different game actions should all be properly wrapped.
*/
const gameId = 'game-123'
const playCardMessage = createActionMessage(gameId, {
action_type: 'play_card',
card_id: 'card-1',
})
const attachEnergyMessage = createActionMessage(gameId, {
action_type: 'attach_energy',
card_id: 'energy-1',
target_id: 'pokemon-1',
})
const endTurnMessage = createActionMessage(gameId, {
action_type: 'end_turn',
})
expect(playCardMessage.action.action_type).toBe('play_card')
expect(attachEnergyMessage.action.action_type).toBe('attach_energy')
expect(endTurnMessage.action.action_type).toBe('end_turn')
})
it('generates unique message IDs for different actions', () => {
/**
* Test that each action message has a unique ID.
*
* Even identical actions should have different message IDs.
*/
const gameId = 'game-123'
const action: GameAction = { action_type: 'end_turn' }
const message1 = createActionMessage(gameId, action)
const message2 = createActionMessage(gameId, action)
expect(message1.message_id).not.toBe(message2.message_id)
})
})
describe('createResignMessage', () => {
it('creates a resign message with game ID', () => {
/**
* Test createResignMessage basic structure.
*
* Resign messages must include type, message_id, and game_id.
*/
const gameId = 'game-123'
const message = createResignMessage(gameId)
expect(message.type).toBe('resign')
expect(message.game_id).toBe(gameId)
expect(message.message_id).toBeTruthy()
})
it('generates unique message IDs', () => {
/**
* Test that each resign message has a unique ID.
*
* Message IDs must be unique even for the same game.
*/
const gameId = 'game-123'
const message1 = createResignMessage(gameId)
const message2 = createResignMessage(gameId)
expect(message1.message_id).not.toBe(message2.message_id)
})
it('includes only required fields', () => {
/**
* Test that resign messages don't include extra fields.
*
* Resign is simple - just type, message_id, and game_id.
*/
const gameId = 'game-123'
const message = createResignMessage(gameId)
const keys = Object.keys(message)
expect(keys).toContain('type')
expect(keys).toContain('message_id')
expect(keys).toContain('game_id')
expect(keys.length).toBe(3)
})
})
describe('createHeartbeatMessage', () => {
it('creates a heartbeat message', () => {
/**
* Test createHeartbeatMessage basic structure.
*
* Heartbeat messages must include type and message_id.
*/
const message = createHeartbeatMessage()
expect(message.type).toBe('heartbeat')
expect(message.message_id).toBeTruthy()
})
it('does not include game_id', () => {
/**
* Test that heartbeat messages are game-independent.
*
* Heartbeats maintain the connection regardless of active game.
*/
const message = createHeartbeatMessage()
expect('game_id' in message).toBe(false)
})
it('generates unique message IDs', () => {
/**
* Test that each heartbeat has a unique ID.
*
* Message IDs must be unique for tracking.
*/
const message1 = createHeartbeatMessage()
const message2 = createHeartbeatMessage()
expect(message1.message_id).not.toBe(message2.message_id)
})
it('includes only required fields', () => {
/**
* Test that heartbeat messages don't include extra fields.
*
* Heartbeat is minimal - just type and message_id.
*/
const message = createHeartbeatMessage()
const keys = Object.keys(message)
expect(keys).toContain('type')
expect(keys).toContain('message_id')
expect(keys.length).toBe(2)
})
})
describe('message factories integration', () => {
it('all factories generate valid UUID message IDs', () => {
/**
* Test that all message factories use proper UUID format.
*
* Consistent ID format ensures reliable message tracking.
*/
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const joinMessage = createJoinGameMessage('game-1')
const actionMessage = createActionMessage('game-1', { action_type: 'end_turn' })
const resignMessage = createResignMessage('game-1')
const heartbeatMessage = createHeartbeatMessage()
expect(joinMessage.message_id).toMatch(uuidRegex)
expect(actionMessage.message_id).toMatch(uuidRegex)
expect(resignMessage.message_id).toMatch(uuidRegex)
expect(heartbeatMessage.message_id).toMatch(uuidRegex)
})
it('messages for the same game have different IDs', () => {
/**
* Test that multiple messages for one game have unique IDs.
*
* This ensures proper message tracking within a game session.
*/
const gameId = 'game-123'
const message1 = createJoinGameMessage(gameId)
const message2 = createActionMessage(gameId, { action_type: 'end_turn' })
const message3 = createResignMessage(gameId)
const ids = [message1.message_id, message2.message_id, message3.message_id]
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(3) // All IDs should be unique
})
it('message structure matches expected types', () => {
/**
* Test that factory output matches TypeScript type definitions.
*
* This validates that the factories produce correctly-typed messages.
*/
const gameId = 'game-123'
const joinMessage = createJoinGameMessage(gameId)
expect(joinMessage).toHaveProperty('type')
expect(joinMessage).toHaveProperty('message_id')
expect(joinMessage).toHaveProperty('game_id')
const actionMessage = createActionMessage(gameId, { action_type: 'end_turn' })
expect(actionMessage).toHaveProperty('type')
expect(actionMessage).toHaveProperty('message_id')
expect(actionMessage).toHaveProperty('game_id')
expect(actionMessage).toHaveProperty('action')
const resignMessage = createResignMessage(gameId)
expect(resignMessage).toHaveProperty('type')
expect(resignMessage).toHaveProperty('message_id')
expect(resignMessage).toHaveProperty('game_id')
const heartbeatMessage = createHeartbeatMessage()
expect(heartbeatMessage).toHaveProperty('type')
expect(heartbeatMessage).toHaveProperty('message_id')
})
})
})