/** * Unit Tests: Player Deduplication * * Tests the player deduplication logic for CSV uploads */ import { describe, test, expect, mock, beforeEach,} from 'bun:test'; import { prisma } from '@/lib/prisma'; // Create mock functions at module level const playerFindFirstMock = mock(async (_args: any): Promise => null); const playerCreateMock = mock(async (_args: any): Promise => ({})); // Mock the prisma module mock.module('@/lib/prisma', () => ({ prisma: { player: { findFirst: playerFindFirstMock, create: playerCreateMock, }, }, })); // Extract the findOrCreatePlayer logic for testing async function findOrCreatePlayer(name: string) { // Normalize the name for deduplication: trim whitespace and convert to lowercase const normalizedName = name.trim().toLowerCase(); // Try to find existing player with matching normalized name let player = await prisma.player.findFirst({ where: { normalizedName }, }); if (!player) { // Create a new player with the original name and normalized name player = await prisma.player.create({ data: { name: name.trim(), normalizedName, rating: 1000, currentElo: 1000, createdAt: new Date(), updatedAt: new Date(), }, }); } return player; } describe('Player Deduplication', () => { beforeEach(() => { // Clear all mock history before each test playerFindFirstMock.mockClear(); playerCreateMock.mockClear(); }); describe('findOrCreatePlayer', () => { test('should find existing player by exact name match', async () => { const mockPlayer = { id: 1, name: 'Emily', normalizedName: 'emily', currentElo: 1008, gamesPlayed: 5, wins: 4, losses: 1, createdAt: new Date(), updatedAt: new Date(), rating: 0, }; playerFindFirstMock.mockImplementation(async () => mockPlayer); const result = await findOrCreatePlayer('Emily'); expect(result).toEqual(mockPlayer); expect(prisma.player.findFirst).toHaveBeenCalledWith({ where: { normalizedName: 'emily' }, }); expect(prisma.player.create).not.toHaveBeenCalled(); }); test('should find existing player by case-insensitive match', async () => { const mockPlayer = { id: 1, name: 'Emily', normalizedName: 'emily', currentElo: 1008, gamesPlayed: 5, wins: 4, losses: 1, createdAt: new Date(), updatedAt: new Date(), rating: 0, }; playerFindFirstMock.mockImplementation(async () => mockPlayer); const result = await findOrCreatePlayer('EMILY'); expect(result).toEqual(mockPlayer); expect(prisma.player.findFirst).toHaveBeenCalledWith({ where: { normalizedName: 'emily' }, }); expect(prisma.player.create).not.toHaveBeenCalled(); }); test('should trim whitespace from names', async () => { const mockPlayer = { id: 1, name: 'Emily', normalizedName: 'emily', currentElo: 1008, gamesPlayed: 5, wins: 4, losses: 1, createdAt: new Date(), updatedAt: new Date(), rating: 0, }; playerFindFirstMock.mockImplementation(async () => mockPlayer); const result = await findOrCreatePlayer(' Emily '); expect(result).toEqual(mockPlayer); expect(prisma.player.findFirst).toHaveBeenCalledWith({ where: { normalizedName: 'emily' }, }); expect(prisma.player.create).not.toHaveBeenCalled(); }); test('should create new player if not found', async () => { playerFindFirstMock.mockImplementation(async () => null); const newPlayer = { id: 100, name: 'NewPlayer', normalizedName: 'newplayer', rating: 1000, currentElo: 1000, gamesPlayed: 0, wins: 0, losses: 0, createdAt: new Date(), updatedAt: new Date(), }; playerCreateMock.mockImplementation(async () => newPlayer); const result = await findOrCreatePlayer('NewPlayer'); expect(result).toEqual(newPlayer); expect(prisma.player.findFirst).toHaveBeenCalledWith({ where: { normalizedName: 'newplayer' }, }); expect(prisma.player.create).toHaveBeenCalledWith({ data: { name: 'NewPlayer', normalizedName: 'newplayer', rating: 1000, currentElo: 1000, createdAt: expect.any(Date), updatedAt: expect.any(Date), }, }); }); test('should handle names with special characters', async () => { playerFindFirstMock.mockImplementation(async () => null); const newPlayer = { id: 100, name: 'Test-Player_123', normalizedName: 'test-player_123', rating: 1000, currentElo: 1000, gamesPlayed: 0, wins: 0, losses: 0, createdAt: new Date(), updatedAt: new Date(), }; playerCreateMock.mockImplementation(async () => newPlayer); const result = await findOrCreatePlayer('Test-Player_123'); expect(result).toEqual(newPlayer); expect(prisma.player.create).toHaveBeenCalledWith({ data: { name: 'Test-Player_123', normalizedName: 'test-player_123', rating: 1000, currentElo: 1000, createdAt: expect.any(Date), updatedAt: expect.any(Date), }, }); }); test('should handle names with spaces', async () => { playerFindFirstMock.mockImplementation(async () => null); const newPlayer = { id: 100, name: 'Dave B', normalizedName: 'dave b', rating: 1000, currentElo: 1000, gamesPlayed: 0, wins: 0, losses: 0, createdAt: new Date(), updatedAt: new Date(), }; playerCreateMock.mockImplementation(async () => newPlayer); const result = await findOrCreatePlayer('Dave B'); expect(result).toEqual(newPlayer); expect(prisma.player.create).toHaveBeenCalledWith({ data: { name: 'Dave B', normalizedName: 'dave b', rating: 1000, currentElo: 1000, createdAt: expect.any(Date), updatedAt: expect.any(Date), }, }); }); test('should deduplicate players with different cases', async () => { // First call with "EMILY" const mockPlayer = { id: 1, name: 'Emily', normalizedName: 'emily', currentElo: 1008, gamesPlayed: 5, wins: 4, losses: 1, createdAt: new Date(), updatedAt: new Date(), rating: 0, }; playerFindFirstMock.mockImplementation(async () => mockPlayer); const result1 = await findOrCreatePlayer('EMILY'); const result2 = await findOrCreatePlayer('Emily'); const result3 = await findOrCreatePlayer('emily'); // All should return the same player expect(result1.id).toEqual(result2.id); expect(result2.id).toEqual(result3.id); expect(prisma.player.create).not.toHaveBeenCalled(); }); test('should handle empty or whitespace-only names', async () => { playerFindFirstMock.mockImplementation(async () => null); const newPlayer = { id: 100, name: '', normalizedName: '', rating: 1000, currentElo: 1000, gamesPlayed: 0, wins: 0, losses: 0, createdAt: new Date(), updatedAt: new Date(), }; playerCreateMock.mockImplementation(async () => newPlayer); const result = await findOrCreatePlayer(' '); expect(result).toEqual(newPlayer); expect(prisma.player.create).toHaveBeenCalledWith({ data: { name: '', normalizedName: '', rating: 1000, currentElo: 1000, createdAt: expect.any(Date), updatedAt: expect.any(Date), }, }); }); }); describe('Player Name Normalization', () => { test('should normalize names consistently', () => { const testCases = [ { input: 'Emily', expected: 'emily' }, { input: 'EMILY', expected: 'emily' }, { input: ' Emily ', expected: 'emily' }, { input: 'Dave B', expected: 'dave b' }, { input: 'Sara R', expected: 'sara r' }, { input: 'Test-Player', expected: 'test-player' }, ]; testCases.forEach(({ input, expected }) => { const normalizedName = input.trim().toLowerCase(); expect(normalizedName).toBe(expected); }); }); test('should prevent duplicate players with same normalized name', async () => { // Simulate checking for existing player const existingPlayer = { id: 1, name: 'Emily', normalizedName: 'emily', currentElo: 1008, gamesPlayed: 5, wins: 4, losses: 1, createdAt: new Date(), updatedAt: new Date(), rating: 0, }; // Configure mock to return existing player for these specific calls playerFindFirstMock.mockImplementation(async (args: any) => { if (args.where.normalizedName === 'emily') { return existingPlayer; } if (args.where.normalizedName === 'emily') { return existingPlayer; } return null; }); const result1 = await findOrCreatePlayer('Emily'); const result2 = await findOrCreatePlayer('EMILY'); const result3 = await findOrCreatePlayer(' Emily '); // All should return the same player ID expect(result1.id).toBe(1); expect(result2.id).toBe(1); expect(result3.id).toBe(1); // No new players should be created expect(prisma.player.create).not.toHaveBeenCalled(); }); }); });