From 861e14503be463e665b80cd34ed192ba87e46794 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 11 May 2026 06:40:05 +0000 Subject: [PATCH] feat: SDLC database separation for CI/testing (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix `isProductionDatabase()` to allow CI database (`euchre_camp_ci`) - Add database schema reset before CI test runs - Create `global.teardown.ts` for cleanup (CI: full reset, dev/prod: selective cleanup) - Add `acceptance-tests` job to PR workflow with CI database - Create `sync-prod-to-dev.js` script for one-way prod→dev sync - Add `just` recipes: `sync-dev`, `test-prod`, `reset-ci-db` - Store credentials in `.credentials` (gitignored) with unique CI user ## Testing Verified against CI database: - Schema reset works - Migrations apply correctly - Test users created successfully - 219 tests pass (slow but working) ## Next Steps - Set `CI_DATABASE_URL` as Gitea repository variable Reviewed-on: https://git.notsosm.art/david/euchre_camp/pulls/35 Co-authored-by: David Gwilliam Co-committed-by: David Gwilliam --- .env.example | 58 ++++----- .gitea/workflows/build-ci-images.yml | 39 +++--- .gitea/workflows/deploy-prod.yml | 52 ++++++++ .gitea/workflows/pr.yml | 67 +++++++++-- .gitea/workflows/release.yml | 46 ++++---- .gitignore | 3 + docs/TODO.md | 32 +++-- e2e/epic3-rankings.test.ts | 47 ++++++++ e2e/global.setup.ts | 141 +++++++++------------- e2e/global.teardown.ts | 132 +++++++++++++++++++++ e2e/home-page.test.ts | 110 +++++++++++++++++ justfile | 69 +++++++++++ playwright.config.ts | 13 +- scripts/sync-prod-to-dev.js | 157 +++++++++++++++++++++++++ src/__tests__/Navigation.test.tsx | 21 +++- src/__tests__/unit/permissions.test.ts | 1 - 16 files changed, 792 insertions(+), 196 deletions(-) create mode 100644 .gitea/workflows/deploy-prod.yml create mode 100644 e2e/epic3-rankings.test.ts create mode 100644 e2e/global.teardown.ts create mode 100644 e2e/home-page.test.ts create mode 100644 scripts/sync-prod-to-dev.js diff --git a/.env.example b/.env.example index 241629c..3bb74b5 100644 --- a/.env.example +++ b/.env.example @@ -1,54 +1,48 @@ # EuchreCamp Environment Configuration -# Copy this file to .env and fill in your values +# ============================================ +# Copy this file to .env (for local dev) or use +# .env.development / .env.ci for specific environments # ============================================ # Database Configuration # ============================================ -# PostgreSQL connection string -# Format: postgresql://username:password@host:port/database -DATABASE_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp +# IMPORTANT: Use the appropriate DATABASE_URL for your environment: +# +# - Development (ephemeral, synced from prod): euchre_camp_dev +# - CI/Testing (reset before each run): euchre_camp_ci +# - Production (DO NOT USE FOR TESTS): euchre_camp +# +# The .credentials file in the project root contains +# the actual connection strings - DO NOT commit .credentials -# Shadow database for Prisma migrations (optional for PostgreSQL) -DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp_shadow - -# Database provider (postgresql) DATABASE_PROVIDER=postgresql # ============================================ # Better Auth Configuration # ============================================ -# Secret key for session encryption (generate a strong random string) -# Run: openssl rand -base64 32 -BETTER_AUTH_SECRET=your-secret-key-change-in-production +# Generate a new secret with: openssl rand -base64 32 +BETTER_AUTH_SECRET=generate-new-secret-in-production -# Base URL for authentication callbacks -# For production: https://your-domain.com +# Base URL - update for production BETTER_AUTH_URL=http://localhost:3000 # ============================================ # Application Configuration # ============================================ -# Environment: development, production, test -NODE_ENV=production +NODE_ENV=development -# Trusted origins for CORS and authentication -# Add your domain(s) for production +# Comma-separated list of trusted origins TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 # ============================================ -# Optional: External Services +# Environment-Specific Overrides # ============================================ -# If using external database (e.g., Supabase, Railway) -# DATABASE_URL=postgresql://user:pass@host:port/db - -# If using external auth provider -# BETTER_AUTH_URL=https://your-app.com - -# ============================================ -# CasaOS Deployment Notes -# ============================================ -# When deploying to CasaOS, set these via the UI: -# 1. DATABASE_URL: Your PostgreSQL connection string -# 2. BETTER_AUTH_SECRET: Generate with: openssl rand -base64 32 -# 3. BETTER_AUTH_URL: Your app's public URL -# 4. TRUSTED_ORIGINS: Your app's public URL(s) +# For development (.env.development): +# DATABASE_URL from .credentials (euchre_camp_dev) +# NODE_ENV=development +# BETTER_AUTH_URL=http://localhost:3000 +# +# For CI (.env.ci): +# DATABASE_URL from .credentials (euchre_camp_ci) +# NODE_ENV=test +# BETTER_AUTH_URL=http://localhost:3000 \ No newline at end of file diff --git a/.gitea/workflows/build-ci-images.yml b/.gitea/workflows/build-ci-images.yml index 18b0484..394e164 100644 --- a/.gitea/workflows/build-ci-images.yml +++ b/.gitea/workflows/build-ci-images.yml @@ -5,14 +5,14 @@ on: branches: - main paths: - - 'Dockerfile.ci-base' - - 'package.json' - - 'bun.lock' - - '.gitea/workflows/build-ci-images.yml' + - "Dockerfile.ci-base" + - "package.json" + - "bun.lock" + - ".gitea/workflows/build-ci-images.yml" schedule: # Weekly rebuild to get latest Playwright/Bun versions - - cron: '0 2 * * 0' # Every Sunday at 2 AM - workflow_dispatch: # Manual trigger + - cron: "0 2 * * 0" # Every Sunday at 2 AM + workflow_dispatch: # Manual trigger env: REGISTRY: docker.notsosm.art @@ -24,32 +24,32 @@ jobs: permissions: contents: read packages: write - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Login to Registry run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.DOCKER_LOGIN }} --password-stdin - + - name: Extract metadata for CI base image id: meta run: | # Get Playwright version from package.json - PLAYWRIGHT_VERSION=$(grep -o '"@playwright/test": "[^"]*"' package.json | cut -d'"' -f4) + PLAYWRIGHT_VERSION=$(grep -o '"@playwright/test": "[^"]*"' package.json | cut -d'"' -f4 | sed 's/^\^//') echo "playwright_version=${PLAYWRIGHT_VERSION}" >> $GITHUB_OUTPUT - + # Get Bun version (latest) BUN_VERSION=$(bun --version 2>/dev/null || echo "latest") echo "bun_version=${BUN_VERSION}" >> $GITHUB_OUTPUT - + # Set tags echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:playwright-${PLAYWRIGHT_VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:${{ github.sha }}" >> $GITHUB_OUTPUT - + - name: Build and push CI base image run: | # Build with multiple tags @@ -58,14 +58,9 @@ jobs: --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:latest \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:playwright-${{ steps.meta.outputs.playwright_version }} \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:${{ github.sha }} \ - . - - # Push all tags - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:latest - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:playwright-${{ steps.meta.outputs.playwright_version }} - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:${{ github.sha }} - + - name: Clean up if: always() run: | - docker logout ${{ env.REGISTRY }} \ No newline at end of file + docker logout ${{ env.REGISTRY }} + diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..c6b6770 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,52 @@ +name: Deploy Production + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag to deploy (e.g., v0.1.21)' + required: true + type: string + +env: + REGISTRY: docker.notsosm.art + IMAGE_NAME: euchre-camp + PROD_APPS_PATH: /apps/youthful_simon + +jobs: + deploy-prod: + runs-on: ubuntu-latest + container: + image: docker.notsosm.art/euchre-camp/ci-base:latest + options: --user root + + steps: + - name: Deploy to production + run: | + VERSION="${{ inputs.version }}" + COMPOSE_FILE="${{ env.PROD_APPS_PATH }}/docker-compose.yml" + + echo "Deploying ${VERSION} to production..." + + # Update prod compose file with the release tag + # Note: Standardizing to docker.notsosm.art registry + sed -i "s|image: euchre-camp/euchre-camp:[a-zA-Z0-9.-]*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}|" ${COMPOSE_FILE} + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${VERSION}|" ${COMPOSE_FILE} + + # Pull and restart the prod container + cd ${{ env.PROD_APPS_PATH }} + docker compose pull app + docker compose up -d app + + # Wait for production site to be healthy + echo "Waiting for production site to be healthy..." + for i in {1..30}; do + if curl -sf https://euchre.notsosm.art/api/health > /dev/null 2>&1; then + echo "✅ Production successfully deployed with version ${VERSION}" + exit 0 + fi + sleep 0.5 + done + echo "❌ Production deployment failed" + docker compose logs app + exit 1 \ No newline at end of file diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index 6de9407..0096e89 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -5,6 +5,10 @@ on: branches: - main +env: + REGISTRY: docker.notsosm.art + IMAGE_NAME: euchre-camp + jobs: unit-tests: runs-on: ubuntu-latest @@ -16,9 +20,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Clear Bun cache - run: bun pm cache rm || true - - name: Install dependencies run: bun install @@ -30,7 +31,7 @@ jobs: - name: Run unit tests run: bun test src/__tests__/unit/ src/__tests__/*.test.tsx src/__tests__/auth-simple.test.ts - e2e-tests: + build-and-deploy-ci: runs-on: ubuntu-latest needs: unit-tests container: @@ -41,9 +42,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Clear Bun cache - run: bun pm cache rm || true - - name: Install dependencies run: bun install @@ -52,15 +50,60 @@ jobs: env: DATABASE_URL: postgresql://user:pass@localhost:5432/dummy - - name: Run E2E tests - run: npm run test:acceptance:cucumber:prod + - name: Extract PR number and commit info + id: info + run: | + echo "pr_number=$(echo $GITHUB_REF | grep -oP 'refs/pull/\K[0-9]+')" >> $GITHUB_OUTPUT + echo "short_sha=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_OUTPUT + + - name: Build Docker image for PR + run: | + IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}" + docker build \ + --target runner \ + --build-arg GIT_COMMIT=$GITHUB_SHA \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \ + . + + - name: Update CI site compose and restart + run: | + IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}" + COMPOSE_FILE="/apps/euchre_camp_ci/docker-compose.yml" + + # Update the image tag in the compose file + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" ${COMPOSE_FILE} + + # Pull the new image and restart the CI stack + cd /apps/euchre_camp_ci + docker compose pull app + docker compose up -d app + + - name: Wait for CI site to be healthy + run: | + for i in {1..30}; do + if curl -sf https://euchre-ci.notsosm.art/api/health > /dev/null 2>&1; then + echo "CI site is healthy" + exit 0 + fi + sleep 0.5 + done + echo "CI site failed to become healthy after 15 seconds" + docker compose -f /apps/euchre_camp_ci/docker-compose.yml logs app + exit 1 + + - name: Run acceptance tests + run: DATABASE_URL="${{ secrets.CI_DATABASE_URL }}" bun test:acceptance env: - DATABASE_URL: postgresql://euchre_camp:${{ secrets.DB_PASSWORD }}@dhg.lol:5432/euchre_camp_dev - DATABASE_PROVIDER: postgresql + CI: true + + - name: Cleanup PR images + if: always() + run: | + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }} || true analyze-bump-type: runs-on: ubuntu-latest - needs: e2e-tests + needs: unit-tests steps: - name: Checkout code diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9394d39..6e906e5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -163,32 +163,26 @@ jobs: if: steps.commit.outputs.committed == 'true' run: | echo "Deploying version ${{ steps.version.outputs.new_version }} to dev environment..." - - # Update docker-compose.yml with new image tag using full registry path - # The registry is docker.notsosm.art and image is euchre-camp + + # Update dev compose file with new image tag IMAGE_TAG="${{ steps.version.outputs.new_version }}" - sed -i "s|image: docker.notsosm.art/euchre-camp:[0-9.]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" docker-compose.yml - - # Copy the updated docker-compose.yml to the deployment location - # The runners are on the same Docker server where the container is running - sudo mkdir -p /home/euchre_camp - sudo cp docker-compose.yml /home/euchre_camp/ - sudo chown -R euchre:euchre /home/euchre_camp - + COMPOSE_FILE="/apps/intelligent_silasak/docker-compose.yml" + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" ${COMPOSE_FILE} + # Pull and restart the dev container - cd /home/euchre_camp - docker-compose pull app - docker-compose up -d app - + cd /apps/intelligent_silasak + docker compose pull app + docker compose up -d app + # Wait for container to be healthy - echo "Waiting for container to start..." - sleep 10 - - # Check if container is running - if docker ps --filter "name=euchre-camp-app" --format "{{.Status}}" | grep -q "Up"; then - echo "✅ Dev environment successfully deployed with version ${{ steps.version.outputs.new_version }}" - else - echo "❌ Dev environment deployment failed" - docker-compose logs app - exit 1 - fi + echo "Waiting for dev site to be healthy..." + for i in {1..30}; do + if curl -sf https://euchre-dev.notsosm.art/api/health > /dev/null 2>&1; then + echo "✅ Dev environment successfully deployed with version ${{ steps.version.outputs.new_version }}" + exit 0 + fi + sleep 0.5 + done + echo "❌ Dev environment deployment failed" + docker compose logs app + exit 1 diff --git a/.gitignore b/.gitignore index 6a67180..cb831b1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ /playwright/.auth/ /test-results /cookies.txt +.env.test +prisma/ci.db # next.js /.next/ @@ -59,3 +61,4 @@ playwright-report/ cucumber-pretty .env.production +.credentials diff --git a/docs/TODO.md b/docs/TODO.md index f3cc8b3..9b59a30 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -24,11 +24,11 @@ - [x] Write TODO list to repository file - [x] Auto-create tournament when uploading matches without selecting one -### In Progress 🔄 -- [ ] Update API routes to handle new variant scoring fields -- [ ] Update EditTournamentForm to add variant scoring controls -- [ ] Update MatchEditor to use tournament-specific target score -- [ ] Run tests and verify variant scoring implementation +### Completed ✅ +- [x] Update API routes to handle new variant scoring fields +- [x] Update EditTournamentForm to add variant scoring controls +- [x] Update MatchEditor to use tournament-specific target score +- [x] Run tests and verify variant scoring implementation ### Recently Completed ✅ - [x] Update CI/CD workflows to use Bun (PR, release) @@ -59,11 +59,27 @@ - [x] Create migration to add rating system tables (elo_ratings, glicko2_ratings, open_skill_ratings) - [x] Add tabbed rankings page to display Elo, OpenSkill, and Glicko2 ratings +### Completed ✅ +- [x] Add UI controls for variant scoring in tournament creation/edit +- [x] Test variant tournament functionality end-to-end (e2e/tournament-edit-allowTies.test.ts) +- [x] Add validation for tie scores based on tournament configuration (MatchEditor.tsx) + +### Completed ✅ (CI/DB Infrastructure) +- [x] Fix PostgreSQL database ownership — each env owns its own DB +- [x] Fix role attributes — euchre_camp_dev gets CREATEDB, euchre_camp_ci loses SUPERUSER +- [x] Update .env.development to use euchre_camp_dev user +- [x] Update .env.development.local to use euchre_camp_dev user +- [x] Update CI docker-compose to use euchre_camp_ci user +- [x] Update dev docker-compose to use euchre_camp_dev user +- [x] Recreate dev and CI containers with correct credentials +- [x] Fix Playwright baseURL for CI (https://euchre-ci.notsosm.art) +- [x] Fix Navigation unit tests (RoleSwitcherProvider wrapper) +- [x] Fix secrets vs vars in PR workflow (secrets.CI_DATABASE_URL) + ### Backlog 📋 -- [ ] Add UI controls for variant scoring in tournament creation/edit -- [ ] Test variant tournament functionality end-to-end -- [ ] Add validation for tie scores based on tournament configuration - [ ] Document variant tournament features +- [ ] Update Gitea secret CI_DATABASE_URL to use euchre_camp_ci user +- [ ] Test isolation improvements for parallel CI execution ## Recently Completed (Detailed) diff --git a/e2e/epic3-rankings.test.ts b/e2e/epic3-rankings.test.ts new file mode 100644 index 0000000..d3d6afd --- /dev/null +++ b/e2e/epic3-rankings.test.ts @@ -0,0 +1,47 @@ +/** + * Epic 3: Rankings & Public Data + * Acceptance Test: Player Rankings Page + * + * User Story: As a visitor, I want to view player rankings so that I can see top players + * + * Acceptance Criteria: + * - Sortable rankings table + * - Columns: Rank, Name, Elo, Win Rate, Games Played + * - Search/filter functionality + * - Pagination + */ + +import { test, expect } from '@playwright/test'; + +test.describe('Epic 3: Rankings Page', () => { + test('Rankings page loads and displays rankings table', async ({ page }) => { + await page.goto('http://localhost:3000/rankings'); + + // Check page title or heading + await expect(page.locator('h1, h2')).toContainText(/rankings?/i); + + // Check for rankings table + await expect(page.locator('table')).toBeVisible(); + }); + + test('Rankings table displays player columns', async ({ page }) => { + await page.goto('http://localhost:3000/rankings'); + + // Check for expected column headers + const table = page.locator('table'); + await expect(table).toBeVisible(); + + // Check for column headers (may vary based on implementation) + const headerCount = await page.locator('th').count(); + expect(headerCount).toBeGreaterThan(0); + }); + + test('Rankings page is publicly accessible (no login required)', async ({ page }) => { + // Navigate directly to rankings without logging in + await page.goto('http://localhost:3000/rankings'); + + // Page should load without redirecting to login + await expect(page).toHaveURL(/.*rankings.*/); + await expect(page.locator('body')).toBeVisible(); + }); +}); diff --git a/e2e/global.setup.ts b/e2e/global.setup.ts index 5682b7f..42422a7 100644 --- a/e2e/global.setup.ts +++ b/e2e/global.setup.ts @@ -4,40 +4,28 @@ */ import { chromium, type FullConfig } from '@playwright/test'; -import { prisma } from '@/lib/prisma'; -import { cleanupAllTestData } from '@/__tests__/test-utils'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; 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 +34,70 @@ 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); +function createPrismaClient() { + const databaseUrl = process.env.DATABASE_URL; + if (!databaseUrl) throw new Error('DATABASE_URL is required'); + const adapter = new PrismaPg({ connectionString: databaseUrl }); + return new PrismaClient({ adapter }); } -export default async function globalSetup(config: FullConfig) { +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; + `); + console.log(`Dropped ${tables.length} tables`); + } + + console.log('Running migrations...'); + const { execSync } = await import('child_process'); + execSync('bunx prisma migrate deploy', { stdio: 'inherit' }); +} + +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 +108,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 +137,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 = createPrismaClient(); + const user = await prisma.user.findUnique({ where: { email: adminEmail } }); if (user) { await prisma.user.update({ @@ -169,44 +149,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 = createPrismaClient(); + 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..43c7949 --- /dev/null +++ b/e2e/global.teardown.ts @@ -0,0 +1,132 @@ +/** + * 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 { PrismaPg } from '@prisma/adapter-pg'; +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 '); +} + +function createPrismaClient() { + const databaseUrl = process.env.DATABASE_URL; + if (!databaseUrl) throw new Error('DATABASE_URL is required'); + const adapter = new PrismaPg({ connectionString: databaseUrl }); + return new PrismaClient({ adapter }); +} + +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; + `); + 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 = createPrismaClient(); + await resetDatabaseSchema(prisma); + await prisma.$disconnect(); + } else if (isProductionDatabase()) { + console.log('Production environment - selective cleanup of test records'); + const prisma = createPrismaClient(); + await cleanupTestRecords(prisma); + } else { + console.log('Development environment - selective cleanup of test records'); + const prisma = createPrismaClient(); + await cleanupTestRecords(prisma); + } + + console.log('=== Global Teardown Complete ===\n'); +} \ No newline at end of file diff --git a/e2e/home-page.test.ts b/e2e/home-page.test.ts new file mode 100644 index 0000000..13e2e42 --- /dev/null +++ b/e2e/home-page.test.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test' +import { prisma } from '@/lib/prisma' + +test.describe('Home Page', () => { + const createdIds = { + players: [] as number[], + events: [] as number[], + matches: [] as number[], + users: [] as string[], + } + + test.afterEach(async () => { + await prisma.match.deleteMany({ where: { id: { in: createdIds.matches } } }) + await prisma.event.deleteMany({ where: { id: { in: createdIds.events } } }) + await prisma.player.deleteMany({ id: { in: createdIds.players } }) + await prisma.user.deleteMany({ where: { id: { in: createdIds.users } } }) + createdIds.players = [] + createdIds.events = [] + createdIds.matches = [] + createdIds.users = [] + }) + + test('displays top 10 players section', async ({ page }) => { + const timestamp = Date.now() + + for (let i = 0; i < 3; i++) { + const player = await prisma.player.create({ + data: { + name: `Home Test Player ${timestamp} ${i + 1}`, + normalizedName: `home_test_player_${timestamp}_${i + 1}`.toLowerCase(), + currentElo: 2000 - i * 10, + gamesPlayed: 10, + wins: 7, + }, + }) + createdIds.players.push(player.id) + } + + await page.goto('/') + + await expect(page.locator('text=Top 10 Players')).toBeVisible() + await expect( + page.locator(`a:has-text("Home Test Player ${timestamp} 1")`) + ).toBeVisible() + }) + + test('displays club president section', async ({ page }) => { + const timestamp = Date.now() + const user = await prisma.user.create({ + data: { + email: `president-${timestamp}@example.com`, + name: `Club President ${timestamp}`, + role: 'club_admin', + }, + }) + createdIds.users.push(user.id) + + await page.goto('/') + + await expect(page.locator('text=Club President')).toBeVisible() + }) + + test('displays most recent tournament section', async ({ page }) => { + const timestamp = Date.now() + + const tournament = await prisma.event.create({ + data: { + name: `Recent Tournament ${timestamp}`, + eventType: 'tournament', + eventDate: new Date(Date.now() + 86400000), + status: 'completed', + }, + }) + createdIds.events.push(tournament.id) + + const p1 = await prisma.player.create({ + data: { name: `HP1 ${timestamp}`, normalizedName: `hp1_${timestamp}`.toLowerCase(), currentElo: 1500 }, + }) + const p2 = await prisma.player.create({ + data: { name: `HP2 ${timestamp}`, normalizedName: `hp2_${timestamp}`.toLowerCase(), currentElo: 1480 }, + }) + const p3 = await prisma.player.create({ + data: { name: `HP3 ${timestamp}`, normalizedName: `hp3_${timestamp}`.toLowerCase(), currentElo: 1450 }, + }) + const p4 = await prisma.player.create({ + data: { name: `HP4 ${timestamp}`, normalizedName: `hp4_${timestamp}`.toLowerCase(), currentElo: 1420 }, + }) + createdIds.players.push(p1.id, p2.id, p3.id, p4.id) + + const match = await prisma.match.create({ + data: { + eventId: tournament.id, + player1P1Id: p1.id, + player1P2Id: p2.id, + player2P1Id: p3.id, + player2P2Id: p4.id, + team1Score: 10, + team2Score: 5, + status: 'completed', + playedAt: new Date(), + }, + }) + createdIds.matches.push(match.id) + + await page.goto('/') + + await expect(page.locator('text=Most Recent Tournament')).toBeVisible() + await expect(page.locator(`text=Recent Tournament ${timestamp}`)).toBeVisible() + }) +}) \ No newline at end of file diff --git a/justfile b/justfile index e4cb07f..2c73d0c 100644 --- a/justfile +++ b/justfile @@ -239,3 +239,72 @@ workflow-status: @echo "PR Workflow: Runs unit + acceptance tests on pull requests" @echo "Test Workflow: Runs unit tests on all branch pushes" @echo "Release Workflow: Runs on main branch pushes (version bump + Docker build)" + +# --- Production Deployment --- + +# Deploy a specific version to production (human gate) +# Usage: just deploy-prod v0.1.21 +deploy-prod version: + @echo "Deploying {{version}} to production..." + @echo "This requires the image to already be pushed to the registry." + @echo "" + @read -p "Have you verified this version works in dev? (y/N) " confirm; \ + if [ "$$confirm" != "y" ]; then \ + echo "Cancelled. Please verify in dev first."; \ + exit 1; \ + fi + cd /apps/youthful_simon && \ + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:{{version}}|" docker-compose.yml && \ + sed -i "s|image: euchre-camp/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:{{version}}|" docker-compose.yml && \ + docker compose pull app && \ + docker compose up -d app && \ + echo "Waiting for production site to be healthy..." && \ + for i in {1..30}; do \ + if curl -sf https://euchre.notsosm.art/api/health > /dev/null 2>&1; then \ + echo "✅ Production successfully deployed with version {{version}}"; \ + exit 0; \ + fi; \ + sleep 0.5; \ + done && \ + echo "❌ Production deployment failed - health check timed out"; \ + docker compose logs app; \ + exit 1 + +# Show current deployment status across all environments +status: + @echo "=== EuchreCamp Deployment Status ===" + @echo "" + @echo "CI Site (euchre-camp-ci):" + @grep "image:" /apps/euchre_camp_ci/docker-compose.yml | head -1 + @echo "" + @echo "Dev Site (euchre-camp-dev):" + @grep "image:" /apps/intelligent_silasak/docker-compose.yml | head -1 + @echo "" + @echo "Prod Site (euchre-camp):" + @grep "image:" /apps/youthful_simon/docker-compose.yml | head -1 + @echo "" + +# --- SDLC Database Operations --- + +# Sync production data to development database (manual operation) +sync-dev: + @echo "Syncing production data to development database..." + node scripts/sync-prod-to-dev.js + +# Run acceptance tests against production database (opt-in, manual) +test-prod: + @echo "⚠️ WARNING: This will run tests against the PRODUCTION database!" + @echo " All test records will be cleaned up after the run." + @echo "" + @read -p "Are you sure you want to continue? (y/N) " confirm; \ + if [ "$$confirm" != "y" ]; then \ + echo "Cancelled."; \ + else \ + DATABASE_URL="$$PROD_DATABASE_URL" bun test:acceptance; \ + fi + +# Reset CI database (for local CI testing) +reset-ci-db: + @echo "Resetting CI database..." + psql "$CI_DATABASE_URL" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" + bunx prisma migrate deploy diff --git a/playwright.config.ts b/playwright.config.ts index e15df46..cf8a29d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,16 +11,17 @@ export default defineConfig({ // Fail the build on CI if you accidentally left test.only in the source code. forbidOnly: !!process.env.CI, // Retry on CI only. - retries: process.env.CI ? 2 : 0, - // Always run with 1 worker to avoid database conflicts - workers: 1, + retries: process.env.CI ? 1 : 0, + // Use 2 workers in CI for parallel project execution; 1 locally + workers: process.env.CI ? 2 : 1, // Reporter to use reporter: 'html', // Global setup and teardown globalSetup: require.resolve('./e2e/global.setup'), + globalTeardown: require.resolve('./e2e/global.teardown'), // Use base URL for relative navigation use: { - baseURL: 'http://localhost:3000', + baseURL: process.env.CI ? 'https://euchre-ci.notsosm.art' : 'http://localhost:3000', // Collect trace when retrying the failed test. trace: 'on-first-retry', // Capture screenshot only on failure @@ -65,8 +66,8 @@ export default defineConfig({ }, ], // Run your local dev server before starting the tests - webServer: { - command: 'npm run dev', + webServer: process.env.CI ? undefined : { + command: 'bun run dev', url: 'http://localhost:3000', timeout: 120000, reuseExistingServer: !process.env.CI, diff --git a/scripts/sync-prod-to-dev.js b/scripts/sync-prod-to-dev.js new file mode 100644 index 0000000..cdbaf34 --- /dev/null +++ b/scripts/sync-prod-to-dev.js @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +/** + * Sync production data to development database + * + * This script copies real data from production to development, filtering out + * test records to keep the dev database clean for testing. + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const PROD_DB_URL = process.env.PROD_DATABASE_URL; +const DEV_DB_URL = process.env.DEV_DATABASE_URL; + +if (!PROD_DB_URL || !DEV_DB_URL) { + console.error('❌ Missing environment variables'); + console.error('Please set PROD_DATABASE_URL and DEV_DATABASE_URL'); + console.error(''); + console.error('Example:'); + console.error(' PROD_DATABASE_URL="postgresql://user:pass@host:5432/euchre_camp" \\'); + console.error(' DEV_DATABASE_URL="postgresql://user:pass@host:5432/euchre_camp_dev" \\'); + console.error(' node scripts/sync-prod-to-dev.js'); + process.exit(1); +} + +if (PROD_DB_URL.includes('_dev') || PROD_DB_URL.includes('_ci')) { + console.error('❌ PROD_DATABASE_URL appears to be a non-production database!'); + console.error(' This script should only be used with the production database.'); + process.exit(1); +} + +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 buildLikeClause(patterns) { + return patterns.map(p => `name LIKE '${p}'`).join(' OR '); +} + +function buildEmailLikeClause(patterns) { + return patterns.map(p => `email LIKE '${p}'`).join(' OR '); +} + +async function main() { + console.log('🔄 Syncing production data to development database'); + console.log('====================================================\n'); + + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + console.log('⚠️ WARNING: This will OVERWRITE the development database!'); + console.log(' Production data will be copied, with test records excluded.'); + console.log(''); + console.log('Source:', PROD_DB_URL.replace(/:[^:@]+@/, ':***@')); + console.log('Target:', DEV_DB_URL.replace(/:[^:@]+@/, ':***@')); + console.log(''); + + rl.question('Type "sync" to confirm: ', async (answer) => { + if (answer !== 'sync') { + console.log('❌ Cancelled'); + rl.close(); + process.exit(0); + } + + rl.close(); + + try { + console.log('\n📦 Dumping production data...'); + + const dumpFile = path.join(os.tmpdir(), `prod_dump_${Date.now()}.sql`); + + execSync(`pg_dump "${PROD_DB_URL}" -f "${dumpFile}" --no-owner --no-acl`, { + stdio: 'inherit' + }); + console.log('✅ Dump created'); + + console.log('\n🗑️ Clearing development database...'); + execSync(`psql "${DEV_DB_URL}" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"`, { + stdio: 'inherit' + }); + console.log('✅ Development database cleared'); + + console.log('\n📥 Restoring to development database...'); + execSync(`psql "${DEV_DB_URL}" -f "${dumpFile}"`, { + stdio: 'inherit' + }); + console.log('✅ Data restored'); + + console.log('\n🧹 Cleaning up test records in dev database...'); + + const playerWhere = buildLikeClause(TEST_PATTERNS.players); + const eventWhere = buildLikeClause(TEST_PATTERNS.events); + const userWhere = buildEmailLikeClause(TEST_PATTERNS.users); + + execSync(`psql "${DEV_DB_URL}" -c "DELETE FROM events WHERE (${eventWhere});"`, { + stdio: 'inherit' + }); + console.log(' Deleted test events'); + + execSync(`psql "${DEV_DB_URL}" -c "DELETE FROM players WHERE (${playerWhere});"`, { + stdio: 'inherit' + }); + console.log(' Deleted test players'); + + execSync(`psql "${DEV_DB_URL}" -c "DELETE FROM users WHERE (${userWhere}) AND \"playerId\" IS NULL;"`, { + stdio: 'inherit' + }); + console.log(' Deleted test users'); + + fs.unlinkSync(dumpFile); + console.log('\n🧹 Cleaned up temporary dump file'); + + console.log('\n✅ Sync complete!'); + console.log('\n📊 Summary:'); + console.log(' - Production data copied to development'); + console.log(' - Test records filtered out'); + console.log(' - Development database is now a mirror of production (minus test data)'); + + } catch (error) { + console.error('\n❌ Sync failed:', error.message); + process.exit(1); + } + }); +} + +main(); \ No newline at end of file diff --git a/src/__tests__/Navigation.test.tsx b/src/__tests__/Navigation.test.tsx index 70cac87..220bcbd 100644 --- a/src/__tests__/Navigation.test.tsx +++ b/src/__tests__/Navigation.test.tsx @@ -8,6 +8,7 @@ import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test' import { render, screen, waitFor } from '@testing-library/react' import Navigation from '@/components/Navigation' +import { RoleSwitcherProvider } from '@/components/RoleSwitcher' // Mock next/link mock.module('next/link', () => ({ @@ -31,6 +32,14 @@ mock.module('@/lib/auth-client', () => ({ // Mock fetch for role API call global.fetch = mock(async () => new Response()) as any +function renderNavigation() { + return render( + + + + ) +} + import { useSession as useSessionOriginal } from '@/components/SessionProvider' const useSession = useSessionOriginal as any @@ -61,7 +70,7 @@ describe('Epic 1: Navigation Component', () => { refreshSession: mock(() => {}), }) - render() + renderNavigation() expect(screen.getByText('EuchreCamp')).toBeInTheDocument() expect(screen.getByText('Rankings')).toBeInTheDocument() @@ -84,7 +93,7 @@ describe('Epic 1: Navigation Component', () => { refreshSession: mock(() => {}), }) - render() + renderNavigation() await waitFor(() => { expect(screen.getByText('Test User')).toBeInTheDocument() @@ -109,7 +118,7 @@ describe('Epic 1: Navigation Component', () => { refreshSession: mock(() => {}), }) - render() + renderNavigation() await waitFor(() => { expect(screen.getByText('Tournaments')).toBeInTheDocument() @@ -142,7 +151,7 @@ describe('Epic 1: Navigation Component', () => { return new Response(JSON.stringify({}), { status: 200 }) }) - render() + renderNavigation() await waitFor(() => { expect(screen.getByText('Admin')).toBeInTheDocument() @@ -177,7 +186,7 @@ describe('Epic 1: Navigation Component', () => { } as Response }) - render() + renderNavigation() await waitFor(() => { expect(screen.getByText('Tournaments')).toBeInTheDocument() @@ -192,7 +201,7 @@ describe('Epic 1: Navigation Component', () => { refreshSession: mock(() => {}), }) - render() + renderNavigation() expect(screen.getByText('Loading...')).toBeInTheDocument() }) diff --git a/src/__tests__/unit/permissions.test.ts b/src/__tests__/unit/permissions.test.ts index 8a7259c..26a3a07 100644 --- a/src/__tests__/unit/permissions.test.ts +++ b/src/__tests__/unit/permissions.test.ts @@ -7,7 +7,6 @@ import { describe, test, expect, mock, beforeEach } from 'bun:test'; import { hasRole, canManageTournament, canCreateTournaments } from '@/lib/permissions'; import { getSession } from '@/lib/auth-simple'; -import { prisma } from '@/lib/prisma'; import type { User } from '@prisma/client'; // Create mock functions at module level