feat: add recalculateAllElo function for idempotent ELO recalculation
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Unit Tests: ELO Recalculation
|
||||
*
|
||||
* Tests the idempotent full recalculation of ELO ratings and player statistics
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||
import { recalculateAllElo } from '@/lib/elo-utils';
|
||||
|
||||
// Mock Prisma client
|
||||
const mockPrisma = {
|
||||
player: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
eloSnapshot: {
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
partnershipStat: {
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
findFirst: vi.fn().mockResolvedValue(null), // No existing stats initially
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
match: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
|
||||
describe('recalculateAllElo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should reset all player stats to zero', async () => {
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue([]);
|
||||
|
||||
const result = await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
expect(mockPrisma.player.updateMany).toHaveBeenCalledWith({
|
||||
data: {
|
||||
currentElo: 1000,
|
||||
gamesPlayed: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.matchesProcessed).toBe(0);
|
||||
expect(result.playersUpdated).toBe(0);
|
||||
expect(result.partnershipsUpdated).toBe(0);
|
||||
});
|
||||
|
||||
test('should delete all existing elo snapshots', async () => {
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue([]);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
expect(mockPrisma.eloSnapshot.deleteMany).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
test('should delete all existing partnership stats', async () => {
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue([]);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
expect(mockPrisma.partnershipStat.deleteMany).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
test('should return correct result for empty match list', async () => {
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue([]);
|
||||
|
||||
const result = await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
matchesProcessed: 0,
|
||||
playersUpdated: 0,
|
||||
partnershipsUpdated: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('should process matches in chronological order', async () => {
|
||||
const mockMatches = [
|
||||
{
|
||||
id: 1,
|
||||
playedAt: new Date('2024-01-01'),
|
||||
team1P1: { id: 1, name: 'Player 1' },
|
||||
team1P2: { id: 2, name: 'Player 2' },
|
||||
team2P1: { id: 3, name: 'Player 3' },
|
||||
team2P2: { id: 4, name: 'Player 4' },
|
||||
team1Score: 10,
|
||||
team2Score: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
playedAt: new Date('2024-01-02'),
|
||||
team1P1: { id: 1, name: 'Player 1' },
|
||||
team1P2: { id: 2, name: 'Player 2' },
|
||||
team2P1: { id: 5, name: 'Player 5' },
|
||||
team2P2: { id: 6, name: 'Player 6' },
|
||||
team1Score: 8,
|
||||
team2Score: 6,
|
||||
},
|
||||
];
|
||||
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue(mockMatches);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
// Should process 2 matches
|
||||
expect(mockPrisma.match.findMany).toHaveBeenCalledWith({
|
||||
orderBy: { playedAt: 'asc' },
|
||||
include: {
|
||||
team1P1: true,
|
||||
team1P2: true,
|
||||
team2P1: true,
|
||||
team2P2: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should create elo snapshots for each player in each match', async () => {
|
||||
const mockMatches = [
|
||||
{
|
||||
id: 1,
|
||||
playedAt: new Date('2024-01-01'),
|
||||
team1P1: { id: 1, name: 'Player 1' },
|
||||
team1P2: { id: 2, name: 'Player 2' },
|
||||
team2P1: { id: 3, name: 'Player 3' },
|
||||
team2P2: { id: 4, name: 'Player 4' },
|
||||
team1Score: 10,
|
||||
team2Score: 5,
|
||||
},
|
||||
];
|
||||
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue(mockMatches);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
// Should create 4 snapshots (one for each player)
|
||||
expect(mockPrisma.eloSnapshot.create).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test('should update player stats after processing matches', async () => {
|
||||
const mockMatches = [
|
||||
{
|
||||
id: 1,
|
||||
playedAt: new Date('2024-01-01'),
|
||||
team1P1: { id: 1, name: 'Player 1' },
|
||||
team1P2: { id: 2, name: 'Player 2' },
|
||||
team2P1: { id: 3, name: 'Player 3' },
|
||||
team2P2: { id: 4, name: 'Player 4' },
|
||||
team1Score: 10,
|
||||
team2Score: 5,
|
||||
},
|
||||
];
|
||||
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue(mockMatches);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
// Should update 4 players
|
||||
expect(mockPrisma.player.update).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test('should update partnership stats after processing matches', async () => {
|
||||
const mockMatches = [
|
||||
{
|
||||
id: 1,
|
||||
playedAt: new Date('2024-01-01'),
|
||||
team1P1: { id: 1, name: 'Player 1' },
|
||||
team1P2: { id: 2, name: 'Player 2' },
|
||||
team2P1: { id: 3, name: 'Player 3' },
|
||||
team2P2: { id: 4, name: 'Player 4' },
|
||||
team1Score: 10,
|
||||
team2Score: 5,
|
||||
},
|
||||
];
|
||||
|
||||
(mockPrisma.match.findMany as any).mockResolvedValue(mockMatches);
|
||||
|
||||
await recalculateAllElo(mockPrisma as any);
|
||||
|
||||
// Should check for existing partnership stats (2 calls) and create new ones (2 calls)
|
||||
expect(mockPrisma.partnershipStat.findFirst).toHaveBeenCalledTimes(2);
|
||||
expect(mockPrisma.partnershipStat.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user