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