test: add comprehensive integration tests for player ownership
Add 15 integration tests covering real-world multiplayer scenarios: - Full ownership flow with 3 users, 6 players - Turn-based authorization checks - Player filtering for UI display - Edge cases (empty rooms, missing data, fallbacks) - Mid-game scenarios (player leaving, multiple active players) All tests passing ✓ Part of player ownership centralization plan (Phase 7/7). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Integration tests for player ownership in multiplayer scenarios
|
||||
*
|
||||
* These tests verify that ownership logic works correctly across
|
||||
* the full stack: database → utilities → client state
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { RoomData } from '@/hooks/useRoomData'
|
||||
import {
|
||||
buildPlayerMetadata,
|
||||
buildPlayerOwnershipFromRoomData,
|
||||
filterPlayersByOwner,
|
||||
getPlayerOwner,
|
||||
getUniqueOwners,
|
||||
groupPlayersByOwner,
|
||||
isPlayerOwnedByUser,
|
||||
type PlayerOwnershipMap,
|
||||
} from '../player-ownership'
|
||||
|
||||
describe('Player Ownership Integration Tests', () => {
|
||||
// Simulate a real multiplayer room scenario
|
||||
let mockRoomData: RoomData
|
||||
let playerIds: string[]
|
||||
let playersMap: Map<string, { name: string; emoji: string; color: string }>
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup: 3 users in a room, each with different number of players
|
||||
mockRoomData = {
|
||||
id: 'room-integration-test',
|
||||
name: 'Test Multiplayer Room',
|
||||
code: 'TEST',
|
||||
gameName: 'memory-pairs',
|
||||
members: [
|
||||
{
|
||||
id: 'member-1',
|
||||
userId: 'user-alice',
|
||||
displayName: 'Alice',
|
||||
isOnline: true,
|
||||
isCreator: true,
|
||||
},
|
||||
{
|
||||
id: 'member-2',
|
||||
userId: 'user-bob',
|
||||
displayName: 'Bob',
|
||||
isOnline: true,
|
||||
isCreator: false,
|
||||
},
|
||||
{
|
||||
id: 'member-3',
|
||||
userId: 'user-charlie',
|
||||
displayName: 'Charlie',
|
||||
isOnline: true,
|
||||
isCreator: false,
|
||||
},
|
||||
],
|
||||
memberPlayers: {
|
||||
'user-alice': [
|
||||
{ id: 'player-a1', name: 'Alice P1', emoji: '🐶', color: '#ff0000' },
|
||||
{ id: 'player-a2', name: 'Alice P2', emoji: '🐱', color: '#ff4444' },
|
||||
],
|
||||
'user-bob': [{ id: 'player-b1', name: 'Bob P1', emoji: '🐭', color: '#00ff00' }],
|
||||
'user-charlie': [
|
||||
{ id: 'player-c1', name: 'Charlie P1', emoji: '🦊', color: '#0000ff' },
|
||||
{ id: 'player-c2', name: 'Charlie P2', emoji: '🐻', color: '#4444ff' },
|
||||
{ id: 'player-c3', name: 'Charlie P3', emoji: '🐼', color: '#8888ff' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
playerIds = ['player-a1', 'player-a2', 'player-b1', 'player-c1', 'player-c2', 'player-c3']
|
||||
|
||||
playersMap = new Map([
|
||||
['player-a1', { name: 'Alice P1', emoji: '🐶', color: '#ff0000' }],
|
||||
['player-a2', { name: 'Alice P2', emoji: '🐱', color: '#ff4444' }],
|
||||
['player-b1', { name: 'Bob P1', emoji: '🐭', color: '#00ff00' }],
|
||||
['player-c1', { name: 'Charlie P1', emoji: '🦊', color: '#0000ff' }],
|
||||
['player-c2', { name: 'Charlie P2', emoji: '🐻', color: '#4444ff' }],
|
||||
['player-c3', { name: 'Charlie P3', emoji: '🐼', color: '#8888ff' }],
|
||||
])
|
||||
})
|
||||
|
||||
describe('Full multiplayer ownership flow', () => {
|
||||
it('should correctly identify ownership across all players', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
|
||||
// Alice owns 2 players
|
||||
expect(isPlayerOwnedByUser('player-a1', 'user-alice', ownership)).toBe(true)
|
||||
expect(isPlayerOwnedByUser('player-a2', 'user-alice', ownership)).toBe(true)
|
||||
|
||||
// Bob owns 1 player
|
||||
expect(isPlayerOwnedByUser('player-b1', 'user-bob', ownership)).toBe(true)
|
||||
|
||||
// Charlie owns 3 players
|
||||
expect(isPlayerOwnedByUser('player-c1', 'user-charlie', ownership)).toBe(true)
|
||||
expect(isPlayerOwnedByUser('player-c2', 'user-charlie', ownership)).toBe(true)
|
||||
expect(isPlayerOwnedByUser('player-c3', 'user-charlie', ownership)).toBe(true)
|
||||
|
||||
// Cross-ownership checks (should be false)
|
||||
expect(isPlayerOwnedByUser('player-a1', 'user-bob', ownership)).toBe(false)
|
||||
expect(isPlayerOwnedByUser('player-b1', 'user-charlie', ownership)).toBe(false)
|
||||
expect(isPlayerOwnedByUser('player-c1', 'user-alice', ownership)).toBe(false)
|
||||
})
|
||||
|
||||
it('should build complete metadata for game state', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
const metadata = buildPlayerMetadata(playerIds, ownership, playersMap)
|
||||
|
||||
// Check all players have metadata
|
||||
expect(Object.keys(metadata)).toHaveLength(6)
|
||||
|
||||
// Verify Alice's players
|
||||
expect(metadata['player-a1']).toEqual({
|
||||
id: 'player-a1',
|
||||
name: 'Alice P1',
|
||||
emoji: '🐶',
|
||||
color: '#ff0000',
|
||||
userId: 'user-alice',
|
||||
})
|
||||
expect(metadata['player-a2'].userId).toBe('user-alice')
|
||||
|
||||
// Verify Bob's player
|
||||
expect(metadata['player-b1'].userId).toBe('user-bob')
|
||||
|
||||
// Verify Charlie's players
|
||||
expect(metadata['player-c1'].userId).toBe('user-charlie')
|
||||
expect(metadata['player-c2'].userId).toBe('user-charlie')
|
||||
expect(metadata['player-c3'].userId).toBe('user-charlie')
|
||||
})
|
||||
|
||||
it('should correctly group players by owner for team display', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
const groups = groupPlayersByOwner(playerIds, ownership)
|
||||
|
||||
expect(groups.size).toBe(3)
|
||||
expect(groups.get('user-alice')).toEqual(['player-a1', 'player-a2'])
|
||||
expect(groups.get('user-bob')).toEqual(['player-b1'])
|
||||
expect(groups.get('user-charlie')).toEqual(['player-c1', 'player-c2', 'player-c3'])
|
||||
})
|
||||
|
||||
it('should identify unique participants', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
const owners = getUniqueOwners(ownership)
|
||||
|
||||
expect(owners).toHaveLength(3)
|
||||
expect(owners).toContain('user-alice')
|
||||
expect(owners).toContain('user-bob')
|
||||
expect(owners).toContain('user-charlie')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Turn-based game authorization scenarios', () => {
|
||||
let ownership: PlayerOwnershipMap
|
||||
|
||||
beforeEach(() => {
|
||||
ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
})
|
||||
|
||||
it('should allow Alice to act when her player has the turn', () => {
|
||||
const currentPlayerId = 'player-a1'
|
||||
const currentViewerId = 'user-alice'
|
||||
|
||||
const canAct = isPlayerOwnedByUser(currentPlayerId, currentViewerId, ownership)
|
||||
expect(canAct).toBe(true)
|
||||
})
|
||||
|
||||
it('should block Bob from acting when Alice has the turn', () => {
|
||||
const currentPlayerId = 'player-a1' // Alice's turn
|
||||
const currentViewerId = 'user-bob' // Bob trying to act
|
||||
|
||||
const canAct = isPlayerOwnedByUser(currentPlayerId, currentViewerId, ownership)
|
||||
expect(canAct).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle turn rotation correctly', () => {
|
||||
const turnOrder = ['player-a1', 'player-b1', 'player-c1']
|
||||
|
||||
for (const currentPlayer of turnOrder) {
|
||||
const owner = getPlayerOwner(currentPlayer, ownership)
|
||||
expect(owner).toBeDefined()
|
||||
|
||||
// Verify only the owner can act
|
||||
for (const userId of ['user-alice', 'user-bob', 'user-charlie']) {
|
||||
const canAct = isPlayerOwnedByUser(currentPlayer, userId, ownership)
|
||||
expect(canAct).toBe(userId === owner)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Player filtering for UI display', () => {
|
||||
let ownership: PlayerOwnershipMap
|
||||
|
||||
beforeEach(() => {
|
||||
ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
})
|
||||
|
||||
it('should filter to show only local players for current user', () => {
|
||||
const alicePlayers = filterPlayersByOwner(playerIds, 'user-alice', ownership)
|
||||
expect(alicePlayers).toEqual(['player-a1', 'player-a2'])
|
||||
|
||||
const bobPlayers = filterPlayersByOwner(playerIds, 'user-bob', ownership)
|
||||
expect(bobPlayers).toEqual(['player-b1'])
|
||||
|
||||
const charliePlayers = filterPlayersByOwner(playerIds, 'user-charlie', ownership)
|
||||
expect(charliePlayers).toEqual(['player-c1', 'player-c2', 'player-c3'])
|
||||
})
|
||||
|
||||
it('should handle empty results for users not in game', () => {
|
||||
const outsiderPlayers = filterPlayersByOwner(playerIds, 'user-not-in-room', ownership)
|
||||
expect(outsiderPlayers).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases and error handling', () => {
|
||||
it('should handle room with no players gracefully', () => {
|
||||
const emptyRoomData: RoomData = {
|
||||
...mockRoomData,
|
||||
memberPlayers: {},
|
||||
}
|
||||
|
||||
const ownership = buildPlayerOwnershipFromRoomData(emptyRoomData)
|
||||
expect(ownership).toEqual({})
|
||||
|
||||
const owners = getUniqueOwners(ownership)
|
||||
expect(owners).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle single-player room', () => {
|
||||
const soloRoomData: RoomData = {
|
||||
...mockRoomData,
|
||||
memberPlayers: {
|
||||
'user-solo': [{ id: 'player-solo', name: 'Solo', emoji: '🚀', color: '#ff00ff' }],
|
||||
},
|
||||
}
|
||||
|
||||
const ownership = buildPlayerOwnershipFromRoomData(soloRoomData)
|
||||
expect(Object.keys(ownership)).toHaveLength(1)
|
||||
expect(ownership['player-solo']).toBe('user-solo')
|
||||
})
|
||||
|
||||
it('should handle metadata building with missing player data', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
const partialPlayerIds = ['player-a1', 'player-nonexistent', 'player-b1']
|
||||
|
||||
const metadata = buildPlayerMetadata(partialPlayerIds, ownership, playersMap)
|
||||
|
||||
expect(metadata['player-a1']).toBeDefined()
|
||||
expect(metadata['player-b1']).toBeDefined()
|
||||
expect(metadata['player-nonexistent']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should use fallback userId when player not in ownership map', () => {
|
||||
const partialOwnership: PlayerOwnershipMap = {
|
||||
'player-a1': 'user-alice',
|
||||
}
|
||||
|
||||
const metadata = buildPlayerMetadata(
|
||||
['player-a1', 'player-a2'],
|
||||
partialOwnership,
|
||||
playersMap,
|
||||
'fallback-user'
|
||||
)
|
||||
|
||||
expect(metadata['player-a1'].userId).toBe('user-alice')
|
||||
expect(metadata['player-a2'].userId).toBe('fallback-user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Real-world multiplayer scenarios', () => {
|
||||
it('should handle player leaving mid-game', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
|
||||
// Bob leaves, but his player data remains in game state
|
||||
const remainingPlayerIds = playerIds.filter((id) => id !== 'player-b1')
|
||||
|
||||
// Ownership map still has Bob's data
|
||||
expect(ownership['player-b1']).toBe('user-bob')
|
||||
|
||||
// But filtered lists exclude his players
|
||||
const alicePlayers = filterPlayersByOwner(remainingPlayerIds, 'user-alice', ownership)
|
||||
const bobPlayers = filterPlayersByOwner(remainingPlayerIds, 'user-bob', ownership)
|
||||
|
||||
expect(alicePlayers).toHaveLength(2)
|
||||
expect(bobPlayers).toHaveLength(0) // Bob's player filtered out
|
||||
})
|
||||
|
||||
it('should handle user with multiple active players in turn order', () => {
|
||||
const ownership = buildPlayerOwnershipFromRoomData(mockRoomData)
|
||||
|
||||
// Charlie has 3 players in rotation
|
||||
const charliePlayers = ['player-c1', 'player-c2', 'player-c3']
|
||||
|
||||
for (const playerId of charliePlayers) {
|
||||
expect(isPlayerOwnedByUser(playerId, 'user-charlie', ownership)).toBe(true)
|
||||
expect(getPlayerOwner(playerId, ownership)).toBe('user-charlie')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user