From 26c5158724b7bdd1feb024bac1f42ed4fe71e97a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 16:52:56 -0700 Subject: [PATCH] feat: set up development database and test cleanup utilities --- package.json | 3 + scripts/setup-postgres.js | 27 +++ scripts/use-dev-db.js | 49 +++++ src/__tests__/e2e/global.setup.ts | 29 ++- src/__tests__/e2e/home-page.test.ts | 30 +-- src/__tests__/test-utils.ts | 274 ++++++++++++++++++++++++++++ 6 files changed, 389 insertions(+), 23 deletions(-) create mode 100755 scripts/use-dev-db.js create mode 100644 src/__tests__/test-utils.ts diff --git a/package.json b/package.json index 49094a2..efb1376 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "test:acceptance:headed": "playwright test src/__tests__/e2e/ --headed", "db:switch": "node scripts/switch-database.js", "db:setup-postgres": "node scripts/setup-postgres.js", + "db:setup-dev": "DATABASE_URL=\"postgresql://euchre_camp:LINGO5row_hiding@dhg.lol:5432/euchre_camp_dev\" node scripts/setup-postgres.js", + "db:setup-dev:clean": "DATABASE_URL=\"postgresql://euchre_camp:LINGO5row_hiding@dhg.lol:5432/euchre_camp_dev\" node scripts/setup-postgres.js --drop", + "db:use-dev": "node scripts/use-dev-db.js", "db:seed": "node scripts/seed.js", "docker:up": "docker-compose up -d", "docker:down": "docker-compose down", diff --git a/scripts/setup-postgres.js b/scripts/setup-postgres.js index 9623c37..3e2a680 100755 --- a/scripts/setup-postgres.js +++ b/scripts/setup-postgres.js @@ -51,6 +51,22 @@ function createDatabase() { } } +function dropDatabase() { + try { + const dbUrl = process.env.DATABASE_URL; + const dbName = dbUrl.split('/').pop(); + + // Drop existing connections and database + execSync(`psql "${dbUrl}" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();" 2>/dev/null || true`, { stdio: 'pipe' }); + execSync(`psql "${dbUrl}" -c "DROP DATABASE IF EXISTS ${dbName};" 2>/dev/null || true`, { stdio: 'pipe' }); + console.log(`โœ… Database "${dbName}" dropped`); + return true; + } catch (error) { + console.error('โŒ Failed to drop database:', error.message); + return false; + } +} + function runMigrations() { try { console.log('๐Ÿ”„ Running migrations...'); @@ -76,6 +92,9 @@ function generatePrismaClient() { } function main() { + const args = process.argv.slice(2); + const shouldDrop = args.includes('--drop'); + console.log('=== PostgreSQL Setup for EuchreCamp ===\n'); // Check if DATABASE_URL is set @@ -90,6 +109,14 @@ function main() { process.exit(1); } + // Drop database if requested + if (shouldDrop) { + console.log('Dropping existing database...'); + if (!dropDatabase()) { + process.exit(1); + } + } + // Create database if (!createDatabase()) { process.exit(1); diff --git a/scripts/use-dev-db.js b/scripts/use-dev-db.js new file mode 100755 index 0000000..30f46a3 --- /dev/null +++ b/scripts/use-dev-db.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +/** + * Switch to development database + * Creates a .env.development.local file with development database settings + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const envDevPath = path.join(__dirname, '..', '.env.development'); +const envDevLocalPath = path.join(__dirname, '..', '.env.development.local'); + +console.log('๐Ÿ”ง Setting up development database...\n'); + +// Check if .env.development exists +if (!fs.existsSync(envDevPath)) { + console.error('โŒ .env.development file not found'); + console.error('Please create it first or run: npm run db:setup-dev'); + process.exit(1); +} + +// Copy .env.development to .env.development.local if it doesn't exist +if (!fs.existsSync(envDevLocalPath)) { + fs.copyFileSync(envDevPath, envDevLocalPath); + console.log('โœ… Created .env.development.local'); +} else { + console.log('โ„น๏ธ .env.development.local already exists'); +} + +// Set NODE_ENV for the current session +process.env.NODE_ENV = 'development'; +process.env.DATABASE_PROVIDER = 'postgresql'; + +// Read the development database URL +const envContent = fs.readFileSync(envDevPath, 'utf8'); +const match = envContent.match(/DATABASE_URL="([^"]+)"/); +if (match) { + process.env.DATABASE_URL = match[1]; + console.log(`โœ… Development database URL: ${process.env.DATABASE_URL}`); +} + +console.log('\nโœ… Development database configured!'); +console.log('\nNext steps:'); +console.log('1. Setup the dev database: npm run db:setup-dev'); +console.log('2. Start development server: npm run dev'); +console.log('\nNote: This script sets environment variables for the current session.'); +console.log('For persistent configuration, use .env.development.local'); diff --git a/src/__tests__/e2e/global.setup.ts b/src/__tests__/e2e/global.setup.ts index 8205bbc..89ab771 100644 --- a/src/__tests__/e2e/global.setup.ts +++ b/src/__tests__/e2e/global.setup.ts @@ -5,10 +5,24 @@ import { chromium, type FullConfig } from '@playwright/test'; import { prisma } from '@/lib/prisma'; +import { cleanupAllTestData } from '@/__tests__/test-utils'; const authFile = 'playwright/.auth/user.json'; const adminAuthFile = 'playwright/.auth/admin.json'; +// Check if we're using the dev database +function isDevDatabase(): boolean { + const dbUrl = process.env.DATABASE_URL || ''; + return dbUrl.includes('euchre_camp_dev'); +} + +// Warn if not using dev database +if (!isDevDatabase()) { + console.warn('โš ๏ธ WARNING: Not using dev database!'); + console.warn(' Current DATABASE_URL:', process.env.DATABASE_URL); + console.warn(' Expected to contain: euchre_camp_dev'); +} + export default async function globalSetup(config: FullConfig) { const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000'; @@ -144,18 +158,13 @@ export default async function globalSetup(config: FullConfig) { // Return teardown function return async () => { - // Clean up test users + console.log('\n=== Global Teardown ==='); + + // Clean up all test data try { - await prisma.user.deleteMany({ - where: { - email: { - startsWith: 'setup-' - } - } - }); - console.log('Cleaned up test users'); + await cleanupAllTestData(); } catch (error) { - console.error('Error cleaning up test users:', error); + console.error('Error cleaning up test data:', error); } finally { await prisma.$disconnect(); } diff --git a/src/__tests__/e2e/home-page.test.ts b/src/__tests__/e2e/home-page.test.ts index ffe157f..e4ec32f 100644 --- a/src/__tests__/e2e/home-page.test.ts +++ b/src/__tests__/e2e/home-page.test.ts @@ -1,22 +1,27 @@ import { test, expect } from '@playwright/test' import { prisma } from '@/lib/prisma' +import { createTestPlayer, cleanupTestRecords, getCreatedRecordCounts } from '@/__tests__/test-utils' test.describe('Home Page', () => { + test.beforeEach(async () => { + // Clean up any existing test records before each test + await cleanupTestRecords(); + }); + + test.afterEach(async () => { + // Clean up test records after each test + await cleanupTestRecords(); + }); + test('should display top 10 players', async ({ page }) => { // Create some test players with unique names and very high Elo to ensure they're in top 10 const timestamp = Date.now() const players = [] for (let i = 0; i < 3; i++) { const playerName = `Home Test Player ${timestamp} ${i + 1}` - const player = await prisma.player.create({ - data: { - name: playerName, - normalizedName: playerName.toLowerCase(), - currentElo: 2000 - i * 10, // Very high Elo to ensure they're in top 10 - gamesPlayed: 10 + i, - wins: 5 + Math.floor(i / 2), - losses: 5 + Math.ceil(i / 2), - }, + const player = await createTestPlayer({ + name: playerName, + currentElo: 2000 - i * 10, }) players.push(player) } @@ -33,10 +38,9 @@ test.describe('Home Page', () => { page.locator(`a:has-text("Home Test Player ${timestamp} 1")`) ).toBeVisible() - // Clean up - for (const player of players) { - await prisma.player.delete({ where: { id: player.id } }) - } + // Verify cleanup will work + const counts = getCreatedRecordCounts(); + expect(counts.players).toBe(3); }) test('should display club president', async ({ page }) => { diff --git a/src/__tests__/test-utils.ts b/src/__tests__/test-utils.ts new file mode 100644 index 0000000..a0e2155 --- /dev/null +++ b/src/__tests__/test-utils.ts @@ -0,0 +1,274 @@ +/** + * Test Utilities for EuchreCamp + * + * Provides helper functions for creating and cleaning up test data + */ + +import { prisma } from '@/lib/prisma'; +import type { User, Player, Event, Match } from '@prisma/client'; + +// Track created test records for cleanup +const createdRecords = { + users: [] as string[], + players: [] as number[], + events: [] as number[], + matches: [] as number[], +}; + +/** + * Create a test user with optional player association + */ +export async function createTestUser(options: { + email?: string; + name?: string; + role?: string; + playerId?: number | null; +} = {}) { + const timestamp = Date.now(); + const email = options.email || `test-user-${timestamp}@example.com`; + const name = options.name || `Test User ${timestamp}`; + const role = options.role || 'player'; + + const user = await prisma.user.create({ + data: { + email, + name, + role, + playerId: options.playerId, + }, + }); + + createdRecords.users.push(user.id); + return user; +} + +/** + * Create a test player + */ +export async function createTestPlayer(options: { + name?: string; + currentElo?: number; +} = {}) { + const timestamp = Date.now(); + const name = options.name || `Test Player ${timestamp}`; + + const player = await prisma.player.create({ + data: { + name, + normalizedName: name.toLowerCase(), + currentElo: options.currentElo || 1000, + }, + }); + + createdRecords.players.push(player.id); + return player; +} + +/** + * Create a test event (tournament) + */ +export async function createTestEvent(options: { + name?: string; + ownerId?: string; +} = {}) { + const timestamp = Date.now(); + const name = options.name || `Test Event ${timestamp}`; + + const event = await prisma.event.create({ + data: { + name, + eventType: 'tournament', + format: 'round_robin', + status: 'planned', + ownerId: options.ownerId, + }, + }); + + createdRecords.events.push(event.id); + return event; +} + +/** + * Create a test match + */ +export async function createTestMatch(options: { + eventId?: number; + team1P1Id: number; + team1P2Id: number; + team2P1Id: number; + team2P2Id: number; + team1Score?: number; + team2Score?: number; +}) { + const timestamp = Date.now(); + + const match = await prisma.match.create({ + data: { + eventId: options.eventId, + team1P1Id: options.team1P1Id, + team1P2Id: options.team1P2Id, + team2P1Id: options.team2P1Id, + team2P2Id: options.team2P2Id, + team1Score: options.team1Score ?? 10, + team2Score: options.team2Score ?? 5, + status: 'completed', + }, + }); + + createdRecords.matches.push(match.id); + return match; +} + +/** + * Clean up all created test records + */ +export async function cleanupTestRecords() { + console.log('๐Ÿงน Cleaning up test records...'); + + try { + // Delete matches first (they reference players) + if (createdRecords.matches.length > 0) { + await prisma.match.deleteMany({ + where: { id: { in: createdRecords.matches } }, + }); + console.log(` Deleted ${createdRecords.matches.length} matches`); + } + + // Delete events + if (createdRecords.events.length > 0) { + await prisma.event.deleteMany({ + where: { id: { in: createdRecords.events } }, + }); + console.log(` Deleted ${createdRecords.events.length} events`); + } + + // Delete users first (to break player associations) + if (createdRecords.users.length > 0) { + await prisma.user.deleteMany({ + where: { id: { in: createdRecords.users } }, + }); + console.log(` Deleted ${createdRecords.users.length} users`); + } + + // Delete players + if (createdRecords.players.length > 0) { + await prisma.player.deleteMany({ + where: { id: { in: createdRecords.players } }, + }); + console.log(` Deleted ${createdRecords.players.length} players`); + } + + // Reset tracking + createdRecords.users = []; + createdRecords.players = []; + createdRecords.events = []; + createdRecords.matches = []; + + console.log('โœ… Test records cleaned up successfully'); + } catch (error) { + console.error('โŒ Error cleaning up test records:', error); + // Still reset tracking to avoid trying to delete again + createdRecords.users = []; + createdRecords.players = []; + createdRecords.events = []; + createdRecords.matches = []; + } +} + +/** + * Get count of created records (for assertions) + */ +export function getCreatedRecordCounts() { + return { + users: createdRecords.users.length, + players: createdRecords.players.length, + events: createdRecords.events.length, + matches: createdRecords.matches.length, + }; +} + +/** + * Clean up test users by email pattern + */ +export async function cleanupTestUsersByEmailPattern(pattern: string = 'test-') { + try { + const result = await prisma.user.deleteMany({ + where: { + email: { + contains: pattern, + }, + }, + }); + console.log(` Deleted ${result.count} test users matching pattern "${pattern}"`); + return result.count; + } catch (error) { + console.error('โŒ Error cleaning up test users:', error); + return 0; + } +} + +/** + * Clean up test players by name pattern + */ +export async function cleanupTestPlayersByNamePattern(pattern: string = 'Test ') { + try { + const result = await prisma.player.deleteMany({ + where: { + name: { + contains: pattern, + }, + }, + }); + console.log(` Deleted ${result.count} test players matching pattern "${pattern}"`); + return result.count; + } catch (error) { + console.error('โŒ Error cleaning up test players:', error); + return 0; + } +} + +/** + * Clean up all test data (aggressive cleanup) + */ +export async function cleanupAllTestData() { + console.log('๐Ÿงน Aggressive cleanup of all test data...'); + + try { + // Delete test users first + const userResult = await prisma.user.deleteMany({ + where: { + email: { + contains: 'test-', + }, + }, + }); + console.log(` Deleted ${userResult.count} test users`); + + // Delete test players + const playerResult = await prisma.player.deleteMany({ + where: { + name: { + contains: 'Test ', + }, + }, + }); + console.log(` Deleted ${playerResult.count} test players`); + + // Delete test events + const eventResult = await prisma.event.deleteMany({ + where: { + name: { + contains: 'Test ', + }, + }, + }); + console.log(` Deleted ${eventResult.count} test events`); + + console.log('โœ… All test data cleaned up'); + } catch (error) { + console.error('โŒ Error during aggressive cleanup:', error); + } +} + +// Export cleanup function for global setup/teardown +export const testCleanup = cleanupTestRecords;