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:
parent
c45fae8c57
commit
56de143397
367
frontend/src/socket/types.spec.ts
Normal file
367
frontend/src/socket/types.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user