diff --git a/e2e/global.setup.ts b/e2e/global.setup.ts index 5682b7f..f7aea99 100644 --- a/e2e/global.setup.ts +++ b/e2e/global.setup.ts @@ -4,40 +4,27 @@ */ import { chromium, type FullConfig } from '@playwright/test'; -import { prisma } from '@/lib/prisma'; -import { cleanupAllTestData } from '@/__tests__/test-utils'; +import { PrismaClient } from '@prisma/client'; import path from 'path'; import fs from 'fs'; -// Load .env file first, then .env.development (which will override .env) -const envPath = path.resolve(process.cwd(), '.env'); -const envDevPath = path.resolve(process.cwd(), '.env.development'); - -// Load base .env file -if (fs.existsSync(envPath)) { - require('dotenv').config({ path: envPath }); -} - -// Load .env.development file (will override .env settings) -if (fs.existsSync(envDevPath)) { - require('dotenv').config({ path: envDevPath, override: true }); -} - 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'); +function isDatabase(url: string, name: string): boolean { + return url.includes(name); } function isProductionDatabase(): boolean { const dbUrl = process.env.DATABASE_URL || ''; - return dbUrl.includes('euchre_camp') && !dbUrl.includes('_dev'); + return isDatabase(dbUrl, 'euchre_camp') && !isDatabase(dbUrl, '_dev') && !isDatabase(dbUrl, '_ci'); +} + +function isCIDatabase(): boolean { + const dbUrl = process.env.DATABASE_URL || ''; + return isDatabase(dbUrl, '_ci'); } -// Strict check - fail if using production database if (isProductionDatabase()) { console.error(''); console.error('='.repeat(80)); @@ -46,59 +33,65 @@ if (isProductionDatabase()) { console.error(''); console.error('Current DATABASE_URL:', process.env.DATABASE_URL); console.error(''); - console.error('Tests MUST run against the development database (euchre_camp_dev)'); - console.error(''); - console.error('To fix this:'); - console.error(' 1. Run: npm run test:acceptance'); - console.error(' 2. Or set: DATABASE_URL environment variable to dev database URL'); - console.error(' 3. Or load .env.development: source .env.development && npm run test:acceptance'); + console.error('Tests MUST run against development (euchre_camp_dev) or CI (euchre_camp_ci)'); console.error(''); console.error('Aborting test execution to prevent data corruption.'); console.error(''); process.exit(1); } -if (!isDevDatabase()) { - console.warn('⚠️ WARNING: DATABASE_URL does not contain euchre_camp_dev'); - console.warn(' Current DATABASE_URL:', process.env.DATABASE_URL); +async function resetDatabaseSchema(prisma: PrismaClient) { + console.log('Resetting database schema...'); + + const tables = await prisma.$queryRaw<{ tablename: string }[]>` + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' AND tablename NOT LIKE '_prisma_migrations' + `; + + if (tables.length > 0) { + await prisma.$executeRawUnsafe(` + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + GRANT ALL ON SCHEMA public TO postgres; + GRANT ALL ON SCHEMA public TO public; + `); + console.log(`Dropped ${tables.length} tables`); + } + + console.log('Running migrations...'); + const { execSync } = await import('child_process'); + execSync('bunx prisma migrate deploy', { stdio: 'inherit' }); } -export default async function globalSetup(config: FullConfig) { +async function createTestUsers(config: FullConfig) { const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000'; const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); - // Log all responses for debugging page.on('response', response => { if (response.url().includes('/api/auth')) { console.log('API Response:', response.status(), response.url()); } }); - // Generate unique test credentials const timestamp = Date.now(); const testEmail = `setup-user-${timestamp}@example.com`; const testPassword = 'TestPassword1234!'; const testName = 'Setup User'; try { - // Navigate to registration page console.log('Navigating to registration page...'); await page.goto(`${baseURL}/auth/register`); - // Fill in registration form await page.fill('input[name="name"]', testName); await page.fill('input[name="email"]', testEmail); await page.fill('input[name="password"]', testPassword); - // Submit the form console.log('Submitting registration form...'); await page.click('button[type="submit"]'); - // Wait for the sign-up API call to complete - console.log('Waiting for sign-up API call...'); try { await page.waitForResponse(response => response.url().includes('/api/auth/sign-up/email') && response.status() === 200, @@ -109,40 +102,25 @@ export default async function globalSetup(config: FullConfig) { console.log('Sign-up API call failed or timed out'); } - // Wait a bit for session to be established - console.log('Waiting for session establishment...'); await page.waitForTimeout(2000); - - // Check if we're already authenticated - const currentUrl = page.url(); - console.log('Current URL after registration:', currentUrl); - - // Save the authentication state await context.storageState({ path: authFile }); - console.log(`Created and authenticated test user: ${testEmail}`); - // Now create admin user const adminTimestamp = timestamp + 1; const adminEmail = `setup-admin-${adminTimestamp}@example.com`; const adminPassword = 'AdminPassword123!'; const adminName = 'Setup Admin'; - // Navigate to registration page again console.log('Navigating to registration page for admin...'); await page.goto(`${baseURL}/auth/register`); - // Fill in registration form await page.fill('input[name="name"]', adminName); await page.fill('input[name="email"]', adminEmail); await page.fill('input[name="password"]', adminPassword); - // Submit the form console.log('Submitting admin registration form...'); await page.click('button[type="submit"]'); - // Wait for the sign-up API call to complete - console.log('Waiting for admin sign-up API call...'); try { await page.waitForResponse(response => response.url().includes('/api/auth/sign-up/email') && response.status() === 200, @@ -153,14 +131,10 @@ export default async function globalSetup(config: FullConfig) { console.log('Admin sign-up API call failed or timed out'); } - // Wait a bit for session to be established - console.log('Waiting for admin session establishment...'); await page.waitForTimeout(2000); - // Update user role to admin via database - const user = await prisma.user.findUnique({ - where: { email: adminEmail } - }); + const prisma = new PrismaClient(); + const user = await prisma.user.findUnique({ where: { email: adminEmail } }); if (user) { await prisma.user.update({ @@ -169,44 +143,39 @@ export default async function globalSetup(config: FullConfig) { }); console.log('Updated user role to club_admin'); } + await prisma.$disconnect(); - // Navigate to admin page to refresh session console.log('Navigating to admin page for admin user...'); await page.goto(`${baseURL}/admin`); await page.waitForLoadState('domcontentloaded'); console.log('Admin page loaded:', page.url()); - // Wait a bit to ensure session is refreshed await page.waitForTimeout(2000); - - // Refresh the page to force session reload await page.reload(); await page.waitForLoadState('domcontentloaded'); console.log('Page reloaded'); - // Save the authentication state await context.storageState({ path: adminAuthFile }); - console.log(`Created and authenticated admin user: ${adminEmail}`); - } catch (error) { - console.error('Global setup error:', error); - throw error; } finally { await browser.close(); } - - // Return teardown function - return async () => { - console.log('\n=== Global Teardown ==='); - - // Clean up all test data - try { - await cleanupAllTestData(); - } catch (error) { - console.error('Error cleaning up test data:', error); - } finally { - await prisma.$disconnect(); - } - }; } + +export default async function globalSetup(config: FullConfig) { + console.log('=== Global Setup ==='); + console.log('DATABASE_URL:', process.env.DATABASE_URL); + + if (isCIDatabase()) { + console.log('CI environment detected - will reset database schema'); + const prisma = new PrismaClient(); + await resetDatabaseSchema(prisma); + await prisma.$disconnect(); + } else if (!isProductionDatabase()) { + console.log('Development environment - preserving existing data'); + } + + await createTestUsers(config); + console.log('=== Global Setup Complete ===\n'); +} \ No newline at end of file diff --git a/e2e/global.teardown.ts b/e2e/global.teardown.ts new file mode 100644 index 0000000..b5e41f9 --- /dev/null +++ b/e2e/global.teardown.ts @@ -0,0 +1,126 @@ +/** + * Global teardown for Playwright tests + * Handles cleanup based on environment: + * - CI: Full schema reset (database can be destroyed and recreated) + * - Dev: Selective cleanup of test records (preserve real data) + * - Prod: Selective cleanup of test records (preserve real data) + */ + +import { type FullConfig } from '@playwright/test'; +import { PrismaClient } from '@prisma/client'; +import { execSync } from 'child_process'; + +const TEST_PATTERNS = { + players: [ + '%Test%', + '%Setup%', + '%Home Test%', + '%Home Match Player%', + '%Admin User%', + '%NinePart%', + '%Nine Part%', + '%Test Player%', + '%TestUser%', + '%Cucumber%', + '%Config Admin%', + ], + events: [ + '%Test%', + '%Setup%', + '%Recent%', + '%Test Tournament%', + '%Cucumber%', + ], + users: [ + '%test%', + '%setup%', + '%cucumber%', + '%TestUser%', + ] +}; + +function isDatabase(url: string, name: string): boolean { + return url.includes(name); +} + +function isProductionDatabase(): boolean { + const dbUrl = process.env.DATABASE_URL || ''; + return isDatabase(dbUrl, 'euchre_camp') && !isDatabase(dbUrl, '_dev') && !isDatabase(dbUrl, '_ci'); +} + +function isCIDatabase(): boolean { + const dbUrl = process.env.DATABASE_URL || ''; + return isDatabase(dbUrl, '_ci'); +} + +function buildLikeClause(patterns: string[]): string { + return patterns.map(p => `name LIKE '${p}'`).join(' OR '); +} + +function buildEmailLikeClause(patterns: string[]): string { + return patterns.map(p => `email LIKE '${p}'`).join(' OR '); +} + +async function resetDatabaseSchema(prisma: PrismaClient) { + console.log('Resetting database schema...'); + + const tables = await prisma.$queryRaw<{ tablename: string }[]>` + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' AND tablename NOT LIKE '_prisma_migrations' + `; + + if (tables.length > 0) { + await prisma.$executeRawUnsafe(` + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + GRANT ALL ON SCHEMA public TO postgres; + GRANT ALL ON SCHEMA public TO public; + `); + console.log(`Dropped ${tables.length} tables`); + } + + console.log('Running migrations...'); + execSync('bunx prisma migrate deploy', { stdio: 'inherit' }); +} + +async function cleanupTestRecords(prisma: PrismaClient) { + console.log('Cleaning up test records...'); + + const playerWhere = buildLikeClause(TEST_PATTERNS.players); + const eventWhere = buildLikeClause(TEST_PATTERNS.events); + const userWhere = buildEmailLikeClause(TEST_PATTERNS.users); + + await prisma.$executeRawUnsafe(`DELETE FROM events WHERE (${eventWhere});`); + console.log('Deleted test events'); + + await prisma.$executeRawUnsafe(`DELETE FROM players WHERE (${playerWhere});`); + console.log('Deleted test players'); + + await prisma.$executeRawUnsafe(`DELETE FROM users WHERE (${userWhere}) AND "playerId" IS NULL;`); + console.log('Deleted test users (without player associations)'); + + await prisma.$disconnect(); +} + +export default async function globalTeardown(config: FullConfig) { + console.log('\n=== Global Teardown ==='); + + const dbUrl = process.env.DATABASE_URL || ''; + + if (isCIDatabase()) { + console.log('CI environment - resetting database schema'); + const prisma = new PrismaClient(); + await resetDatabaseSchema(prisma); + await prisma.$disconnect(); + } else if (isProductionDatabase()) { + console.log('Production environment - selective cleanup of test records'); + const prisma = new PrismaClient(); + await cleanupTestRecords(prisma); + } else { + console.log('Development environment - selective cleanup of test records'); + const prisma = new PrismaClient(); + await cleanupTestRecords(prisma); + } + + console.log('=== Global Teardown Complete ===\n'); +} \ No newline at end of file