bb6be245b7
## 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>
354 lines
9.7 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|