Compare commits
21 Commits
v0.1.5
..
21bc63701d
| Author | SHA1 | Date | |
|---|---|---|---|
| 21bc63701d | |||
| 48e8f894e3 | |||
| adf20693c5 | |||
| e303955599 | |||
| fa6c4369a6 | |||
| 9c884ee020 | |||
| 5b91cf1426 | |||
| 00373fcef4 | |||
| 80714a5daa | |||
| 039b927bb7 | |||
| 9511eed477 | |||
| 0e1046fcb5 | |||
| 5f02726ba6 | |||
| e08d72bfe6 | |||
| 9980021037 | |||
| 6b70c3f388 | |||
| ff7461973a | |||
| 9b69cb84e3 | |||
| 5c9b98a926 | |||
| 73ba929e36 | |||
| c9cd00a055 |
@@ -1,15 +0,0 @@
|
||||
# Development environment configuration for EuchreCamp
|
||||
# Copy this file to .env.development and fill in your values
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_PROVIDER=postgresql
|
||||
# Development database URL - must contain "_dev" to pass safety checks
|
||||
DATABASE_URL="postgresql://euchre_camp:password@localhost:5432/euchre_camp_dev"
|
||||
|
||||
# Authentication
|
||||
BETTER_AUTH_SECRET="your-secret-key-change-this"
|
||||
BETTER_AUTH_URL="http://localhost:3000"
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=development
|
||||
TRUSTED_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
@@ -1,54 +0,0 @@
|
||||
# EuchreCamp Environment Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# ============================================
|
||||
# PostgreSQL connection string
|
||||
# Format: postgresql://username:password@host:port/database
|
||||
DATABASE_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp
|
||||
|
||||
# Shadow database for Prisma migrations (optional for PostgreSQL)
|
||||
DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@localhost:5432/euchre_camp_shadow
|
||||
|
||||
# Database provider (postgresql, mysql, sqlite, etc.)
|
||||
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
|
||||
|
||||
# Base URL for authentication callbacks
|
||||
# For production: https://your-domain.com
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# ============================================
|
||||
# Application Configuration
|
||||
# ============================================
|
||||
# Environment: development, production, test
|
||||
NODE_ENV=production
|
||||
|
||||
# Trusted origins for CORS and authentication
|
||||
# Add your domain(s) for production
|
||||
TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
|
||||
# ============================================
|
||||
# Optional: External Services
|
||||
# ============================================
|
||||
# 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)
|
||||
+1
-11
@@ -1,15 +1,5 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"ignorePatterns": [
|
||||
".next",
|
||||
"out",
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Build CI Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'Dockerfile.ci-base'
|
||||
- 'package.json'
|
||||
- 'bun.lockb'
|
||||
- '.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
|
||||
|
||||
env:
|
||||
REGISTRY: docker.notsosm.art
|
||||
IMAGE_NAME: euchre-camp
|
||||
|
||||
jobs:
|
||||
build-ci-base:
|
||||
runs-on: ubuntu-latest
|
||||
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)
|
||||
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
|
||||
docker build \
|
||||
--file Dockerfile.ci-base \
|
||||
--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 }}
|
||||
@@ -0,0 +1,82 @@
|
||||
# CI Image Builder Workflow
|
||||
# Automatically builds and pushes CI images when dependencies change
|
||||
# Uses GitHub Container Registry (GHCR) or any container registry
|
||||
|
||||
name: CI Image Builder
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'Dockerfile.ci'
|
||||
- '.gitea/workflows/ci-image-builder.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_rebuild:
|
||||
description: 'Force rebuild even if no changes detected'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
# Use GHCR or your own registry
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}/ci-runner
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
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: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,prefix={{branch}}-
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push CI image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ci
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Update workflow documentation
|
||||
run: |
|
||||
echo "CI Image built and pushed successfully!"
|
||||
echo ""
|
||||
echo "Available tags:"
|
||||
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
||||
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
|
||||
echo ""
|
||||
echo "To use in workflows:"
|
||||
echo " container:"
|
||||
echo " image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
||||
+46
-9
@@ -9,27 +9,64 @@ jobs:
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker.notsosm.art/euchre-camp/ci-base:latest
|
||||
image: node:20-alpine
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apk add --no-cache bash git
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: bun x prisma generate
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:pass@localhost:5432/dummy
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun test src/__tests__/unit/ src/__tests__/*.test.tsx src/__tests__/auth-simple.test.ts
|
||||
run: npm run test:run
|
||||
|
||||
acceptance-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit-tests
|
||||
container:
|
||||
image: node:20-alpine
|
||||
options: --user root
|
||||
env:
|
||||
DATABASE_PROVIDER: sqlite
|
||||
DATABASE_URL: file:./prisma/ci.db
|
||||
BETTER_AUTH_SECRET: test-secret-key-for-ci-only
|
||||
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apk add --no-cache bash git
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: npx prisma generate
|
||||
|
||||
- name: Setup SQLite database
|
||||
run: |
|
||||
# Create SQLite database file
|
||||
mkdir -p prisma
|
||||
npx prisma migrate deploy
|
||||
|
||||
- name: Run acceptance tests
|
||||
run: npm run test:acceptance
|
||||
env:
|
||||
DATABASE_PROVIDER: sqlite
|
||||
DATABASE_URL: file:./prisma/ci.db
|
||||
BETTER_AUTH_SECRET: test-secret-key-for-ci-only
|
||||
|
||||
analyze-bump-type:
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit-tests
|
||||
needs: acceptance-tests
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -14,16 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip if this is an auto-generated version bump commit
|
||||
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
|
||||
container:
|
||||
image: docker.notsosm.art/euchre-camp/ci-base:latest
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITEA_TOKEN || github.token }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@@ -73,42 +69,30 @@ jobs:
|
||||
echo "Bumping version: $BUMP"
|
||||
|
||||
# Run the bump script
|
||||
bun run scripts/bump-version.js "$BUMP" --yes
|
||||
node scripts/bump-version.js "$BUMP" --yes
|
||||
|
||||
# Get new version
|
||||
NEW_VERSION=$(bun -e "console.log(require('./package.json').version)")
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
- name: Commit version bump
|
||||
id: commit
|
||||
run: |
|
||||
git add package.json CHANGELOG.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit (version may already be at target version)"
|
||||
echo "committed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
git commit -m "chore: bump version to v${{ steps.version.outputs.new_version }}"
|
||||
git push origin main
|
||||
echo "committed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
git commit -m "chore: bump version to v${{ steps.version.outputs.new_version }}"
|
||||
git push origin main
|
||||
|
||||
- name: Create git tag for release
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
run: |
|
||||
TAG_NAME="v${{ steps.version.outputs.new_version }}"
|
||||
echo "Creating tag $TAG_NAME"
|
||||
|
||||
# Check if tag already exists
|
||||
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG_NAME already exists, skipping tag creation"
|
||||
else
|
||||
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
|
||||
git push origin "$TAG_NAME"
|
||||
fi
|
||||
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
|
||||
git push origin "$TAG_NAME"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build test-capable image
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
run: |
|
||||
docker build \
|
||||
--target test-runner \
|
||||
@@ -117,15 +101,13 @@ jobs:
|
||||
.
|
||||
|
||||
- name: Run tests inside test-capable container
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" \
|
||||
${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \
|
||||
bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'
|
||||
npm run test:run
|
||||
|
||||
- name: Build production image
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
run: |
|
||||
docker build \
|
||||
--target runner \
|
||||
@@ -135,60 +117,16 @@ jobs:
|
||||
.
|
||||
|
||||
- name: Push Docker images
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
run: |
|
||||
echo "Pushing to ${{ env.REGISTRY }}..."
|
||||
# Check if we can authenticate to the registry using DOCKER_LOGIN and DOCKER_PASSWORD secrets
|
||||
if [ -n "${{ secrets.DOCKER_LOGIN }}" ] && [ -n "${{ secrets.DOCKER_PASSWORD }}" ]; then
|
||||
if docker login ${{ env.REGISTRY }} -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWORD }} 2>/dev/null; then
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
echo "Successfully pushed images to ${{ env.REGISTRY }}"
|
||||
else
|
||||
echo "Warning: Docker login failed with provided credentials"
|
||||
echo "Images built locally but not pushed to registry"
|
||||
echo "Manual push required:"
|
||||
echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }}"
|
||||
echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
||||
fi
|
||||
else
|
||||
echo "Warning: DOCKER_LOGIN or DOCKER_PASSWORD secrets not configured"
|
||||
echo "Images built locally but not pushed to registry"
|
||||
echo "Manual push required:"
|
||||
echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }}"
|
||||
echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
||||
fi
|
||||
echo "Pushing to registry..."
|
||||
# Note: Docker registry credentials should be configured in Gitea secrets
|
||||
# For now, just show the manual push commands
|
||||
echo "Images built locally but not pushed to registry"
|
||||
echo "Manual push required:"
|
||||
echo " docker push docker.notsosm.art/euchre-camp:${{ steps.version.outputs.new_version }}"
|
||||
echo " docker push docker.notsosm.art/euchre-camp:latest"
|
||||
|
||||
- name: Deploy to dev environment
|
||||
if: steps.commit.outputs.committed == 'true'
|
||||
- name: Deploy to dev (placeholder)
|
||||
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
|
||||
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
|
||||
|
||||
# Pull and restart the dev container
|
||||
cd /home/euchre_camp
|
||||
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
|
||||
# TODO: Add actual deployment steps
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-alpine
|
||||
options: --user root
|
||||
# Skip if this is an auto-generated version bump commit (handled by release workflow)
|
||||
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
|
||||
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apk add --no-cache bash git
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:run
|
||||
+1
-8
@@ -34,14 +34,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# Allow example env files to be tracked
|
||||
!.env.example
|
||||
!.env.development.example
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -9,12 +9,11 @@ EuchreCamp is a Next.js 14+ application for managing Euchre tournaments and trac
|
||||
### Key Technologies
|
||||
- **Next.js 14+** (App Router)
|
||||
- **TypeScript**
|
||||
- **Prisma ORM** (SQLite/PostgreSQL)
|
||||
- **Prisma ORM** (SQLite)
|
||||
- **Tailwind CSS**
|
||||
- **Better Auth** (Authentication)
|
||||
- **Bun** (Package Manager & Test Runner)
|
||||
- **Vitest** (Unit Testing)
|
||||
- **Playwright** (Acceptance Testing)
|
||||
- **Vitest** (Legacy - migrated to Bun test runner)
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
@@ -38,55 +37,13 @@ EuchreCamp is a Next.js 14+ application for managing Euchre tournaments and trac
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Package Manager: Bun
|
||||
|
||||
This project uses **Bun** as the package manager and test runner:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Run development server
|
||||
bun run dev
|
||||
|
||||
# Build the application
|
||||
bun run build
|
||||
|
||||
# Run unit/component tests
|
||||
bun test
|
||||
|
||||
# Run unit tests only
|
||||
bun run test:unit
|
||||
|
||||
# Run component tests only
|
||||
bun run test:component
|
||||
|
||||
# Run acceptance tests (Playwright)
|
||||
bun run test:acceptance
|
||||
|
||||
# Run linting
|
||||
bun run lint
|
||||
```
|
||||
|
||||
**Note**: E2E tests still use Playwright, as Bun's test runner doesn't support browser automation. Unit and component tests have been migrated to Bun's native test runner for faster execution. E2E tests are located in the `e2e/` directory (not `src/__tests__/e2e/`).
|
||||
|
||||
### CI/CD with Bun
|
||||
|
||||
All CI/CD workflows have been updated to use Bun:
|
||||
|
||||
- **Dockerfile**: Uses `oven/bun:alpine` base image for all stages
|
||||
- **PR Workflow**: Uses `oven/setup-bun` action and `bun install`, `bun test`
|
||||
- **Release Workflow**: Uses Bun for version bumping and Docker builds
|
||||
|
||||
**Note**: The `test.yml` workflow has been removed as it's redundant with the PR workflow.
|
||||
|
||||
### Admin User Creation
|
||||
|
||||
To create an admin user, use the provided scripts:
|
||||
|
||||
**Option 1: Using Better Auth API (Recommended)**
|
||||
```bash
|
||||
bun run scripts/create-admin-via-api.js
|
||||
node scripts/create-admin-via-api.js
|
||||
```
|
||||
This creates the admin user `david@dhg.lol` with password `adminadmin` using Better Auth's internal API.
|
||||
|
||||
@@ -172,26 +129,23 @@ DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptanc
|
||||
|
||||
### CI Runner Image
|
||||
|
||||
**Note:** The CI runner image approach has been deprecated for Gitea Actions workflows.
|
||||
To speed up CI runs, the project uses a pre-built CI runner image with all dependencies pre-installed.
|
||||
|
||||
The original attempt to use a pre-built CI runner image with pre-installed dependencies encountered fundamental issues with how Gitea Actions handles workspace mounting. When Gitea Actions runs a container job, it mounts the workspace at a specific path (e.g., `/workspace/david/euchre_camp`), which hides the container's `/app` directory where dependencies were installed.
|
||||
**Building the CI image locally:**
|
||||
```bash
|
||||
./scripts/build-ci-image.sh [tag]
|
||||
```
|
||||
|
||||
**Current Approach:**
|
||||
- Workflows use standard `node:20-alpine` or `mcr.microsoft.com/playwright` containers
|
||||
- Dependencies are installed via `npm ci` in each workflow run
|
||||
- This is the recommended approach for Gitea Actions
|
||||
**Using the CI image:**
|
||||
- Workflows automatically use `ghcr.io/<repo>/ci-runner:latest` when available
|
||||
- Falls back to `node:20-alpine` if image is not found
|
||||
- Image is automatically rebuilt when `package.json` changes
|
||||
|
||||
**Why the CI image approach doesn't work:**
|
||||
1. Dockerfile.ci installs dependencies in `/app`
|
||||
2. Gitea Actions mounts workspace at `/workspace/david/euchre_camp`
|
||||
3. Workspace mount hides the `/app` directory
|
||||
4. Symlinks from `/app/node_modules` don't work because `/app` is hidden
|
||||
|
||||
**Alternative for performance:**
|
||||
If CI performance becomes an issue, consider:
|
||||
- Using GitHub Actions cache for node_modules
|
||||
- Using a self-hosted runner with persistent workspace
|
||||
- Using the main Dockerfile's `test-runner` target for release workflows (which works because it builds a complete image)
|
||||
**CI Image contains:**
|
||||
- Node.js 20 Alpine
|
||||
- All npm dependencies pre-installed
|
||||
- Prisma client pre-generated
|
||||
- System tools (bash, git, python, make, g++)
|
||||
|
||||
### CI/CD Pipeline
|
||||
The project uses Gitea Actions for continuous integration:
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
## [0.1.5] - 2026-04-26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ci: fix deployment directory path in release workflow
|
||||
|
||||
## [0.1.4] - 2026-04-02
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ci: remove acceptance tests and add dev deployment
|
||||
- fix(tests): resolve password validation, csv upload, and admin auth issues
|
||||
- fix(ci): correct playwright config paths and add env example files
|
||||
- fix(ci): use existing PostgreSQL server at dhg.lol
|
||||
- fix(ci): use PostgreSQL in acceptance-tests with dev credentials
|
||||
- feat(ci): build custom Docker images for Gitea Actions compatibility
|
||||
- fix(tests): add @prisma/client mock for test isolation
|
||||
- fix(ci): add prisma generate step to unit-tests job
|
||||
- fix: downgrade ESLint to v8.57.1 for LSP compatibility
|
||||
- fix: improve TypeScript types for better IDE code hinting
|
||||
- feat: migrate to ESLint flat config (eslint.config.js)
|
||||
- Revert "fix: downgrade ESLint to v8.x for .eslintrc.json compatibility"
|
||||
- fix: downgrade ESLint to v8.x for .eslintrc.json compatibility
|
||||
- fix: update PR workflow to exclude e2e tests from unit test phase
|
||||
- fix: avoid global mock clearing in EditTournamentForm tests
|
||||
- fix: avoid global mock clearing in Navigation tests
|
||||
- fix: add DOM cleanup to bun-setup.ts
|
||||
- fix: disable test isolation in bunfig.toml
|
||||
- refactor: improve test structure for Bun compatibility
|
||||
- feat: update CI/CD to use Bun
|
||||
- feat: migrate from npm to Bun
|
||||
- docs: update TODO list with recent fix
|
||||
- fix: add defensive checks to prisma.ts to prevent build failures
|
||||
|
||||
## [0.1.3] - 2026-04-01
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: skip release steps if no version bump commit was made
|
||||
- fix: improve error handling in getCommitsSinceLastTag
|
||||
- fix: add tag existence check in release workflow
|
||||
- fix: resolve release workflow version bump issues
|
||||
- ci-image-improvements (#18)
|
||||
- fix: version bumping and Docker registry authentication (#17)
|
||||
- fix: handle Docker registry authentication gracefully in release workflow
|
||||
- fix: run unit tests on branch commits, skip main branch
|
||||
- feat: add test workflow for every commit
|
||||
- trigger: release with test-capable image
|
||||
- feat: build test-capable image, run tests, then build production image
|
||||
- trigger: release workflow
|
||||
- fix: release workflow should not commit, only tag
|
||||
- trigger: manual workflow trigger for docker.notsosm.art
|
||||
- fix: update Docker build script to use docker.notsosm.art registry
|
||||
- fix: update workflow to use docker.notsosm.art registry
|
||||
- trigger: manual workflow trigger
|
||||
- fix: update workflow to use correct Docker registries
|
||||
- feat: add Gitea Actions release workflow
|
||||
- feat: add view match link to admin matches page
|
||||
|
||||
## [0.1.2] - 2026-04-01
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+13
-13
@@ -1,9 +1,9 @@
|
||||
# Multi-stage build for EuchreCamp Next.js application
|
||||
|
||||
# Stage 1: Builder
|
||||
FROM oven/bun:alpine AS builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install dependencies (needed for native modules)
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Set working directory
|
||||
@@ -13,21 +13,21 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies (including dev dependencies for building)
|
||||
RUN bun install
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client (with dummy PostgreSQL DATABASE_URL for build-time generation)
|
||||
# Note: A dummy URL is used since the real database is not available during build
|
||||
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
||||
|
||||
# Build the application (with dummy DATABASE_URL for static page generation and git commit)
|
||||
ARG GIT_COMMIT=unknown
|
||||
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT bun run build
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT npm run build
|
||||
|
||||
# Stage 2: Test runner (includes dev dependencies for testing)
|
||||
FROM oven/bun:alpine AS test-runner
|
||||
FROM node:20-alpine AS test-runner
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache python3 make g++ git
|
||||
@@ -39,16 +39,16 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
# Install ALL dependencies (including dev dependencies for testing)
|
||||
RUN bun install
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client
|
||||
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
||||
|
||||
# Stage 3: Production runner
|
||||
FROM oven/bun:alpine AS runner
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
@@ -64,15 +64,15 @@ WORKDIR /app
|
||||
COPY --from=builder --chown=euchre:euchre /app/.next ./.next
|
||||
COPY --from=builder --chown=euchre:euchre /app/public ./public
|
||||
COPY --from=builder --chown=euchre:euchre /app/package.json ./package.json
|
||||
COPY --from=builder --chown=euchre:euchre /app/bun.lockb ./bun.lockb
|
||||
COPY --from=builder --chown=euchre:euchre /app/package-lock.json ./package-lock.json
|
||||
COPY --from=builder --chown=euchre:euchre /app/prisma ./prisma
|
||||
|
||||
# Install only production dependencies
|
||||
RUN bun install --production
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Generate Prisma client
|
||||
# Note: We need to set DATABASE_URL even for generation because prisma.config.ts requires it
|
||||
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
||||
|
||||
# Switch to non-root user
|
||||
USER euchre
|
||||
@@ -86,4 +86,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
|
||||
# Start command
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
CMD ["bun", "run", "start"]
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# CI Runner Image for EuchreCamp
|
||||
# Pre-installed with all dependencies to speed up CI runs
|
||||
# This image is rebuilt automatically when package.json changes
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install system dependencies needed for various tasks
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
git \
|
||||
curl \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files first (for better caching)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install ALL dependencies including dev dependencies
|
||||
# This is done at image build time, not at CI runtime
|
||||
RUN npm ci
|
||||
|
||||
# Copy Prisma schema (needed for Prisma client generation)
|
||||
COPY prisma/schema.prisma ./prisma/
|
||||
|
||||
# Generate Prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY . .
|
||||
|
||||
# Set environment variables for CI environment
|
||||
ENV NODE_ENV=production
|
||||
ENV DATABASE_PROVIDER=sqlite
|
||||
ENV DATABASE_URL=file:./prisma/ci.db
|
||||
ENV BETTER_AUTH_SECRET=test-secret-key-for-ci-only
|
||||
|
||||
# Verify installations
|
||||
RUN node --version && npm --version && git --version && npx prisma --version
|
||||
|
||||
# Default command
|
||||
CMD ["/bin/sh"]
|
||||
@@ -1,32 +0,0 @@
|
||||
# Base CI image with Bun, Node.js, Playwright, and build tools
|
||||
# Used for Gitea Actions CI workflows
|
||||
# Uses Microsoft Playwright image as base (Ubuntu-based) with Bun added
|
||||
|
||||
FROM mcr.microsoft.com/playwright:v1.58.0-jammy AS base
|
||||
|
||||
# Install unzip (required for Bun installation) and other tools
|
||||
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Bun (latest version)
|
||||
# Note: The playwright image already has Node.js pre-installed
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Add Bun to PATH for subsequent commands
|
||||
ENV PATH="/root/.bun/bin:$PATH"
|
||||
|
||||
# Verify installations
|
||||
RUN echo "=== Bun Version ===" && bun --version && \
|
||||
echo "=== Node.js Version ===" && node --version && \
|
||||
echo "=== Playwright Version ===" && bun x playwright --version
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Set default environment variables
|
||||
ENV DATABASE_PROVIDER=sqlite
|
||||
ENV DATABASE_URL=file:./prisma/ci.db
|
||||
ENV BETTER_AUTH_SECRET=test-secret-key-for-ci-only
|
||||
ENV NODE_ENV=test
|
||||
|
||||
# Health check command (can be overridden)
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD bun --version
|
||||
@@ -368,27 +368,35 @@ The application uses Gitea Actions for continuous integration and deployment:
|
||||
- Unit tests for quick feedback
|
||||
- Skips auto-generated version bumps
|
||||
|
||||
3. **Release Workflow** (`.gitea/workflows/release.yml`): Runs on main branch pushes
|
||||
- Version bumping and tagging
|
||||
- Docker image building and testing
|
||||
- Registry push and deployment
|
||||
3. **Release Workflow** (`.gitea/workflows/release.yml`): Runs on main branch pushes
|
||||
- Version bumping and tagging
|
||||
- Docker image building and testing
|
||||
- Registry push and deployment
|
||||
|
||||
4. **CI Image Builder** (`.gitea/workflows/ci-image-builder.yml`): Runs when dependencies change
|
||||
- Automatically builds CI runner image with all dependencies pre-installed
|
||||
- Triggered by changes to `package.json`, `package-lock.json`, or `Dockerfile.ci`
|
||||
|
||||
### CI Runner Image
|
||||
|
||||
**Note:** The CI runner image approach has been deprecated for Gitea Actions workflows.
|
||||
To avoid installing dependencies on every workflow run, we use a pre-built CI runner image:
|
||||
|
||||
The original attempt to use a pre-built CI runner image with pre-installed dependencies encountered fundamental issues with how Gitea Actions handles workspace mounting. When Gitea Actions runs a container job, it mounts the workspace at a specific path (e.g., `/workspace/david/euchre_camp`), which hides the container's `/app` directory where dependencies were installed.
|
||||
**Dockerfile.ci** - Contains:
|
||||
- Node.js 20 Alpine
|
||||
- All npm dependencies pre-installed
|
||||
- Prisma client pre-generated
|
||||
- System tools (bash, git, python, make, g++)
|
||||
|
||||
**Current Approach:**
|
||||
- Workflows use standard `node:20-alpine` or `mcr.microsoft.com/playwright` containers
|
||||
- Dependencies are installed via `npm ci` in each workflow run
|
||||
- This is the recommended approach for Gitea Actions
|
||||
**Automatic Rebuilding:**
|
||||
- The CI image is automatically rebuilt when `package.json` or `package-lock.json` changes
|
||||
- Images are tagged with commit SHA for traceability
|
||||
- Falls back to `node:20-alpine` if custom image is not available
|
||||
|
||||
**Why the CI image approach doesn't work:**
|
||||
1. Dockerfile.ci installs dependencies in `/app`
|
||||
2. Gitea Actions mounts workspace at `/workspace/david/euchre_camp`
|
||||
3. Workspace mount hides the `/app` directory
|
||||
4. Symlinks from `/app/node_modules` don't work because `/app` is hidden
|
||||
**Manual Build:**
|
||||
```bash
|
||||
docker build -f Dockerfile.ci -t euchre-camp-ci:latest .
|
||||
docker run --rm euchre-camp-ci:latest node --version
|
||||
```
|
||||
|
||||
### Database Strategy for CI
|
||||
- **CI Tests**: SQLite database (fast, no server required)
|
||||
@@ -402,6 +410,9 @@ npm run test:run
|
||||
|
||||
# Run acceptance tests with SQLite
|
||||
DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance
|
||||
|
||||
# Run tests in CI container (if built)
|
||||
docker run --rm -v $(pwd):/app -w /app euchre-camp-ci:latest npm run test:run
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[test]
|
||||
preload = ["./src/__tests__/bun-setup.ts"]
|
||||
exclude = ["e2e/**", "**/e2e/**"]
|
||||
# isolation = true
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
services:
|
||||
app:
|
||||
image: docker.notsosm.art/euchre-camp:0.1.0.dev
|
||||
image: euchre-camp/euchre-camp:0.1.0.dev
|
||||
container_name: euchre-camp-app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -30,23 +30,6 @@
|
||||
- [ ] Update MatchEditor to use tournament-specific target score
|
||||
- [ ] Run tests and verify variant scoring implementation
|
||||
|
||||
### Recently Completed ✅
|
||||
- [x] Update CI/CD workflows to use Bun (PR, release)
|
||||
- [x] Update Dockerfile to use Bun Alpine image
|
||||
- [x] Update PR workflow to use Bun
|
||||
- [x] Update Release workflow to use Bun
|
||||
- [x] Remove test.yml workflow (redundant)
|
||||
- [x] Verify Docker build with Bun
|
||||
|
||||
### Recently Completed ✅
|
||||
- [x] Fix Prisma build error in CI pipeline (Docker build failure due to missing DATABASE_URL validation)
|
||||
- [x] Add defensive checks to src/lib/prisma.ts to prevent build failures
|
||||
- [x] Migrate from npm to Bun package manager
|
||||
- [x] Migrate unit tests (Vitest → Bun test runner)
|
||||
- [x] Migrate component tests (Vitest → Bun test runner)
|
||||
- [x] Configure Bun with DOM environment for React Testing Library
|
||||
- [x] Keep Playwright for E2E tests (hybrid approach)
|
||||
|
||||
### Recently Completed ✅
|
||||
- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts)
|
||||
- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts)
|
||||
|
||||
@@ -173,6 +173,16 @@ pr-validate: lint typecheck test-unit test-acceptance-sqlite
|
||||
ci-postgres: lint typecheck test-unit test-acceptance-postgres docker-build
|
||||
@echo "CI Pipeline with PostgreSQL completed successfully!"
|
||||
|
||||
# --- CI Image ---
|
||||
|
||||
# Build CI runner image locally
|
||||
ci-build:
|
||||
@./scripts/build-ci-image.sh
|
||||
|
||||
# Build CI runner image with custom tag
|
||||
ci-build-tag TAG:
|
||||
@./scripts/build-ci-image.sh {{TAG}}
|
||||
|
||||
# --- Utilities ---
|
||||
|
||||
# Show help information
|
||||
@@ -246,8 +256,6 @@ 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)"
|
||||
@echo ""
|
||||
@echo "Note: CI image approach deprecated due to Gitea Actions workspace mounting"
|
||||
|
||||
# Check current database provider
|
||||
db-status:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[tools]
|
||||
bun = "latest"
|
||||
docker-compose = "latest"
|
||||
just = "latest"
|
||||
node = "latest"
|
||||
|
||||
+24
-30
@@ -1,42 +1,38 @@
|
||||
{
|
||||
"name": "euchre_camp",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "bun run eslint",
|
||||
"test": "bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'",
|
||||
"test:unit": "bun test src/__tests__/unit/",
|
||||
"test:component": "bun test src/__tests__/*.test.tsx",
|
||||
"test:run": "bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'",
|
||||
"test:randomize": "bun test src/__tests__/unit/ --randomize",
|
||||
"test:unit:sequential": "bun test src/__tests__/unit/ --max-concurrency=1",
|
||||
"test:acceptance": "bun x playwright test e2e/",
|
||||
"test:acceptance:headed": "bun x playwright test e2e/ --headed",
|
||||
"db:switch": "bun run scripts/switch-database.js",
|
||||
"db:setup-postgres": "bun run scripts/setup-postgres.js",
|
||||
"db:setup-dev": "bun run scripts/setup-postgres.js",
|
||||
"db:setup-dev:clean": "bun run scripts/setup-postgres.js --drop",
|
||||
"db:reset-dev": "bun run scripts/reset-dev-db.js",
|
||||
"db:use-dev": "bun run scripts/use-dev-db.js",
|
||||
"db:cleanup-prod": "bun run scripts/cleanup-prod-db.js",
|
||||
"db:check-prod": "bun run scripts/check-test-records.js",
|
||||
"db:seed": "bun run scripts/seed.js",
|
||||
"lint": "eslint",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:acceptance": "playwright test src/__tests__/e2e/",
|
||||
"test:acceptance:headed": "playwright test src/__tests__/e2e/ --headed",
|
||||
"db:switch": "node scripts/switch-database.js",
|
||||
"db:setup-postgres": "node scripts/setup-postgres.js",
|
||||
"db:setup-dev": "node scripts/setup-postgres.js",
|
||||
"db:setup-dev:clean": "node scripts/setup-postgres.js --drop",
|
||||
"db:reset-dev": "node scripts/reset-dev-db.js",
|
||||
"db:use-dev": "node scripts/use-dev-db.js",
|
||||
"db:cleanup-prod": "node scripts/cleanup-prod-db.js",
|
||||
"db:check-prod": "node scripts/check-test-records.js",
|
||||
"db:seed": "node scripts/seed.js",
|
||||
"docker:up": "docker-compose up -d",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:logs": "docker-compose logs -f",
|
||||
"docker:build": "docker-compose build",
|
||||
"docker:shell": "docker exec -it euchre-camp-app sh",
|
||||
"version": "bun run scripts/bump-version.js",
|
||||
"set-version": "bun run scripts/set-version.js",
|
||||
"version:patch": "bun run scripts/bump-version.js patch",
|
||||
"version:minor": "bun run scripts/bump-version.js minor",
|
||||
"version:major": "bun run scripts/bump-version.js major",
|
||||
"docker:build:push": "bun run scripts/build-and-push-docker.js",
|
||||
"docker:compose:generate": "bun run scripts/generate-docker-compose.js",
|
||||
"release": "bun run version:patch && bun run docker:compose:generate && bun run docker:build:push"
|
||||
"version": "node scripts/bump-version.js",
|
||||
"set-version": "node scripts/set-version.js",
|
||||
"version:patch": "node scripts/bump-version.js patch",
|
||||
"version:minor": "node scripts/bump-version.js minor",
|
||||
"version:major": "node scripts/bump-version.js major",
|
||||
"docker:build:push": "node scripts/build-and-push-docker.js",
|
||||
"docker:compose:generate": "node scripts/generate-docker-compose.js",
|
||||
"release": "npm run version:patch && npm run docker:compose:generate && npm run docker:build:push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
@@ -65,8 +61,6 @@
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/jsdom": "^28.0.1",
|
||||
"@types/node": "^20",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/pg": "^8.20.0",
|
||||
@@ -74,7 +68,7 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"argon2": "^0.44.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-config-next": "^16.2.1",
|
||||
"jsdom": "^29.0.1",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testDir: './src/__tests__/e2e',
|
||||
timeout: 30000,
|
||||
expect: {
|
||||
timeout: 5000
|
||||
@@ -17,7 +17,7 @@ export default defineConfig({
|
||||
// Reporter to use
|
||||
reporter: 'html',
|
||||
// Global setup and teardown
|
||||
globalSetup: require.resolve('./e2e/global.setup'),
|
||||
globalSetup: require.resolve('./src/__tests__/e2e/global.setup'),
|
||||
// Use base URL for relative navigation
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Build and test the CI runner image locally
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE_NAME="euchre-camp-ci"
|
||||
TAG="${1:-latest}"
|
||||
|
||||
echo "Building CI runner image..."
|
||||
echo "Image: ${IMAGE_NAME}:${TAG}"
|
||||
echo ""
|
||||
|
||||
# Build the image
|
||||
docker build \
|
||||
-f Dockerfile.ci \
|
||||
-t ${IMAGE_NAME}:${TAG} \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "✅ Image built successfully!"
|
||||
echo ""
|
||||
|
||||
# Test the image
|
||||
echo "Testing CI image..."
|
||||
docker run --rm ${IMAGE_NAME}:${TAG} sh -c "
|
||||
echo 'Node.js version:' && node --version
|
||||
echo 'npm version:' && npm --version
|
||||
echo 'Git version:' && git --version
|
||||
echo 'Prisma version:' && npx prisma --version
|
||||
echo ''
|
||||
echo 'Installed packages (top 10):'
|
||||
npm list --depth=0 | head -10
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "✅ CI image is ready to use!"
|
||||
echo ""
|
||||
echo "To use in GitHub Actions/Gitea Actions:"
|
||||
echo " container:"
|
||||
echo " image: ${IMAGE_NAME}:${TAG}"
|
||||
echo ""
|
||||
echo "To push to registry:"
|
||||
echo " docker tag ${IMAGE_NAME}:${TAG} registry.example.com/${IMAGE_NAME}:${TAG}"
|
||||
echo " docker push registry.example.com/${IMAGE_NAME}:${TAG}"
|
||||
+29
-47
@@ -48,22 +48,14 @@ function getCommitsSinceLastTag() {
|
||||
|
||||
if (!latestTag) {
|
||||
// No tags yet, get all commits
|
||||
const commits = execSync('git log --oneline --format=%s', { encoding: 'utf8' }).trim();
|
||||
return commits ? commits.split('\n') : [];
|
||||
return execSync('git log --oneline --format=%s', { encoding: 'utf8' }).trim().split('\n');
|
||||
}
|
||||
|
||||
const commits = execSync(`git log ${latestTag}..HEAD --oneline --format=%s`, { encoding: 'utf8' }).trim();
|
||||
return commits ? commits.split('\n') : [];
|
||||
} catch (error) {
|
||||
// If no commits since tag or other error, get recent commits
|
||||
try {
|
||||
const commits = execSync('git log --oneline --format=%s -n 20', { encoding: 'utf8' }).trim();
|
||||
return commits ? commits.split('\n') : [];
|
||||
} catch (innerError) {
|
||||
// If all git log attempts fail, return empty array
|
||||
console.warn('Warning: Could not retrieve commit history, defaulting to patch version');
|
||||
return [];
|
||||
}
|
||||
return execSync('git log --oneline --format=%s -n 20', { encoding: 'utf8' }).trim().split('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,47 +175,37 @@ function main() {
|
||||
console.log(`New version: ${currentVersion} → ${newVersion}`);
|
||||
}
|
||||
|
||||
// Confirm with user (or skip if --yes flag is set)
|
||||
if (skipConfirm) {
|
||||
// Update package.json
|
||||
updatePackageJson(newVersion);
|
||||
// Confirm with user
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// Update changelog
|
||||
if (bumpType !== 'custom') {
|
||||
const commits = getCommitsSinceLastTag();
|
||||
updateChangelog(newVersion, commits, bumpType);
|
||||
}
|
||||
rl.question(`\nApply version ${newVersion}? (y/N) `, (answer) => {
|
||||
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||
// Update package.json
|
||||
updatePackageJson(newVersion);
|
||||
|
||||
console.log(`\n✅ Version bumped to ${newVersion}`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question(`\nApply version ${newVersion}? (y/N) `, (answer) => {
|
||||
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||
// Update package.json
|
||||
updatePackageJson(newVersion);
|
||||
|
||||
// Update changelog
|
||||
if (bumpType !== 'custom') {
|
||||
const commits = getCommitsSinceLastTag();
|
||||
updateChangelog(newVersion, commits, bumpType);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Version bumped to ${newVersion}`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('❌ Version bump cancelled');
|
||||
process.exit(1);
|
||||
// Update changelog
|
||||
if (bumpType !== 'custom') {
|
||||
const commits = getCommitsSinceLastTag();
|
||||
updateChangelog(newVersion, commits, bumpType);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
});
|
||||
}
|
||||
console.log(`\n✅ Version bumped to ${newVersion}`);
|
||||
console.log(`\nNext steps:`);
|
||||
console.log(` 1. Review changes: git diff`);
|
||||
console.log(` 2. Commit changes: git commit -am "chore: bump version to v${newVersion}"`);
|
||||
console.log(` 3. Create tag: git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
||||
console.log(` 4. Push changes: git push origin main`);
|
||||
console.log(` 5. Push tag: git push origin v${newVersion}`);
|
||||
} else {
|
||||
console.log('❌ Version bump cancelled');
|
||||
}
|
||||
|
||||
rl.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Run main function
|
||||
|
||||
@@ -12,7 +12,7 @@ const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Configuration
|
||||
const REGISTRY = 'docker.notsosm.art';
|
||||
const REGISTRY = 'euchre-camp';
|
||||
const IMAGE_NAME = 'euchre-camp';
|
||||
|
||||
// Get current version from package.json
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { describe, it, expect, mock, beforeEach } from 'bun:test'
|
||||
import { describe, it, expect, vi, beforeEach, MockedFunction } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import EditTournamentForm from '@/components/EditTournamentForm'
|
||||
|
||||
// Mock next/navigation
|
||||
mock.module('next/navigation', () => ({
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mock(() => {}),
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock next/link
|
||||
mock.module('next/link', () => ({
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = mock(async () => new Response())
|
||||
global.fetch = mockFetch as any
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
||||
|
||||
const mockTournament = {
|
||||
id: 1,
|
||||
@@ -40,8 +40,7 @@ const mockTournament = {
|
||||
|
||||
describe('EditTournamentForm', () => {
|
||||
beforeEach(() => {
|
||||
// Only clear the specific mocks we create, not global module mocks
|
||||
mockFetch.mockClear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders form with initial values', () => {
|
||||
|
||||
@@ -5,40 +5,31 @@
|
||||
* Tests the navigation component for proper display based on session state
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import Navigation from '@/components/Navigation'
|
||||
|
||||
// Mock next/link
|
||||
mock.module('next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock the SessionProvider
|
||||
mock.module('@/components/SessionProvider', () => ({
|
||||
useSession: mock(() => ({ session: null, loading: false, refreshSession: mock(() => {}) })),
|
||||
vi.mock('@/components/SessionProvider', () => ({
|
||||
useSession: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the auth-client
|
||||
mock.module('@/lib/auth-client', () => ({
|
||||
vi.mock('@/lib/auth-client', () => ({
|
||||
authClient: {
|
||||
signOut: mock(() => {}),
|
||||
signOut: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock fetch for role API call
|
||||
global.fetch = mock(async () => new Response()) as any
|
||||
global.fetch = vi.fn()
|
||||
|
||||
import { useSession as useSessionOriginal } from '@/components/SessionProvider'
|
||||
const useSession = useSessionOriginal as any
|
||||
import { useSession } from '@/components/SessionProvider'
|
||||
|
||||
describe('Epic 1: Navigation Component', () => {
|
||||
beforeEach(() => {
|
||||
// Don't clear all mocks as it might affect bun-setup.ts
|
||||
// Set up default fetch mock
|
||||
(global.fetch as any).mockImplementation?.(async (url: any) => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
||||
if (url?.toString().includes('/api/users/')) {
|
||||
return new Response(JSON.stringify({ role: 'player' }), {
|
||||
status: 200,
|
||||
@@ -50,15 +41,14 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Don't clear mocks - let them persist for other test files
|
||||
// Navigation relies on module mocks set up at file level
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders the logo and basic links when not logged in', () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: null,
|
||||
loading: false,
|
||||
refreshSession: mock(() => {}),
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
render(<Navigation />)
|
||||
@@ -70,7 +60,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
it('shows user menu when logged in', async () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: {
|
||||
user: {
|
||||
id: 'user-123',
|
||||
@@ -81,7 +71,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
session: { token: 'abc123' },
|
||||
},
|
||||
loading: false,
|
||||
refreshSession: mock(() => {}),
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
render(<Navigation />)
|
||||
@@ -95,7 +85,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
it('shows Tournaments link when logged in', async () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: {
|
||||
user: {
|
||||
id: 'user-123',
|
||||
@@ -106,7 +96,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
session: { token: 'abc123' },
|
||||
},
|
||||
loading: false,
|
||||
refreshSession: mock(() => {}),
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
render(<Navigation />)
|
||||
@@ -117,7 +107,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
it('shows admin link for club_admin role', async () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: {
|
||||
user: {
|
||||
id: 'admin-123',
|
||||
@@ -128,11 +118,11 @@ describe('Epic 1: Navigation Component', () => {
|
||||
session: { token: 'abc123' },
|
||||
},
|
||||
loading: false,
|
||||
refreshSession: mock(() => {}),
|
||||
});
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
// Mock fetch to return club_admin role
|
||||
(global.fetch as any).mockImplementation(async (url: any) => {
|
||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
||||
if (url?.toString().includes('/api/users/admin-123/role')) {
|
||||
return new Response(JSON.stringify({ role: 'club_admin' }), {
|
||||
status: 200,
|
||||
@@ -151,7 +141,7 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
it('hides admin link for non-admin users', async () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: {
|
||||
user: {
|
||||
id: 'player-123',
|
||||
@@ -162,11 +152,11 @@ describe('Epic 1: Navigation Component', () => {
|
||||
session: { token: 'abc123' },
|
||||
},
|
||||
loading: false,
|
||||
refreshSession: mock(() => {}),
|
||||
});
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
// Mock fetch to return player role (already set in beforeEach, but explicit here)
|
||||
(global.fetch as any).mockImplementation(async (url: any) => {
|
||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
||||
if (url?.toString().includes('/api/users/')) {
|
||||
return {
|
||||
json: () => Promise.resolve({ role: 'player' }),
|
||||
@@ -186,10 +176,10 @@ describe('Epic 1: Navigation Component', () => {
|
||||
})
|
||||
|
||||
it('shows loading state', () => {
|
||||
(useSession).mockReturnValue({
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
session: null,
|
||||
loading: true,
|
||||
refreshSession: mock(() => {}),
|
||||
refreshSession: vi.fn(),
|
||||
})
|
||||
|
||||
render(<Navigation />)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { describe, it, expect, mock, beforeEach } from 'bun:test'
|
||||
import { describe, it, expect, vi, beforeEach, MockedFunction } from 'vitest'
|
||||
import { getSession } from '@/lib/auth-simple'
|
||||
|
||||
// Mock next/headers
|
||||
mock.module('next/headers', () => ({
|
||||
cookies: mock(() => Promise.resolve({
|
||||
get: mock(() => ({ name: 'better-auth.session_token', value: 'test-token' })),
|
||||
})),
|
||||
headers: mock(() => Promise.resolve({
|
||||
get: mock(() => null),
|
||||
})),
|
||||
vi.mock('next/headers', () => ({
|
||||
cookies: vi.fn().mockResolvedValue({
|
||||
get: vi.fn().mockReturnValue({ name: 'better-auth.session_token', value: 'test-token' }),
|
||||
}),
|
||||
headers: vi.fn().mockResolvedValue({
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = mock(async () => new Response())
|
||||
global.fetch = mockFetch as any
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
||||
|
||||
describe('getSession', () => {
|
||||
beforeEach(() => {
|
||||
mock.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns session data when auth API returns 200', async () => {
|
||||
@@ -26,7 +26,7 @@ describe('getSession', () => {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
}
|
||||
|
||||
mockFetch.mockImplementation(async () =>
|
||||
mockFetch.mockResolvedValue(
|
||||
new Response(JSON.stringify(mockSession), { status: 200 })
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('getSession', () => {
|
||||
})
|
||||
|
||||
it('returns null when auth API returns non-200 status', async () => {
|
||||
mockFetch.mockImplementation(async () =>
|
||||
mockFetch.mockResolvedValue(
|
||||
new Response(null, { status: 401 })
|
||||
)
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Setup file for Bun test runner to provide DOM environment
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { mock } from 'bun:test';
|
||||
|
||||
console.log('Loading bun-setup.ts...');
|
||||
|
||||
// Mock @prisma/client to avoid dependency on generated Prisma client
|
||||
// This allows unit tests to run without requiring `prisma generate`
|
||||
mock.module('@prisma/client', () => {
|
||||
return {
|
||||
PrismaClient: class MockPrismaClient {
|
||||
$connect() {}
|
||||
$disconnect() {}
|
||||
$transaction() {}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||
url: 'http://localhost',
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
|
||||
(global as any).window = dom.window;
|
||||
(global as any).document = dom.window.document;
|
||||
(global as any).navigator = dom.window.navigator;
|
||||
|
||||
console.log('bun-setup.ts loaded - document:', typeof (global as any).document);
|
||||
console.log('document.body:', (global as any).document?.body);
|
||||
|
||||
// Import jest-dom matchers after setting up the DOM
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Clear document body after each test
|
||||
import { afterEach } from 'bun:test';
|
||||
afterEach(() => {
|
||||
if (global.document?.body) {
|
||||
global.document.body.innerHTML = '';
|
||||
}
|
||||
});
|
||||
+3
-3
@@ -79,7 +79,7 @@ test.describe('CSV Upload Player Deduplication', () => {
|
||||
formData.append('eventId', testTournamentId.toString());
|
||||
|
||||
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
||||
multipart: formData,
|
||||
data: formData,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
@@ -133,7 +133,7 @@ test.describe('CSV Upload Player Deduplication', () => {
|
||||
formData.append('eventId', testTournamentId.toString());
|
||||
|
||||
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
||||
multipart: formData,
|
||||
data: formData,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
@@ -190,7 +190,7 @@ test.describe('CSV Upload Player Deduplication', () => {
|
||||
formData.append('eventId', testTournamentId.toString());
|
||||
|
||||
const response = await request.post('http://localhost:3000/api/matches/upload', {
|
||||
multipart: formData,
|
||||
data: formData,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
+3
-3
@@ -41,7 +41,7 @@ test.describe('Tournament Edit - allowTies functionality', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should display allowTies checkbox on edit form @chromium-admin', async ({ page }) => {
|
||||
test('should display allowTies checkbox on edit form', async ({ page }) => {
|
||||
// Navigate to tournament edit page
|
||||
await page.goto(`/admin/tournaments/${tournamentId}/edit`);
|
||||
|
||||
@@ -54,7 +54,7 @@ test.describe('Tournament Edit - allowTies functionality', () => {
|
||||
await expect(allowTiesCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('should save allowTies when toggled to true @chromium-admin', async ({ page }) => {
|
||||
test('should save allowTies when toggled to true', async ({ page }) => {
|
||||
// Navigate to tournament edit page
|
||||
await page.goto(`/admin/tournaments/${tournamentId}/edit`);
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe('Tournament Edit - allowTies functionality', () => {
|
||||
expect(updatedTournament?.allowTies).toBe(true);
|
||||
});
|
||||
|
||||
test('should save allowTies when toggled to false @chromium-admin', async ({ page }) => {
|
||||
test('should save allowTies when toggled to false', async ({ page }) => {
|
||||
// First, set allowTies to true
|
||||
await prisma.event.update({
|
||||
where: { id: tournamentId },
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
|
||||
import { type expect } from 'bun:test'
|
||||
import { type TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'
|
||||
|
||||
declare module 'bun:test' {
|
||||
interface Matchers<T = any>
|
||||
extends TestingLibraryMatchers<
|
||||
ReturnType<typeof expect.stringContaining>,
|
||||
T
|
||||
> {}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* Tests the mathematical correctness of Elo calculations and rating updates
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { calculateEloChange, calculateExpectedScore, calculateTeamElo } from '@/lib/elo-utils';
|
||||
|
||||
describe('Elo Rating System', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Tests for ID parsing and validation in API routes and pages
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
|
||||
describe('ID Validation', () => {
|
||||
describe('parseInt with validation', () => {
|
||||
|
||||
@@ -4,33 +4,12 @@
|
||||
* Tests the permission system for tournament management
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
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
|
||||
const getSessionMock = mock(() => {});
|
||||
const userFindUniqueMock = mock(() => {});
|
||||
const eventFindUniqueMock = mock(() => {});
|
||||
|
||||
// Mock the getSession and prisma functions
|
||||
mock.module('@/lib/auth-simple', () => ({
|
||||
getSession: getSessionMock,
|
||||
}));
|
||||
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findUnique: userFindUniqueMock,
|
||||
},
|
||||
event: {
|
||||
findUnique: eventFindUniqueMock,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper to create mock user
|
||||
const createMockUser = (id: string, email: string, role: string): User => ({
|
||||
id,
|
||||
@@ -44,21 +23,30 @@ const createMockUser = (id: string, email: string, role: string): User => ({
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
describe('Permissions', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock implementations to default (no-op) before each test
|
||||
getSessionMock.mockImplementation(() => undefined);
|
||||
userFindUniqueMock.mockImplementation(() => undefined);
|
||||
eventFindUniqueMock.mockImplementation(() => undefined);
|
||||
});
|
||||
// Mock the getSession and prisma functions
|
||||
vi.mock('@/lib/auth-simple', () => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
event: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Permissions', () => {
|
||||
describe('hasRole', () => {
|
||||
test('should allow club_admin to access tournament_admin resources', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: '1', email: 'test@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('1', 'test@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -67,11 +55,11 @@ describe('Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny player from accessing tournament_admin resources', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: '1', email: 'test@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('1', 'test@example.com', 'player')
|
||||
);
|
||||
|
||||
@@ -80,7 +68,7 @@ describe('Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny unauthenticated user', async () => {
|
||||
getSessionMock.mockImplementation(async () => null);
|
||||
vi.mocked(getSession).mockResolvedValue(null);
|
||||
|
||||
const result = await hasRole('club_admin');
|
||||
expect(result.allowed).toBe(false);
|
||||
@@ -90,11 +78,11 @@ describe('Permissions', () => {
|
||||
|
||||
describe('canManageTournament', () => {
|
||||
test('should allow club_admin to manage any tournament', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -103,11 +91,11 @@ describe('Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny player from managing tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
|
||||
@@ -119,11 +107,11 @@ describe('Permissions', () => {
|
||||
|
||||
describe('canCreateTournaments', () => {
|
||||
test('should allow tournament_admin to create tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'tournament_admin')
|
||||
);
|
||||
|
||||
@@ -132,11 +120,11 @@ describe('Permissions', () => {
|
||||
});
|
||||
|
||||
test('should allow club_admin to create tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -145,11 +133,11 @@ describe('Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny player from creating tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
|
||||
|
||||
@@ -4,19 +4,15 @@
|
||||
* Tests the player deduplication logic for CSV uploads
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock, beforeEach,} from 'bun:test';
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Create mock functions at module level
|
||||
const playerFindFirstMock = mock(() => {});
|
||||
const playerCreateMock = mock(() => {});
|
||||
|
||||
// Mock the prisma module
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
player: {
|
||||
findFirst: playerFindFirstMock,
|
||||
create: playerCreateMock,
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -50,9 +46,7 @@ async function findOrCreatePlayer(name: string) {
|
||||
|
||||
describe('Player Deduplication', () => {
|
||||
beforeEach(() => {
|
||||
// Clear all mock history before each test
|
||||
playerFindFirstMock.mockClear();
|
||||
playerCreateMock.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findOrCreatePlayer', () => {
|
||||
@@ -70,7 +64,7 @@ describe('Player Deduplication', () => {
|
||||
rating: 0,
|
||||
};
|
||||
|
||||
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer('Emily');
|
||||
|
||||
@@ -95,7 +89,7 @@ describe('Player Deduplication', () => {
|
||||
rating: 0,
|
||||
};
|
||||
|
||||
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer('EMILY');
|
||||
|
||||
@@ -120,7 +114,7 @@ describe('Player Deduplication', () => {
|
||||
rating: 0,
|
||||
};
|
||||
|
||||
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer(' Emily ');
|
||||
|
||||
@@ -132,7 +126,7 @@ describe('Player Deduplication', () => {
|
||||
});
|
||||
|
||||
test('should create new player if not found', async () => {
|
||||
playerFindFirstMock.mockImplementation(async () => null);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
||||
|
||||
const newPlayer = {
|
||||
id: 100,
|
||||
@@ -147,7 +141,7 @@ describe('Player Deduplication', () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer('NewPlayer');
|
||||
|
||||
@@ -168,7 +162,7 @@ describe('Player Deduplication', () => {
|
||||
});
|
||||
|
||||
test('should handle names with special characters', async () => {
|
||||
playerFindFirstMock.mockImplementation(async () => null);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
||||
|
||||
const newPlayer = {
|
||||
id: 100,
|
||||
@@ -183,7 +177,7 @@ describe('Player Deduplication', () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer('Test-Player_123');
|
||||
|
||||
@@ -201,7 +195,7 @@ describe('Player Deduplication', () => {
|
||||
});
|
||||
|
||||
test('should handle names with spaces', async () => {
|
||||
playerFindFirstMock.mockImplementation(async () => null);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
||||
|
||||
const newPlayer = {
|
||||
id: 100,
|
||||
@@ -216,7 +210,7 @@ describe('Player Deduplication', () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer('Dave B');
|
||||
|
||||
@@ -248,7 +242,7 @@ describe('Player Deduplication', () => {
|
||||
rating: 0,
|
||||
};
|
||||
|
||||
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
||||
|
||||
const result1 = await findOrCreatePlayer('EMILY');
|
||||
const result2 = await findOrCreatePlayer('Emily');
|
||||
@@ -261,7 +255,7 @@ describe('Player Deduplication', () => {
|
||||
});
|
||||
|
||||
test('should handle empty or whitespace-only names', async () => {
|
||||
playerFindFirstMock.mockImplementation(async () => null);
|
||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
||||
|
||||
const newPlayer = {
|
||||
id: 100,
|
||||
@@ -276,7 +270,7 @@ describe('Player Deduplication', () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
||||
|
||||
const result = await findOrCreatePlayer(' ');
|
||||
|
||||
@@ -326,7 +320,7 @@ describe('Player Deduplication', () => {
|
||||
rating: 0,
|
||||
};
|
||||
|
||||
playerFindFirstMock
|
||||
vi.mocked(prisma.player.findFirst)
|
||||
.mockResolvedValueOnce(existingPlayer) // First call for "Emily"
|
||||
.mockResolvedValueOnce(existingPlayer) // Second call for "EMILY"
|
||||
.mockResolvedValueOnce(existingPlayer); // Third call for " Emily "
|
||||
|
||||
@@ -7,30 +7,24 @@
|
||||
* - Partnership performance
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import type { Player, Event, Match, PartnershipStat } from '@prisma/client';
|
||||
|
||||
// Create mock functions at module level
|
||||
const playerFindUniqueMock = mock(() => {});
|
||||
const eventFindManyMock = mock(() => {});
|
||||
const matchFindManyMock = mock(() => {});
|
||||
const partnershipStatFindManyMock = mock(() => {});
|
||||
|
||||
// Mock the prisma module
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
player: {
|
||||
findUnique: playerFindUniqueMock,
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
event: {
|
||||
findMany: eventFindManyMock,
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
match: {
|
||||
findMany: matchFindManyMock,
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
partnershipStat: {
|
||||
findMany: partnershipStatFindManyMock,
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -117,11 +111,7 @@ const createMockPartnershipStat = (
|
||||
|
||||
describe('Player Profile Enhancements', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock implementations to default (no-op) before each test
|
||||
playerFindUniqueMock.mockImplementation(() => undefined);
|
||||
eventFindManyMock.mockImplementation(() => undefined);
|
||||
matchFindManyMock.mockImplementation(() => undefined);
|
||||
partnershipStatFindManyMock.mockImplementation(() => undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Tournaments Participated', () => {
|
||||
@@ -132,8 +122,8 @@ describe('Player Profile Enhancements', () => {
|
||||
createMockTournament(2, 'Tournament B'),
|
||||
];
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
||||
|
||||
// Simulate the query that would be run
|
||||
const tournaments = await prisma.event.findMany({
|
||||
@@ -155,8 +145,8 @@ describe('Player Profile Enhancements', () => {
|
||||
test('should return empty array if player has no tournaments', async () => {
|
||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
eventFindManyMock.mockImplementation(async () => []);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.event.findMany).mockResolvedValue([]);
|
||||
|
||||
const tournaments = await prisma.event.findMany({
|
||||
where: {
|
||||
@@ -183,8 +173,8 @@ describe('Player Profile Enhancements', () => {
|
||||
createMockMatch(2, 1, 3, 5, 6, 4, 4),
|
||||
];
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
matchFindManyMock.mockImplementation(async () => mockMatches);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.match.findMany).mockResolvedValue(mockMatches);
|
||||
|
||||
// Simulate the query that would be run
|
||||
const matches = await prisma.match.findMany({
|
||||
@@ -214,8 +204,8 @@ describe('Player Profile Enhancements', () => {
|
||||
test('should return empty array if player has no matches', async () => {
|
||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
matchFindManyMock.mockImplementation(async () => []);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.match.findMany).mockResolvedValue([]);
|
||||
|
||||
const matches = await prisma.match.findMany({
|
||||
where: {
|
||||
@@ -250,8 +240,8 @@ describe('Player Profile Enhancements', () => {
|
||||
createMockPartnershipStat(2, 1, 3, 5, 2, 3),
|
||||
];
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
partnershipStatFindManyMock.mockImplementation(async () => mockPartnershipStats);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.partnershipStat.findMany).mockResolvedValue(mockPartnershipStats);
|
||||
|
||||
// Simulate the query that would be run
|
||||
const partnershipStats = await prisma.partnershipStat.findMany({
|
||||
@@ -275,8 +265,8 @@ describe('Player Profile Enhancements', () => {
|
||||
test('should return empty array if player has no partnership data', async () => {
|
||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||
|
||||
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||
partnershipStatFindManyMock.mockImplementation(async () => []);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
||||
vi.mocked(prisma.partnershipStat.findMany).mockResolvedValue([]);
|
||||
|
||||
const partnershipStats = await prisma.partnershipStat.findMany({
|
||||
where: {
|
||||
|
||||
@@ -5,33 +5,33 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||
import { recalculateAllElo } from '@/lib/elo-utils';
|
||||
|
||||
// Mock Prisma client
|
||||
const mockPrisma = {
|
||||
player: {
|
||||
updateMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||
update: mock(() => {}).mockResolvedValue({}),
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
eloSnapshot: {
|
||||
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||
create: mock(() => {}).mockResolvedValue({}),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
partnershipStat: {
|
||||
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||
findFirst: mock(() => {}).mockResolvedValue(null), // No existing stats initially
|
||||
update: mock(() => {}).mockResolvedValue({}),
|
||||
create: mock(() => {}).mockResolvedValue({}),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
findFirst: vi.fn().mockResolvedValue(null), // No existing stats initially
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
match: {
|
||||
findMany: mock(() => {}).mockResolvedValue([]),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
|
||||
describe('recalculateAllElo', () => {
|
||||
beforeEach(() => {
|
||||
mock.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should reset all player stats to zero', async () => {
|
||||
|
||||
@@ -5,31 +5,25 @@
|
||||
* Regression tests for the issue where tournament_admin users were redirected to login
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { canManageTournament, ownsTournament, getManageableTournaments } from '@/lib/permissions';
|
||||
import { getSession } from '@/lib/auth-simple';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import type { User, Event } from '@prisma/client';
|
||||
|
||||
// Create mock functions first
|
||||
const getSessionMock = mock(() => {});
|
||||
const userFindUniqueMock = mock(() => {});
|
||||
const eventFindUniqueMock = mock(() => {});
|
||||
const eventFindManyMock = mock(() => {});
|
||||
|
||||
// Mock the getSession and prisma functions
|
||||
mock.module('@/lib/auth-simple', () => ({
|
||||
getSession: getSessionMock,
|
||||
vi.mock('@/lib/auth-simple', () => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findUnique: userFindUniqueMock,
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
event: {
|
||||
findUnique: eventFindUniqueMock,
|
||||
findMany: eventFindManyMock,
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -67,21 +61,16 @@ const createMockTournament = (id: number, ownerId: string | null): Event => ({
|
||||
|
||||
describe('Tournament Permissions', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock implementations to default (no-op) before each test
|
||||
// This prevents pollution from previous tests
|
||||
getSessionMock.mockImplementation(() => undefined);
|
||||
userFindUniqueMock.mockImplementation(() => undefined);
|
||||
eventFindUniqueMock.mockImplementation(() => undefined);
|
||||
eventFindManyMock.mockImplementation(() => undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('canManageTournament', () => {
|
||||
test('should allow club_admin to manage any tournament', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -90,14 +79,14 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should allow tournament_admin to manage their own tournament', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||
);
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
||||
createMockTournament(1, 'tour-admin-1')
|
||||
);
|
||||
|
||||
@@ -106,14 +95,14 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny tournament_admin from managing other users tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||
);
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
||||
createMockTournament(1, 'other-user-1')
|
||||
);
|
||||
|
||||
@@ -123,11 +112,11 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny player from managing tournaments', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
|
||||
@@ -137,7 +126,7 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should deny unauthenticated user', async () => {
|
||||
getSessionMock.mockImplementation(async () => null);
|
||||
vi.mocked(getSession).mockResolvedValue(null);
|
||||
|
||||
const result = await canManageTournament(999);
|
||||
expect(result.allowed).toBe(false);
|
||||
@@ -147,11 +136,11 @@ describe('Tournament Permissions', () => {
|
||||
|
||||
describe('ownsTournament', () => {
|
||||
test('should return true if user owns tournament', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'owner-1', email: 'owner@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
||||
createMockTournament(1, 'owner-1')
|
||||
);
|
||||
|
||||
@@ -160,11 +149,11 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should return false if user does not own tournament', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'non-owner-1', email: 'nonowner@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
||||
createMockTournament(1, 'owner-1')
|
||||
);
|
||||
|
||||
@@ -176,11 +165,11 @@ describe('Tournament Permissions', () => {
|
||||
|
||||
describe('getManageableTournaments', () => {
|
||||
test('should return all tournaments for club_admin', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -189,11 +178,11 @@ describe('Tournament Permissions', () => {
|
||||
createMockTournament(2, 'user-2'),
|
||||
createMockTournament(3, 'user-3'),
|
||||
];
|
||||
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
||||
|
||||
const result = await getManageableTournaments();
|
||||
expect(result).toEqual(mockTournaments);
|
||||
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
||||
where: { eventType: 'tournament' },
|
||||
include: { participants: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
@@ -201,11 +190,11 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should return only owned tournaments for tournament_admin', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||
);
|
||||
|
||||
@@ -213,11 +202,11 @@ describe('Tournament Permissions', () => {
|
||||
createMockTournament(1, 'tour-admin-1'),
|
||||
createMockTournament(2, 'tour-admin-1'),
|
||||
];
|
||||
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
||||
|
||||
const result = await getManageableTournaments();
|
||||
expect(result).toEqual(mockTournaments);
|
||||
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
eventType: 'tournament',
|
||||
ownerId: 'tour-admin-1'
|
||||
@@ -228,11 +217,11 @@ describe('Tournament Permissions', () => {
|
||||
});
|
||||
|
||||
test('should return only non-draft tournaments for players', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
|
||||
@@ -240,11 +229,11 @@ describe('Tournament Permissions', () => {
|
||||
createMockTournament(1, 'user-1'),
|
||||
createMockTournament(2, 'user-2'),
|
||||
];
|
||||
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
||||
|
||||
const result = await getManageableTournaments();
|
||||
expect(result).toEqual(mockTournaments);
|
||||
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
eventType: 'tournament',
|
||||
status: { not: 'draft' }
|
||||
@@ -260,14 +249,14 @@ describe('Tournament Permissions', () => {
|
||||
// This simulates the scenario where a tournament_admin user
|
||||
// clicks "Edit" on a tournament they own
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||
);
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
||||
createMockTournament(1, 'tour-admin-1')
|
||||
);
|
||||
|
||||
@@ -282,11 +271,11 @@ describe('Tournament Permissions', () => {
|
||||
test('club_admin should still be able to manage any tournament', async () => {
|
||||
// This ensures we didn't break the existing club_admin functionality
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'club-admin-1', email: 'club@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('club-admin-1', 'club@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -297,16 +286,13 @@ describe('Tournament Permissions', () => {
|
||||
test('players should still be denied from managing tournaments', async () => {
|
||||
// This ensures we didn't accidentally grant players access
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
eventFindUniqueMock.mockImplementation(async () =>
|
||||
createMockTournament(1, 'other-user-1')
|
||||
);
|
||||
|
||||
const result = await canManageTournament(1);
|
||||
expect(result.allowed).toBe(false);
|
||||
|
||||
@@ -3,33 +3,23 @@
|
||||
* Tests the allowTies field is properly saved when updating tournaments
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach,} from 'bun:test';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Create mock functions at module level
|
||||
const eventFindUniqueMock = mock(() => {});
|
||||
const eventUpdateMock = mock(() => {});
|
||||
const canManageTournamentMock = mock(() => {});
|
||||
const canDeleteTournamentMock = mock(() => {});
|
||||
|
||||
// Store default implementations
|
||||
const defaultCanManageTournament = canManageTournamentMock.mockResolvedValue({ allowed: true });
|
||||
const defaultCanDeleteTournament = canDeleteTournamentMock.mockResolvedValue({ allowed: true });
|
||||
|
||||
// Mock the prisma client
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
event: {
|
||||
findUnique: eventFindUniqueMock,
|
||||
update: eventUpdateMock,
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the permissions module
|
||||
mock.module('@/lib/permissions', () => ({
|
||||
canManageTournament: defaultCanManageTournament,
|
||||
canDeleteTournament: defaultCanDeleteTournament,
|
||||
vi.mock('@/lib/permissions', () => ({
|
||||
canManageTournament: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
canDeleteTournament: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
}));
|
||||
|
||||
// Import the route handler after mocking
|
||||
@@ -37,16 +27,12 @@ import { PUT } from '@/app/api/tournaments/[id]/route';
|
||||
|
||||
describe('Tournament Update API', () => {
|
||||
beforeEach(() => {
|
||||
// Clear all mock history before each test
|
||||
eventFindUniqueMock.mockClear();
|
||||
eventUpdateMock.mockClear();
|
||||
canManageTournamentMock.mockClear();
|
||||
canDeleteTournamentMock.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should update allowTies field when provided', async () => {
|
||||
// Mock existing tournament
|
||||
eventFindUniqueMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Test Tournament',
|
||||
allowTies: false,
|
||||
@@ -60,10 +46,10 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
// Mock successful update
|
||||
eventUpdateMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Test Tournament',
|
||||
allowTies: true,
|
||||
@@ -77,7 +63,7 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
const request = new Request('http://localhost/api/tournaments/1', {
|
||||
method: 'PUT',
|
||||
@@ -92,7 +78,7 @@ describe('Tournament Update API', () => {
|
||||
const response = await PUT(request, { params });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(prisma.event.update).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(prisma.event.update)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
allowTies: true,
|
||||
@@ -103,7 +89,7 @@ describe('Tournament Update API', () => {
|
||||
|
||||
it('should default allowTies to false when not provided', async () => {
|
||||
// Mock existing tournament
|
||||
eventFindUniqueMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Test Tournament',
|
||||
allowTies: true,
|
||||
@@ -117,10 +103,10 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
// Mock successful update
|
||||
eventUpdateMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Test Tournament',
|
||||
allowTies: false,
|
||||
@@ -134,7 +120,7 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
const request = new Request('http://localhost/api/tournaments/1', {
|
||||
method: 'PUT',
|
||||
@@ -149,7 +135,7 @@ describe('Tournament Update API', () => {
|
||||
const response = await PUT(request, { params });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(prisma.event.update).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(prisma.event.update)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
allowTies: false, // Should default to false
|
||||
@@ -160,7 +146,7 @@ describe('Tournament Update API', () => {
|
||||
|
||||
it('should preserve allowTies value when updating other fields', async () => {
|
||||
// Mock existing tournament with allowTies = true
|
||||
eventFindUniqueMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Test Tournament',
|
||||
allowTies: true,
|
||||
@@ -174,10 +160,10 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
// Mock successful update
|
||||
eventUpdateMock.mockImplementation(async () => ({
|
||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Updated Tournament Name',
|
||||
allowTies: true,
|
||||
@@ -191,7 +177,7 @@ describe('Tournament Update API', () => {
|
||||
ownerId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any));
|
||||
} as any);
|
||||
|
||||
const request = new Request('http://localhost/api/tournaments/1', {
|
||||
method: 'PUT',
|
||||
@@ -206,7 +192,7 @@ describe('Tournament Update API', () => {
|
||||
const response = await PUT(request, { params });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const updateCall = eventUpdateMock.mock.calls[0][0];
|
||||
const updateCall = vi.mocked(prisma.event.update).mock.calls[0][0];
|
||||
expect(updateCall.data.allowTies).toBe(true);
|
||||
expect(updateCall.data.name).toBe('Updated Tournament Name');
|
||||
expect(updateCall.data.targetScore).toBe(10);
|
||||
|
||||
@@ -4,31 +4,25 @@
|
||||
* Tests for user name editing and profile management
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock, beforeEach,} from 'bun:test';
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { hasRole } from '@/lib/permissions';
|
||||
import { getSession } from '@/lib/auth-simple';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import type { User, Player } from '@prisma/client';
|
||||
|
||||
// Create mock functions at module level
|
||||
const getSessionMock = mock(() => {});
|
||||
const userFindUniqueMock = mock(() => {});
|
||||
const userUpdateMock = mock(() => {});
|
||||
const playerFindUniqueMock = mock(() => {});
|
||||
|
||||
// Mock the getSession and prisma functions
|
||||
mock.module('@/lib/auth-simple', () => ({
|
||||
getSession: getSessionMock,
|
||||
vi.mock('@/lib/auth-simple', () => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findUnique: userFindUniqueMock,
|
||||
update: userUpdateMock,
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
player: {
|
||||
findUnique: playerFindUniqueMock,
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -62,20 +56,16 @@ const createMockPlayer = (id: number, name: string): Player => ({
|
||||
|
||||
describe('User Management', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock implementations to default (no-op) before each test
|
||||
getSessionMock.mockImplementation(() => undefined);
|
||||
userFindUniqueMock.mockImplementation(() => undefined);
|
||||
userUpdateMock.mockImplementation(() => undefined);
|
||||
playerFindUniqueMock.mockImplementation(() => undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('User Name Editing', () => {
|
||||
test('club_admin should be able to edit any user name', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||
);
|
||||
|
||||
@@ -84,11 +74,11 @@ describe('User Management', () => {
|
||||
});
|
||||
|
||||
test('tournament_admin should NOT be able to edit user names', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||
);
|
||||
|
||||
@@ -97,11 +87,11 @@ describe('User Management', () => {
|
||||
});
|
||||
|
||||
test('player should NOT be able to edit user names', async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'player-1', email: 'player@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () =>
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
||||
createMockUser('player-1', 'player@example.com', 'player')
|
||||
);
|
||||
|
||||
@@ -110,7 +100,7 @@ describe('User Management', () => {
|
||||
});
|
||||
|
||||
test('unauthenticated user should NOT be able to edit user names', async () => {
|
||||
getSessionMock.mockImplementation(async () => null);
|
||||
vi.mocked(getSession).mockResolvedValue(null);
|
||||
|
||||
const result = await hasRole('club_admin');
|
||||
expect(result.allowed).toBe(false);
|
||||
@@ -121,11 +111,11 @@ describe('User Management', () => {
|
||||
test('user should be able to view their own profile', async () => {
|
||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'user-1', email: 'user@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||
|
||||
// In the actual implementation, this would check if session.user.id === params.id
|
||||
const canViewOwnProfile = true; // This logic is in the API route
|
||||
@@ -136,11 +126,11 @@ describe('User Management', () => {
|
||||
const mockAdmin = createMockUser('admin-1', 'admin@example.com', 'club_admin');
|
||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
(userFindUniqueMock)
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique)
|
||||
.mockResolvedValueOnce(mockAdmin) // For the requesting user
|
||||
.mockResolvedValueOnce(mockUser); // For the target user
|
||||
|
||||
@@ -152,11 +142,11 @@ describe('User Management', () => {
|
||||
test('non-admin should NOT be able to view other user profiles', async () => {
|
||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'user-1', email: 'user@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||
|
||||
// In the actual implementation, this would check if session.user.id === params.id
|
||||
const canViewOtherProfile = false; // This logic is in the API route
|
||||
@@ -169,11 +159,11 @@ describe('User Management', () => {
|
||||
const mockUser = createMockUser('user-1', 'user@example.com', 'club_admin');
|
||||
const mockPlayer = createMockPlayer(1, 'Old Name');
|
||||
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
user: { id: 'user-1', email: 'user@example.com' },
|
||||
session: { token: 'test', expiresAt: new Date() }
|
||||
}));
|
||||
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||
|
||||
const updatedUser = {
|
||||
...mockUser,
|
||||
@@ -181,7 +171,7 @@ describe('User Management', () => {
|
||||
player: { ...mockPlayer, name: 'New Name', normalizedName: 'new name' }
|
||||
};
|
||||
|
||||
userUpdateMock.mockImplementation(async () => updatedUser);
|
||||
vi.mocked(prisma.user.update).mockResolvedValue(updatedUser);
|
||||
|
||||
// The API route should update both user.name and player.name
|
||||
expect(updatedUser.name).toBe('New Name');
|
||||
|
||||
@@ -14,8 +14,6 @@ export const auth = betterAuth({
|
||||
enabled: true,
|
||||
autoSignIn: true, // Automatically sign in after registration
|
||||
requireEmailVerification: false, // Don't require email verification for tests
|
||||
minPasswordLength: 8, // Set minimum password length
|
||||
maxPasswordLength: 128, // Set maximum password length
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET,
|
||||
baseURL: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL || "http://localhost:3000",
|
||||
|
||||
+4
-11
@@ -1,7 +1,8 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
// Load .env file if it exists
|
||||
require('dotenv').config()
|
||||
dotenv.config()
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
@@ -9,24 +10,16 @@ const globalForPrisma = globalThis as unknown as {
|
||||
|
||||
// Detect database provider from environment (default to sqlite for local development)
|
||||
const databaseProvider = process.env.DATABASE_PROVIDER || 'sqlite'
|
||||
const databaseUrl = process.env.DATABASE_URL
|
||||
|
||||
// Create PrismaClient with appropriate adapter
|
||||
const createPrismaClient = () => {
|
||||
let client: PrismaClient
|
||||
|
||||
if (databaseProvider === 'postgresql') {
|
||||
// Validate DATABASE_URL is present for PostgreSQL
|
||||
if (!databaseUrl) {
|
||||
throw new Error(
|
||||
'DATABASE_URL environment variable is required when DATABASE_PROVIDER is set to postgresql. ' +
|
||||
'Current DATABASE_PROVIDER: ' + databaseProvider
|
||||
)
|
||||
}
|
||||
|
||||
// Use PrismaPg adapter for PostgreSQL
|
||||
const { PrismaPg } = require('@prisma/adapter-pg')
|
||||
const adapter = new PrismaPg({ connectionString: databaseUrl })
|
||||
const pg = require('pg')
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
||||
client = new PrismaClient({ adapter })
|
||||
} else {
|
||||
// No adapter needed for SQLite
|
||||
|
||||
Reference in New Issue
Block a user