204 lines
6.3 KiB
TypeScript
204 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { eq } from 'drizzle-orm'
|
|
import { db, schema } from '../src/db'
|
|
import { createArcadeSession, getArcadeSession } from '../src/lib/arcade/session-manager'
|
|
import { cleanupExpiredRooms, createRoom } from '../src/lib/arcade/room-manager'
|
|
|
|
/**
|
|
* E2E Test: Orphaned Session After Room TTL Deletion
|
|
*
|
|
* This test simulates the exact scenario reported by the user:
|
|
* 1. User creates a game session in a room
|
|
* 2. Room expires via TTL cleanup
|
|
* 3. User navigates to /arcade
|
|
* 4. System should NOT redirect to the orphaned game
|
|
* 5. User should see the arcade lobby normally
|
|
*/
|
|
describe('E2E: Orphaned Session Cleanup on Navigation', () => {
|
|
const testUserId = 'e2e-user-id'
|
|
const testGuestId = 'e2e-guest-id'
|
|
let testRoomId: string
|
|
|
|
beforeEach(async () => {
|
|
// Create test user (simulating new or returning visitor)
|
|
await db
|
|
.insert(schema.users)
|
|
.values({
|
|
id: testUserId,
|
|
guestId: testGuestId,
|
|
createdAt: new Date(),
|
|
})
|
|
.onConflictDoNothing()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
// Clean up test data
|
|
await db.delete(schema.arcadeSessions).where(eq(schema.arcadeSessions.userId, testUserId))
|
|
await db.delete(schema.users).where(eq(schema.users.id, testUserId))
|
|
if (testRoomId) {
|
|
try {
|
|
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, testRoomId))
|
|
} catch {
|
|
// Room may already be deleted
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should not redirect user to orphaned game after room TTL cleanup', async () => {
|
|
// === SETUP PHASE ===
|
|
// User creates or joins a room
|
|
const room = await createRoom({
|
|
name: 'My Game Room',
|
|
createdBy: testGuestId,
|
|
creatorName: 'Test Player',
|
|
gameName: 'matching',
|
|
gameConfig: { difficulty: 6, gameType: 'abacus-numeral', turnTimer: 30 },
|
|
ttlMinutes: 1, // Short TTL for testing
|
|
})
|
|
testRoomId = room.id
|
|
|
|
// User starts a game session
|
|
const session = await createArcadeSession({
|
|
userId: testGuestId,
|
|
gameName: 'matching',
|
|
gameUrl: '/arcade/matching',
|
|
initialState: {
|
|
gamePhase: 'playing',
|
|
cards: [],
|
|
gameCards: [],
|
|
flippedCards: [],
|
|
matchedPairs: 0,
|
|
totalPairs: 6,
|
|
currentPlayer: 'player-1',
|
|
difficulty: 6,
|
|
gameType: 'abacus-numeral',
|
|
turnTimer: 30,
|
|
},
|
|
activePlayers: ['player-1'],
|
|
roomId: room.id,
|
|
})
|
|
|
|
// Verify session was created
|
|
expect(session).toBeDefined()
|
|
expect(session.roomId).toBe(room.id)
|
|
|
|
// === TTL EXPIRATION PHASE ===
|
|
// Simulate time passing - room's TTL expires
|
|
// Set lastActivity to past so cleanup detects it
|
|
await db
|
|
.update(schema.arcadeRooms)
|
|
.set({
|
|
lastActivity: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago
|
|
})
|
|
.where(eq(schema.arcadeRooms.id, room.id))
|
|
|
|
// Run cleanup (simulating background cleanup job)
|
|
const deletedCount = await cleanupExpiredRooms()
|
|
expect(deletedCount).toBeGreaterThan(0) // Room should be deleted
|
|
|
|
// === USER NAVIGATION PHASE ===
|
|
// User navigates to /arcade (arcade lobby)
|
|
// Client checks for active session
|
|
const activeSession = await getArcadeSession(testGuestId)
|
|
|
|
// === ASSERTION PHASE ===
|
|
// Expected behavior: NO active session returned
|
|
// This prevents redirect to /arcade/matching which would be broken
|
|
expect(activeSession).toBeUndefined()
|
|
|
|
// Verify the orphaned session was cleaned up from database
|
|
const [orphanedSessionCheck] = await db
|
|
.select()
|
|
.from(schema.arcadeSessions)
|
|
.where(eq(schema.arcadeSessions.userId, testUserId))
|
|
.limit(1)
|
|
|
|
expect(orphanedSessionCheck).toBeUndefined()
|
|
})
|
|
|
|
it('should allow user to start new game after orphaned session cleanup', async () => {
|
|
// === SETUP: Create and orphan a session ===
|
|
const oldRoom = await createRoom({
|
|
name: 'Old Room',
|
|
createdBy: testGuestId,
|
|
creatorName: 'Test Player',
|
|
gameName: 'matching',
|
|
gameConfig: { difficulty: 6 },
|
|
ttlMinutes: 1,
|
|
})
|
|
|
|
await createArcadeSession({
|
|
userId: testGuestId,
|
|
gameName: 'matching',
|
|
gameUrl: '/arcade/matching',
|
|
initialState: { gamePhase: 'setup' },
|
|
activePlayers: ['player-1'],
|
|
roomId: oldRoom.id,
|
|
})
|
|
|
|
// Delete room (TTL cleanup)
|
|
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, oldRoom.id))
|
|
|
|
// === ACTION: User tries to access arcade ===
|
|
const orphanedSession = await getArcadeSession(testGuestId)
|
|
expect(orphanedSession).toBeUndefined() // Orphan cleaned up
|
|
|
|
// === ACTION: User creates new room and session ===
|
|
const newRoom = await createRoom({
|
|
name: 'New Room',
|
|
createdBy: testGuestId,
|
|
creatorName: 'Test Player',
|
|
gameName: 'matching',
|
|
gameConfig: { difficulty: 8 },
|
|
ttlMinutes: 60,
|
|
})
|
|
testRoomId = newRoom.id
|
|
|
|
const newSession = await createArcadeSession({
|
|
userId: testGuestId,
|
|
gameName: 'matching',
|
|
gameUrl: '/arcade/matching',
|
|
initialState: { gamePhase: 'setup' },
|
|
activePlayers: ['player-1', 'player-2'],
|
|
roomId: newRoom.id,
|
|
})
|
|
|
|
// === ASSERTION: New session works correctly ===
|
|
expect(newSession).toBeDefined()
|
|
expect(newSession.roomId).toBe(newRoom.id)
|
|
|
|
const activeSession = await getArcadeSession(testGuestId)
|
|
expect(activeSession).toBeDefined()
|
|
expect(activeSession?.roomId).toBe(newRoom.id)
|
|
})
|
|
|
|
it('should handle race condition: getArcadeSession called while room is being deleted', async () => {
|
|
// Create room and session
|
|
const room = await createRoom({
|
|
name: 'Race Condition Room',
|
|
createdBy: testGuestId,
|
|
creatorName: 'Test Player',
|
|
gameName: 'matching',
|
|
gameConfig: { difficulty: 6 },
|
|
ttlMinutes: 60,
|
|
})
|
|
testRoomId = room.id
|
|
|
|
await createArcadeSession({
|
|
userId: testGuestId,
|
|
gameName: 'matching',
|
|
gameUrl: '/arcade/matching',
|
|
initialState: { gamePhase: 'setup' },
|
|
activePlayers: ['player-1'],
|
|
roomId: room.id,
|
|
})
|
|
|
|
// Simulate race: delete room while getArcadeSession is checking
|
|
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room.id))
|
|
|
|
// Should gracefully handle and return undefined
|
|
const result = await getArcadeSession(testGuestId)
|
|
expect(result).toBeUndefined()
|
|
})
|
|
})
|