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:
Thomas Hallock
2025-10-10 10:45:26 -05:00
parent 6c66bb27b7
commit 76a63901c4

View File

@@ -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')
}
})
})
})