967bdc1b89
- Set explicit min/max password length in Better Auth config (8-128 chars) - Fix CSV upload tests to use multipart/form-data instead of data - Add @chromium-admin annotations to tests requiring admin access
220 lines
7.1 KiB
TypeScript
220 lines
7.1 KiB
TypeScript
/**
|
|
* E2E Test: CSV Upload Player Deduplication
|
|
*
|
|
* Tests that CSV uploads correctly deduplicate players by name
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { prisma } from '@/lib/prisma';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
|
|
test.describe('CSV Upload Player Deduplication', () => {
|
|
let testTournamentId: number;
|
|
const testPlayerIds: number[] = [];
|
|
|
|
test.beforeAll(async () => {
|
|
// Create a test tournament
|
|
const tournament = await prisma.event.create({
|
|
data: {
|
|
name: 'Deduplication Test Tournament',
|
|
eventDate: new Date(),
|
|
eventType: 'tournament',
|
|
format: 'round_robin',
|
|
status: 'planned',
|
|
},
|
|
});
|
|
testTournamentId = tournament.id;
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
// Clean up test data
|
|
if (testTournamentId) {
|
|
// Delete matches first (they reference players)
|
|
await prisma.match.deleteMany({
|
|
where: { eventId: testTournamentId },
|
|
});
|
|
|
|
// Delete event participants
|
|
await prisma.eventParticipant.deleteMany({
|
|
where: { eventId: testTournamentId },
|
|
});
|
|
|
|
// Delete test tournament
|
|
await prisma.event.delete({
|
|
where: { id: testTournamentId },
|
|
});
|
|
}
|
|
|
|
// Delete test players (those with "Dedupe" in the name)
|
|
await prisma.player.deleteMany({
|
|
where: {
|
|
name: {
|
|
contains: 'Dedupe',
|
|
},
|
|
},
|
|
});
|
|
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
test('should not create duplicate players with different cases', async ({ request }) => {
|
|
// Create a CSV with the same player name in different cases
|
|
const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points
|
|
1,1,1,Dedupe Player,Dedupe Player2,5,Dedupe player,Dedupe PLAYER2,3`;
|
|
|
|
// Create a temporary CSV file
|
|
const csvPath = path.join(__dirname, 'temp-deduplication-test.csv');
|
|
fs.writeFileSync(csvPath, csvContent);
|
|
|
|
try {
|
|
// Upload the CSV
|
|
const csvFileContent = fs.readFileSync(csvPath, 'utf-8');
|
|
const blob = new Blob([csvFileContent], { type: 'text/csv' });
|
|
const file = new File([blob], 'deduplication-test.csv', { type: 'text/csv' });
|
|
|
|
const formData = new FormData();
|
|
formData.append('csvFile', file);
|
|
formData.append('eventId', testTournamentId.toString());
|
|
|
|
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
|
|
// Check that only 4 unique players were created (not 8)
|
|
const dedupePlayers = await prisma.player.findMany({
|
|
where: {
|
|
name: {
|
|
contains: 'Dedupe',
|
|
},
|
|
},
|
|
});
|
|
|
|
// We expect 4 unique players: "Dedupe Player", "Dedupe Player2", "Dedupe player", "Dedupe PLAYER2"
|
|
// But due to normalization, "Dedupe player" should match "Dedupe Player" (case-insensitive)
|
|
// and "Dedupe PLAYER2" should match "Dedupe Player2"
|
|
// So we should have exactly 2 unique players after deduplication
|
|
expect(dedupePlayers.length).toBe(2);
|
|
|
|
// Verify the players have the correct names (original case preserved)
|
|
const playerNames = dedupePlayers.map((p: { name: string }) => p.name).sort();
|
|
expect(playerNames).toEqual(['Dedupe Player', 'Dedupe Player2']);
|
|
|
|
// Verify each player has games played
|
|
dedupePlayers.forEach((player: { gamesPlayed: number }) => {
|
|
expect(player.gamesPlayed).toBeGreaterThan(0);
|
|
});
|
|
} finally {
|
|
// Clean up temp file
|
|
if (fs.existsSync(csvPath)) {
|
|
fs.unlinkSync(csvPath);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should handle whitespace variations correctly', async ({ request }) => {
|
|
// Create a CSV with whitespace variations
|
|
const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points
|
|
1,1,1, Whitespace Player , Whitespace Player2 ,5,Whitespace Player, WhitespacePlayer2,3`;
|
|
|
|
const csvPath = path.join(__dirname, 'temp-whitespace-test.csv');
|
|
fs.writeFileSync(csvPath, csvContent);
|
|
|
|
try {
|
|
const csvFileContent = fs.readFileSync(csvPath, 'utf-8');
|
|
const blob = new Blob([csvFileContent], { type: 'text/csv' });
|
|
const file = new File([blob], 'whitespace-test.csv', { type: 'text/csv' });
|
|
|
|
const formData = new FormData();
|
|
formData.append('csvFile', file);
|
|
formData.append('eventId', testTournamentId.toString());
|
|
|
|
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
|
|
// Check that players were created without extra whitespace
|
|
const whitespacePlayers = await prisma.player.findMany({
|
|
where: {
|
|
name: {
|
|
contains: 'Whitespace',
|
|
},
|
|
},
|
|
});
|
|
|
|
// Verify no players have leading/trailing whitespace
|
|
whitespacePlayers.forEach((player: { name: string, normalizedName: string }) => {
|
|
expect(player.name).toBe(player.name.trim());
|
|
expect(player.normalizedName).toBe(player.name.trim().toLowerCase());
|
|
});
|
|
} finally {
|
|
if (fs.existsSync(csvPath)) {
|
|
fs.unlinkSync(csvPath);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should aggregate stats for duplicate players', async ({ request }) => {
|
|
// First, create some players manually to simulate previous uploads
|
|
const player1 = await prisma.player.create({
|
|
data: {
|
|
name: 'Aggregate Test',
|
|
normalizedName: 'aggregate test',
|
|
currentElo: 1050,
|
|
gamesPlayed: 5,
|
|
wins: 3,
|
|
losses: 2,
|
|
},
|
|
});
|
|
testPlayerIds.push(player1.id);
|
|
|
|
// Upload a CSV with the same player name
|
|
const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points
|
|
1,1,1,Aggregate Test,Test Player 1,5,Test Player 2,Test Player 3,3`;
|
|
|
|
const csvPath = path.join(__dirname, 'temp-aggregate-test.csv');
|
|
fs.writeFileSync(csvPath, csvContent);
|
|
|
|
try {
|
|
const csvFileContent = fs.readFileSync(csvPath, 'utf-8');
|
|
const blob = new Blob([csvFileContent], { type: 'text/csv' });
|
|
const file = new File([blob], 'aggregate-test.csv', { type: 'text/csv' });
|
|
|
|
const formData = new FormData();
|
|
formData.append('csvFile', file);
|
|
formData.append('eventId', testTournamentId.toString());
|
|
|
|
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
|
|
// Check that the existing player was updated (not duplicated)
|
|
const aggregatePlayers = await prisma.player.findMany({
|
|
where: {
|
|
name: 'Aggregate Test',
|
|
},
|
|
});
|
|
|
|
// Should still be only one player
|
|
expect(aggregatePlayers.length).toBe(1);
|
|
|
|
// Stats should be updated (1 additional game played, 1 additional win)
|
|
const player = aggregatePlayers[0];
|
|
expect(player.gamesPlayed).toBe(6); // 5 + 1
|
|
expect(player.wins).toBe(4); // 3 + 1
|
|
expect(player.losses).toBe(2); // unchanged (team lost)
|
|
} finally {
|
|
if (fs.existsSync(csvPath)) {
|
|
fs.unlinkSync(csvPath);
|
|
}
|
|
}
|
|
});
|
|
});
|