Files
euchre_camp/src/__tests__/unit/player-deduplication.test.ts
T
david bb6be245b7
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 18s
feat: Implement tournament schedule tab and fix E2E tests (#27)
## Summary

This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures.

### Changes Included

1. **Tournament Schedule Feature**
   - Added tournament schedule page at `/admin/tournaments/[id]/schedule`
   - Implemented "Generate Schedule" button functionality
   - Added schedule generation logic for round-robin tournaments

2. **E2E Test Fixes**
   - Fixed database connection issues in production builds
   - Improved test reliability with better error handling and debugging
   - Updated test infrastructure to use environment variables instead of hardcoded values

3. **CI/CD Updates**
   - Added E2E test job to PR workflow
   - Configured tests to run against development database
   - Moved database password to Gitea secrets

4. **Code Quality**
   - Removed hardcoded passwords from codebase
   - Improved Prisma client configuration
   - Enhanced authentication and navigation components

### Test Results
All 16 E2E test scenarios are now passing:
- Authentication tests: 
- Registration tests: 
- Tournament schedule tests: 
- Player schedule tests: 
- Admin navigation tests: 

### Database Configuration
- Tests run against `euchre_camp_dev` database
- Production builds use environment variables for database configuration
- Database password stored in Gitea secrets as `DB_PASSWORD`

### CI Pipeline
The PR workflow now includes:
1. Unit tests
2. E2E tests (using production build)
3. Version bump analysis

E2E tests must pass before PR can be merged.

Reviewed-on: #27
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
2026-04-27 01:59:16 +00:00

354 lines
9.7 KiB
TypeScript

/**
* 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<any> => null);
const playerCreateMock = mock(async (_args: any): Promise<any> => ({}));
// 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();
});
});
});