Compare commits
13 Commits
v0.1.3
...
e8f0fd2538
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f0fd2538 | |||
| 2e98e9eb51 | |||
| 3a7a9b4b8e | |||
| 66ee38f456 | |||
| 4e3b25e2fc | |||
| e4c4333b40 | |||
| 09302fd911 | |||
| 0d9707ea27 | |||
| b90ec08966 | |||
| 1cd2cbd0a6 | |||
| 24db43eb7f | |||
| 1c9f70c3ed | |||
| 5caa284b3c |
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
|
||||||
"ignorePatterns": [
|
|
||||||
".next",
|
|
||||||
"out",
|
|
||||||
"build",
|
|
||||||
"next-env.d.ts",
|
|
||||||
"src/app/test-api/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+11
-10
@@ -9,7 +9,7 @@ jobs:
|
|||||||
unit-tests:
|
unit-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: node:20-alpine
|
image: oven/bun:alpine
|
||||||
options: --user root
|
options: --user root
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -17,10 +17,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: bun install
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm run test:run
|
run: bun test src/__tests__/unit/ src/__tests__/*.test.tsx src/__tests__/auth-simple.test.ts
|
||||||
|
|
||||||
acceptance-tests:
|
acceptance-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -37,16 +37,16 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Bun
|
||||||
uses: actions/setup-node@v4
|
uses: oven/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: bun install
|
||||||
|
|
||||||
- name: Generate Prisma client
|
- name: Generate Prisma client
|
||||||
run: npx prisma generate
|
run: bun x prisma generate
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: postgresql://user:pass@localhost:5432/dummy
|
DATABASE_URL: postgresql://user:pass@localhost:5432/dummy
|
||||||
|
|
||||||
@@ -54,14 +54,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Create SQLite database file
|
# Create SQLite database file
|
||||||
mkdir -p prisma
|
mkdir -p prisma
|
||||||
npx prisma migrate deploy
|
bun x prisma migrate deploy
|
||||||
|
|
||||||
- name: Run acceptance tests
|
- name: Run acceptance tests
|
||||||
run: npm run test:acceptance
|
run: bun run test:acceptance
|
||||||
env:
|
env:
|
||||||
DATABASE_PROVIDER: sqlite
|
DATABASE_PROVIDER: sqlite
|
||||||
DATABASE_URL: file:./prisma/ci.db
|
DATABASE_URL: file:./prisma/ci.db
|
||||||
BETTER_AUTH_SECRET: test-secret-key-for-ci-only
|
BETTER_AUTH_SECRET: test-secret-key-for-ci-only
|
||||||
|
HOME: /root
|
||||||
|
|
||||||
analyze-bump-type:
|
analyze-bump-type:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
@@ -70,10 +75,10 @@ jobs:
|
|||||||
echo "Bumping version: $BUMP"
|
echo "Bumping version: $BUMP"
|
||||||
|
|
||||||
# Run the bump script
|
# Run the bump script
|
||||||
node scripts/bump-version.js "$BUMP" --yes
|
bun run scripts/bump-version.js "$BUMP" --yes
|
||||||
|
|
||||||
# Get new version
|
# Get new version
|
||||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
NEW_VERSION=$(bun -e "console.log(require('./package.json').version)")
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "New version: $NEW_VERSION"
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
@@ -123,7 +128,7 @@ jobs:
|
|||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" \
|
-e DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" \
|
||||||
${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \
|
${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \
|
||||||
npm run test:run
|
bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'
|
||||||
|
|
||||||
- name: Build production image
|
- name: Build production image
|
||||||
if: steps.commit.outputs.committed == 'true'
|
if: steps.commit.outputs.committed == 'true'
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: npm run test:run
|
|
||||||
@@ -9,11 +9,12 @@ EuchreCamp is a Next.js 14+ application for managing Euchre tournaments and trac
|
|||||||
### Key Technologies
|
### Key Technologies
|
||||||
- **Next.js 14+** (App Router)
|
- **Next.js 14+** (App Router)
|
||||||
- **TypeScript**
|
- **TypeScript**
|
||||||
- **Prisma ORM** (SQLite)
|
- **Prisma ORM** (SQLite/PostgreSQL)
|
||||||
- **Tailwind CSS**
|
- **Tailwind CSS**
|
||||||
- **Better Auth** (Authentication)
|
- **Better Auth** (Authentication)
|
||||||
- **Vitest** (Unit Testing)
|
- **Bun** (Package Manager & Test Runner)
|
||||||
- **Playwright** (Acceptance Testing)
|
- **Playwright** (Acceptance Testing)
|
||||||
|
- **Vitest** (Legacy - migrated to Bun test runner)
|
||||||
|
|
||||||
## Architecture Patterns
|
## Architecture Patterns
|
||||||
|
|
||||||
@@ -37,13 +38,55 @@ EuchreCamp is a Next.js 14+ application for managing Euchre tournaments and trac
|
|||||||
|
|
||||||
## Common Tasks
|
## 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
|
### Admin User Creation
|
||||||
|
|
||||||
To create an admin user, use the provided scripts:
|
To create an admin user, use the provided scripts:
|
||||||
|
|
||||||
**Option 1: Using Better Auth API (Recommended)**
|
**Option 1: Using Better Auth API (Recommended)**
|
||||||
```bash
|
```bash
|
||||||
node scripts/create-admin-via-api.js
|
bun run scripts/create-admin-via-api.js
|
||||||
```
|
```
|
||||||
This creates the admin user `david@dhg.lol` with password `adminadmin` using Better Auth's internal API.
|
This creates the admin user `david@dhg.lol` with password `adminadmin` using Better Auth's internal API.
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -1,9 +1,9 @@
|
|||||||
# Multi-stage build for EuchreCamp Next.js application
|
# Multi-stage build for EuchreCamp Next.js application
|
||||||
|
|
||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM node:20-alpine AS builder
|
FROM oven/bun:alpine AS builder
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies (needed for native modules)
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
@@ -13,21 +13,21 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies (including dev dependencies for building)
|
# Install dependencies (including dev dependencies for building)
|
||||||
RUN npm ci
|
RUN bun install
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Generate Prisma client (with dummy PostgreSQL DATABASE_URL for build-time generation)
|
# 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
|
# Note: A dummy URL is used since the real database is not available during build
|
||||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||||
|
|
||||||
# Build the application (with dummy DATABASE_URL for static page generation and git commit)
|
# Build the application (with dummy DATABASE_URL for static page generation and git commit)
|
||||||
ARG GIT_COMMIT=unknown
|
ARG GIT_COMMIT=unknown
|
||||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT npm run build
|
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT bun run build
|
||||||
|
|
||||||
# Stage 2: Test runner (includes dev dependencies for testing)
|
# Stage 2: Test runner (includes dev dependencies for testing)
|
||||||
FROM node:20-alpine AS test-runner
|
FROM oven/bun:alpine AS test-runner
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN apk add --no-cache python3 make g++ git
|
RUN apk add --no-cache python3 make g++ git
|
||||||
@@ -39,16 +39,16 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install ALL dependencies (including dev dependencies for testing)
|
# Install ALL dependencies (including dev dependencies for testing)
|
||||||
RUN npm ci
|
RUN bun install
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Generate Prisma client
|
# Generate Prisma client
|
||||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||||
|
|
||||||
# Stage 3: Production runner
|
# Stage 3: Production runner
|
||||||
FROM node:20-alpine AS runner
|
FROM oven/bun:alpine AS runner
|
||||||
|
|
||||||
# Install dumb-init for proper signal handling
|
# Install dumb-init for proper signal handling
|
||||||
RUN apk add --no-cache dumb-init
|
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/.next ./.next
|
||||||
COPY --from=builder --chown=euchre:euchre /app/public ./public
|
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/package.json ./package.json
|
||||||
COPY --from=builder --chown=euchre:euchre /app/package-lock.json ./package-lock.json
|
COPY --from=builder --chown=euchre:euchre /app/bun.lockb ./bun.lockb
|
||||||
COPY --from=builder --chown=euchre:euchre /app/prisma ./prisma
|
COPY --from=builder --chown=euchre:euchre /app/prisma ./prisma
|
||||||
|
|
||||||
# Install only production dependencies
|
# Install only production dependencies
|
||||||
RUN npm ci --omit=dev
|
RUN bun install --production
|
||||||
|
|
||||||
# Generate Prisma client
|
# Generate Prisma client
|
||||||
# Note: We need to set DATABASE_URL even for generation because prisma.config.ts requires it
|
# Note: We need to set DATABASE_URL even for generation because prisma.config.ts requires it
|
||||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" npx prisma generate
|
RUN DATABASE_PROVIDER=postgresql DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" bun x prisma generate
|
||||||
|
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER euchre
|
USER euchre
|
||||||
@@ -86,4 +86,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|||||||
|
|
||||||
# Start command
|
# Start command
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
CMD ["npm", "start"]
|
CMD ["bun", "run", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./src/__tests__/bun-setup.ts"]
|
||||||
|
exclude = ["e2e/**", "**/e2e/**"]
|
||||||
|
# isolation = true
|
||||||
@@ -30,6 +30,23 @@
|
|||||||
- [ ] Update MatchEditor to use tournament-specific target score
|
- [ ] Update MatchEditor to use tournament-specific target score
|
||||||
- [ ] Run tests and verify variant scoring implementation
|
- [ ] 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 ✅
|
### Recently Completed ✅
|
||||||
- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts)
|
- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts)
|
||||||
- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts)
|
- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import next from 'eslint-config-next';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'.next',
|
||||||
|
'out',
|
||||||
|
'build',
|
||||||
|
'next-env.d.ts',
|
||||||
|
'src/app/test-api/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...next,
|
||||||
|
];
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
|
bun = "latest"
|
||||||
docker-compose = "latest"
|
docker-compose = "latest"
|
||||||
just = "latest"
|
just = "latest"
|
||||||
node = "latest"
|
node = "latest"
|
||||||
|
|||||||
+26
-22
@@ -6,33 +6,37 @@
|
|||||||
"dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev",
|
"dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "bun run eslint",
|
||||||
"test": "vitest",
|
"test": "bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'",
|
||||||
"test:run": "vitest run",
|
"test:unit": "bun test src/__tests__/unit/",
|
||||||
"test:acceptance": "playwright test src/__tests__/e2e/",
|
"test:component": "bun test src/__tests__/*.test.tsx",
|
||||||
"test:acceptance:headed": "playwright test src/__tests__/e2e/ --headed",
|
"test:run": "bun test 'src/__tests__/unit/**' 'src/__tests__/*.test.tsx' 'src/__tests__/auth-simple.test.ts'",
|
||||||
"db:switch": "node scripts/switch-database.js",
|
"test:randomize": "bun test src/__tests__/unit/ --randomize",
|
||||||
"db:setup-postgres": "node scripts/setup-postgres.js",
|
"test:unit:sequential": "bun test src/__tests__/unit/ --max-concurrency=1",
|
||||||
"db:setup-dev": "node scripts/setup-postgres.js",
|
"test:acceptance": "bun x playwright test e2e/",
|
||||||
"db:setup-dev:clean": "node scripts/setup-postgres.js --drop",
|
"test:acceptance:headed": "bun x playwright test e2e/ --headed",
|
||||||
"db:reset-dev": "node scripts/reset-dev-db.js",
|
"db:switch": "bun run scripts/switch-database.js",
|
||||||
"db:use-dev": "node scripts/use-dev-db.js",
|
"db:setup-postgres": "bun run scripts/setup-postgres.js",
|
||||||
"db:cleanup-prod": "node scripts/cleanup-prod-db.js",
|
"db:setup-dev": "bun run scripts/setup-postgres.js",
|
||||||
"db:check-prod": "node scripts/check-test-records.js",
|
"db:setup-dev:clean": "bun run scripts/setup-postgres.js --drop",
|
||||||
"db:seed": "node scripts/seed.js",
|
"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",
|
||||||
"docker:up": "docker-compose up -d",
|
"docker:up": "docker-compose up -d",
|
||||||
"docker:down": "docker-compose down",
|
"docker:down": "docker-compose down",
|
||||||
"docker:logs": "docker-compose logs -f",
|
"docker:logs": "docker-compose logs -f",
|
||||||
"docker:build": "docker-compose build",
|
"docker:build": "docker-compose build",
|
||||||
"docker:shell": "docker exec -it euchre-camp-app sh",
|
"docker:shell": "docker exec -it euchre-camp-app sh",
|
||||||
"version": "node scripts/bump-version.js",
|
"version": "bun run scripts/bump-version.js",
|
||||||
"set-version": "node scripts/set-version.js",
|
"set-version": "bun run scripts/set-version.js",
|
||||||
"version:patch": "node scripts/bump-version.js patch",
|
"version:patch": "bun run scripts/bump-version.js patch",
|
||||||
"version:minor": "node scripts/bump-version.js minor",
|
"version:minor": "bun run scripts/bump-version.js minor",
|
||||||
"version:major": "node scripts/bump-version.js major",
|
"version:major": "bun run scripts/bump-version.js major",
|
||||||
"docker:build:push": "node scripts/build-and-push-docker.js",
|
"docker:build:push": "bun run scripts/build-and-push-docker.js",
|
||||||
"docker:compose:generate": "node scripts/generate-docker-compose.js",
|
"docker:compose:generate": "bun run scripts/generate-docker-compose.js",
|
||||||
"release": "npm run version:patch && npm run docker:compose:generate && npm run docker:build:push"
|
"release": "bun run version:patch && bun run docker:compose:generate && bun run docker:build:push"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, MockedFunction } from 'vitest'
|
import { describe, it, expect, mock, beforeEach, MockedFunction } from 'bun:test'
|
||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import EditTournamentForm from '@/components/EditTournamentForm'
|
import EditTournamentForm from '@/components/EditTournamentForm'
|
||||||
|
|
||||||
// Mock next/navigation
|
// Mock next/navigation
|
||||||
vi.mock('next/navigation', () => ({
|
mock.module('next/navigation', () => ({
|
||||||
useRouter: () => ({
|
useRouter: () => ({
|
||||||
push: vi.fn(),
|
push: mock(() => {}),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock next/link
|
// Mock next/link
|
||||||
vi.mock('next/link', () => ({
|
mock.module('next/link', () => ({
|
||||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||||
<a href={href}>{children}</a>
|
<a href={href}>{children}</a>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock fetch
|
// Mock fetch
|
||||||
const mockFetch = vi.fn()
|
const mockFetch = mock(() => {})
|
||||||
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
||||||
|
|
||||||
const mockTournament = {
|
const mockTournament = {
|
||||||
@@ -40,7 +40,8 @@ const mockTournament = {
|
|||||||
|
|
||||||
describe('EditTournamentForm', () => {
|
describe('EditTournamentForm', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
// Only clear the specific mocks we create, not global module mocks
|
||||||
|
mockFetch.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders form with initial values', () => {
|
it('renders form with initial values', () => {
|
||||||
|
|||||||
@@ -5,31 +5,39 @@
|
|||||||
* Tests the navigation component for proper display based on session state
|
* Tests the navigation component for proper display based on session state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'
|
||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
import Navigation from '@/components/Navigation'
|
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 the SessionProvider
|
||||||
vi.mock('@/components/SessionProvider', () => ({
|
mock.module('@/components/SessionProvider', () => ({
|
||||||
useSession: vi.fn(),
|
useSession: mock(() => {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock the auth-client
|
// Mock the auth-client
|
||||||
vi.mock('@/lib/auth-client', () => ({
|
mock.module('@/lib/auth-client', () => ({
|
||||||
authClient: {
|
authClient: {
|
||||||
signOut: vi.fn(),
|
signOut: mock(() => {}),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock fetch for role API call
|
// Mock fetch for role API call
|
||||||
global.fetch = vi.fn()
|
global.fetch = mock(() => {})
|
||||||
|
|
||||||
import { useSession } from '@/components/SessionProvider'
|
import { useSession } from '@/components/SessionProvider'
|
||||||
|
|
||||||
describe('Epic 1: Navigation Component', () => {
|
describe('Epic 1: Navigation Component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
// Don't clear all mocks as it might affect bun-setup.ts
|
||||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
// Set up default fetch mock
|
||||||
|
(global.fetch as any).mockImplementation?.(async (url: any) => {
|
||||||
if (url?.toString().includes('/api/users/')) {
|
if (url?.toString().includes('/api/users/')) {
|
||||||
return new Response(JSON.stringify({ role: 'player' }), {
|
return new Response(JSON.stringify({ role: 'player' }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -41,14 +49,15 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks()
|
// Don't clear mocks - let them persist for other test files
|
||||||
|
// Navigation relies on module mocks set up at file level
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the logo and basic links when not logged in', () => {
|
it('renders the logo and basic links when not logged in', () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: null,
|
session: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<Navigation />)
|
render(<Navigation />)
|
||||||
@@ -60,7 +69,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows user menu when logged in', async () => {
|
it('shows user menu when logged in', async () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
id: 'user-123',
|
id: 'user-123',
|
||||||
@@ -71,7 +80,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
session: { token: 'abc123' },
|
session: { token: 'abc123' },
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<Navigation />)
|
render(<Navigation />)
|
||||||
@@ -85,7 +94,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows Tournaments link when logged in', async () => {
|
it('shows Tournaments link when logged in', async () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
id: 'user-123',
|
id: 'user-123',
|
||||||
@@ -96,7 +105,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
session: { token: 'abc123' },
|
session: { token: 'abc123' },
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<Navigation />)
|
render(<Navigation />)
|
||||||
@@ -107,7 +116,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows admin link for club_admin role', async () => {
|
it('shows admin link for club_admin role', async () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
id: 'admin-123',
|
id: 'admin-123',
|
||||||
@@ -118,11 +127,11 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
session: { token: 'abc123' },
|
session: { token: 'abc123' },
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
});
|
||||||
|
|
||||||
// Mock fetch to return club_admin role
|
// Mock fetch to return club_admin role
|
||||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
(global.fetch).mockImplementation(async (url) => {
|
||||||
if (url?.toString().includes('/api/users/admin-123/role')) {
|
if (url?.toString().includes('/api/users/admin-123/role')) {
|
||||||
return new Response(JSON.stringify({ role: 'club_admin' }), {
|
return new Response(JSON.stringify({ role: 'club_admin' }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -141,7 +150,7 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('hides admin link for non-admin users', async () => {
|
it('hides admin link for non-admin users', async () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
id: 'player-123',
|
id: 'player-123',
|
||||||
@@ -152,11 +161,11 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
session: { token: 'abc123' },
|
session: { token: 'abc123' },
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
});
|
||||||
|
|
||||||
// Mock fetch to return player role (already set in beforeEach, but explicit here)
|
// Mock fetch to return player role (already set in beforeEach, but explicit here)
|
||||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
(global.fetch).mockImplementation(async (url) => {
|
||||||
if (url?.toString().includes('/api/users/')) {
|
if (url?.toString().includes('/api/users/')) {
|
||||||
return {
|
return {
|
||||||
json: () => Promise.resolve({ role: 'player' }),
|
json: () => Promise.resolve({ role: 'player' }),
|
||||||
@@ -176,10 +185,10 @@ describe('Epic 1: Navigation Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows loading state', () => {
|
it('shows loading state', () => {
|
||||||
vi.mocked(useSession).mockReturnValue({
|
(useSession).mockReturnValue({
|
||||||
session: null,
|
session: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
refreshSession: vi.fn(),
|
refreshSession: mock(() => {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<Navigation />)
|
render(<Navigation />)
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, MockedFunction } from 'vitest'
|
import { describe, it, expect, mock, beforeEach, MockedFunction } from 'bun:test'
|
||||||
import { getSession } from '@/lib/auth-simple'
|
import { getSession } from '@/lib/auth-simple'
|
||||||
|
|
||||||
// Mock next/headers
|
// Mock next/headers
|
||||||
vi.mock('next/headers', () => ({
|
mock.module('next/headers', () => ({
|
||||||
cookies: vi.fn().mockResolvedValue({
|
cookies: mock(() => {}).mockResolvedValue({
|
||||||
get: vi.fn().mockReturnValue({ name: 'better-auth.session_token', value: 'test-token' }),
|
get: mock(() => {}).mockReturnValue({ name: 'better-auth.session_token', value: 'test-token' }),
|
||||||
}),
|
}),
|
||||||
headers: vi.fn().mockResolvedValue({
|
headers: mock(() => {}).mockResolvedValue({
|
||||||
get: vi.fn().mockReturnValue(null),
|
get: mock(() => {}).mockReturnValue(null),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock fetch
|
// Mock fetch
|
||||||
const mockFetch = vi.fn()
|
const mockFetch = mock(() => {})
|
||||||
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
global.fetch = mockFetch as MockedFunction<typeof global.fetch>
|
||||||
|
|
||||||
describe('getSession', () => {
|
describe('getSession', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
mock.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns session data when auth API returns 200', async () => {
|
it('returns session data when auth API returns 200', async () => {
|
||||||
@@ -26,7 +26,7 @@ describe('getSession', () => {
|
|||||||
user: { id: 'user-123', email: 'test@example.com' },
|
user: { id: 'user-123', email: 'test@example.com' },
|
||||||
}
|
}
|
||||||
|
|
||||||
mockFetch.mockResolvedValue(
|
mockFetch.mockImplementation(async () =>
|
||||||
new Response(JSON.stringify(mockSession), { status: 200 })
|
new Response(JSON.stringify(mockSession), { status: 200 })
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ describe('getSession', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns null when auth API returns non-200 status', async () => {
|
it('returns null when auth API returns non-200 status', async () => {
|
||||||
mockFetch.mockResolvedValue(
|
mockFetch.mockImplementation(async () =>
|
||||||
new Response(null, { status: 401 })
|
new Response(null, { status: 401 })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Setup file for Bun test runner to provide DOM environment
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
|
||||||
|
console.log('Loading bun-setup.ts...');
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* Tests the mathematical correctness of Elo calculations and rating updates
|
* Tests the mathematical correctness of Elo calculations and rating updates
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'bun:test';
|
||||||
import { calculateEloChange, calculateExpectedScore, calculateTeamElo } from '@/lib/elo-utils';
|
import { calculateEloChange, calculateExpectedScore, calculateTeamElo } from '@/lib/elo-utils';
|
||||||
|
|
||||||
describe('Elo Rating System', () => {
|
describe('Elo Rating System', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Tests for ID parsing and validation in API routes and pages
|
* Tests for ID parsing and validation in API routes and pages
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
|
||||||
describe('ID Validation', () => {
|
describe('ID Validation', () => {
|
||||||
describe('parseInt with validation', () => {
|
describe('parseInt with validation', () => {
|
||||||
|
|||||||
@@ -4,12 +4,33 @@
|
|||||||
* Tests the permission system for tournament management
|
* Tests the permission system for tournament management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, vi } from 'vitest';
|
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||||
import { hasRole, canManageTournament, canCreateTournaments } from '@/lib/permissions';
|
import { hasRole, canManageTournament, canCreateTournaments } from '@/lib/permissions';
|
||||||
import { getSession } from '@/lib/auth-simple';
|
import { getSession } from '@/lib/auth-simple';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import type { User } from '@prisma/client';
|
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
|
// Helper to create mock user
|
||||||
const createMockUser = (id: string, email: string, role: string): User => ({
|
const createMockUser = (id: string, email: string, role: string): User => ({
|
||||||
id,
|
id,
|
||||||
@@ -23,30 +44,21 @@ const createMockUser = (id: string, email: string, role: string): User => ({
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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('Permissions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mock implementations to default (no-op) before each test
|
||||||
|
getSessionMock.mockImplementation(() => undefined);
|
||||||
|
userFindUniqueMock.mockImplementation(() => undefined);
|
||||||
|
eventFindUniqueMock.mockImplementation(() => undefined);
|
||||||
|
});
|
||||||
|
|
||||||
describe('hasRole', () => {
|
describe('hasRole', () => {
|
||||||
test('should allow club_admin to access tournament_admin resources', async () => {
|
test('should allow club_admin to access tournament_admin resources', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: '1', email: 'test@example.com' },
|
user: { id: '1', email: 'test@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('1', 'test@example.com', 'club_admin')
|
createMockUser('1', 'test@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,11 +67,11 @@ describe('Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny player from accessing tournament_admin resources', async () => {
|
test('should deny player from accessing tournament_admin resources', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: '1', email: 'test@example.com' },
|
user: { id: '1', email: 'test@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('1', 'test@example.com', 'player')
|
createMockUser('1', 'test@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -68,7 +80,7 @@ describe('Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny unauthenticated user', async () => {
|
test('should deny unauthenticated user', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue(null);
|
getSessionMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const result = await hasRole('club_admin');
|
const result = await hasRole('club_admin');
|
||||||
expect(result.allowed).toBe(false);
|
expect(result.allowed).toBe(false);
|
||||||
@@ -78,11 +90,11 @@ describe('Permissions', () => {
|
|||||||
|
|
||||||
describe('canManageTournament', () => {
|
describe('canManageTournament', () => {
|
||||||
test('should allow club_admin to manage any tournament', async () => {
|
test('should allow club_admin to manage any tournament', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,11 +103,11 @@ describe('Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny player from managing tournaments', async () => {
|
test('should deny player from managing tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'player-1', email: 'player@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,11 +119,11 @@ describe('Permissions', () => {
|
|||||||
|
|
||||||
describe('canCreateTournaments', () => {
|
describe('canCreateTournaments', () => {
|
||||||
test('should allow tournament_admin to create tournaments', async () => {
|
test('should allow tournament_admin to create tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'tournament_admin')
|
createMockUser('admin-1', 'admin@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,11 +132,11 @@ describe('Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should allow club_admin to create tournaments', async () => {
|
test('should allow club_admin to create tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -133,11 +145,11 @@ describe('Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny player from creating tournaments', async () => {
|
test('should deny player from creating tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'player-1', email: 'player@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
* Tests the player deduplication logic for CSV uploads
|
* Tests the player deduplication logic for CSV uploads
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
import { describe, test, expect, mock, beforeEach,} from 'bun:test';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// Create mock functions at module level
|
||||||
|
const playerFindFirstMock = mock(() => {});
|
||||||
|
const playerCreateMock = mock(() => {});
|
||||||
|
|
||||||
// Mock the prisma module
|
// Mock the prisma module
|
||||||
vi.mock('@/lib/prisma', () => ({
|
mock.module('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
player: {
|
player: {
|
||||||
findFirst: vi.fn(),
|
findFirst: playerFindFirstMock,
|
||||||
create: vi.fn(),
|
create: playerCreateMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -46,7 +50,9 @@ async function findOrCreatePlayer(name: string) {
|
|||||||
|
|
||||||
describe('Player Deduplication', () => {
|
describe('Player Deduplication', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
// Clear all mock history before each test
|
||||||
|
playerFindFirstMock.mockClear();
|
||||||
|
playerCreateMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findOrCreatePlayer', () => {
|
describe('findOrCreatePlayer', () => {
|
||||||
@@ -64,7 +70,7 @@ describe('Player Deduplication', () => {
|
|||||||
rating: 0,
|
rating: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer('Emily');
|
const result = await findOrCreatePlayer('Emily');
|
||||||
|
|
||||||
@@ -89,7 +95,7 @@ describe('Player Deduplication', () => {
|
|||||||
rating: 0,
|
rating: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer('EMILY');
|
const result = await findOrCreatePlayer('EMILY');
|
||||||
|
|
||||||
@@ -114,7 +120,7 @@ describe('Player Deduplication', () => {
|
|||||||
rating: 0,
|
rating: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer(' Emily ');
|
const result = await findOrCreatePlayer(' Emily ');
|
||||||
|
|
||||||
@@ -126,7 +132,7 @@ describe('Player Deduplication', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create new player if not found', async () => {
|
test('should create new player if not found', async () => {
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
playerFindFirstMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const newPlayer = {
|
const newPlayer = {
|
||||||
id: 100,
|
id: 100,
|
||||||
@@ -141,7 +147,7 @@ describe('Player Deduplication', () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer('NewPlayer');
|
const result = await findOrCreatePlayer('NewPlayer');
|
||||||
|
|
||||||
@@ -162,7 +168,7 @@ describe('Player Deduplication', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle names with special characters', async () => {
|
test('should handle names with special characters', async () => {
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
playerFindFirstMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const newPlayer = {
|
const newPlayer = {
|
||||||
id: 100,
|
id: 100,
|
||||||
@@ -177,7 +183,7 @@ describe('Player Deduplication', () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer('Test-Player_123');
|
const result = await findOrCreatePlayer('Test-Player_123');
|
||||||
|
|
||||||
@@ -195,7 +201,7 @@ describe('Player Deduplication', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle names with spaces', async () => {
|
test('should handle names with spaces', async () => {
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
playerFindFirstMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const newPlayer = {
|
const newPlayer = {
|
||||||
id: 100,
|
id: 100,
|
||||||
@@ -210,7 +216,7 @@ describe('Player Deduplication', () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer('Dave B');
|
const result = await findOrCreatePlayer('Dave B');
|
||||||
|
|
||||||
@@ -242,7 +248,7 @@ describe('Player Deduplication', () => {
|
|||||||
rating: 0,
|
rating: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(mockPlayer);
|
playerFindFirstMock.mockImplementation(async () => mockPlayer);
|
||||||
|
|
||||||
const result1 = await findOrCreatePlayer('EMILY');
|
const result1 = await findOrCreatePlayer('EMILY');
|
||||||
const result2 = await findOrCreatePlayer('Emily');
|
const result2 = await findOrCreatePlayer('Emily');
|
||||||
@@ -255,7 +261,7 @@ describe('Player Deduplication', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle empty or whitespace-only names', async () => {
|
test('should handle empty or whitespace-only names', async () => {
|
||||||
vi.mocked(prisma.player.findFirst).mockResolvedValue(null);
|
playerFindFirstMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const newPlayer = {
|
const newPlayer = {
|
||||||
id: 100,
|
id: 100,
|
||||||
@@ -270,7 +276,7 @@ describe('Player Deduplication', () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.create).mockResolvedValue(newPlayer);
|
playerCreateMock.mockImplementation(async () => newPlayer);
|
||||||
|
|
||||||
const result = await findOrCreatePlayer(' ');
|
const result = await findOrCreatePlayer(' ');
|
||||||
|
|
||||||
@@ -320,7 +326,7 @@ describe('Player Deduplication', () => {
|
|||||||
rating: 0,
|
rating: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.player.findFirst)
|
playerFindFirstMock
|
||||||
.mockResolvedValueOnce(existingPlayer) // First call for "Emily"
|
.mockResolvedValueOnce(existingPlayer) // First call for "Emily"
|
||||||
.mockResolvedValueOnce(existingPlayer) // Second call for "EMILY"
|
.mockResolvedValueOnce(existingPlayer) // Second call for "EMILY"
|
||||||
.mockResolvedValueOnce(existingPlayer); // Third call for " Emily "
|
.mockResolvedValueOnce(existingPlayer); // Third call for " Emily "
|
||||||
|
|||||||
@@ -7,24 +7,30 @@
|
|||||||
* - Partnership performance
|
* - Partnership performance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import type { Player, Event, Match, PartnershipStat } from '@prisma/client';
|
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 the prisma module
|
||||||
vi.mock('@/lib/prisma', () => ({
|
mock.module('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
player: {
|
player: {
|
||||||
findUnique: vi.fn(),
|
findUnique: playerFindUniqueMock,
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
findMany: vi.fn(),
|
findMany: eventFindManyMock,
|
||||||
},
|
},
|
||||||
match: {
|
match: {
|
||||||
findMany: vi.fn(),
|
findMany: matchFindManyMock,
|
||||||
},
|
},
|
||||||
partnershipStat: {
|
partnershipStat: {
|
||||||
findMany: vi.fn(),
|
findMany: partnershipStatFindManyMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -111,7 +117,11 @@ const createMockPartnershipStat = (
|
|||||||
|
|
||||||
describe('Player Profile Enhancements', () => {
|
describe('Player Profile Enhancements', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
// Reset mock implementations to default (no-op) before each test
|
||||||
|
playerFindUniqueMock.mockImplementation(() => undefined);
|
||||||
|
eventFindManyMock.mockImplementation(() => undefined);
|
||||||
|
matchFindManyMock.mockImplementation(() => undefined);
|
||||||
|
partnershipStatFindManyMock.mockImplementation(() => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tournaments Participated', () => {
|
describe('Tournaments Participated', () => {
|
||||||
@@ -122,8 +132,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
createMockTournament(2, 'Tournament B'),
|
createMockTournament(2, 'Tournament B'),
|
||||||
];
|
];
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||||
|
|
||||||
// Simulate the query that would be run
|
// Simulate the query that would be run
|
||||||
const tournaments = await prisma.event.findMany({
|
const tournaments = await prisma.event.findMany({
|
||||||
@@ -145,8 +155,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
test('should return empty array if player has no tournaments', async () => {
|
test('should return empty array if player has no tournaments', async () => {
|
||||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.event.findMany).mockResolvedValue([]);
|
eventFindManyMock.mockImplementation(async () => []);
|
||||||
|
|
||||||
const tournaments = await prisma.event.findMany({
|
const tournaments = await prisma.event.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -173,8 +183,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
createMockMatch(2, 1, 3, 5, 6, 4, 4),
|
createMockMatch(2, 1, 3, 5, 6, 4, 4),
|
||||||
];
|
];
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.match.findMany).mockResolvedValue(mockMatches);
|
matchFindManyMock.mockImplementation(async () => mockMatches);
|
||||||
|
|
||||||
// Simulate the query that would be run
|
// Simulate the query that would be run
|
||||||
const matches = await prisma.match.findMany({
|
const matches = await prisma.match.findMany({
|
||||||
@@ -204,8 +214,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
test('should return empty array if player has no matches', async () => {
|
test('should return empty array if player has no matches', async () => {
|
||||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.match.findMany).mockResolvedValue([]);
|
matchFindManyMock.mockImplementation(async () => []);
|
||||||
|
|
||||||
const matches = await prisma.match.findMany({
|
const matches = await prisma.match.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -240,8 +250,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
createMockPartnershipStat(2, 1, 3, 5, 2, 3),
|
createMockPartnershipStat(2, 1, 3, 5, 2, 3),
|
||||||
];
|
];
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.partnershipStat.findMany).mockResolvedValue(mockPartnershipStats);
|
partnershipStatFindManyMock.mockImplementation(async () => mockPartnershipStats);
|
||||||
|
|
||||||
// Simulate the query that would be run
|
// Simulate the query that would be run
|
||||||
const partnershipStats = await prisma.partnershipStat.findMany({
|
const partnershipStats = await prisma.partnershipStat.findMany({
|
||||||
@@ -265,8 +275,8 @@ describe('Player Profile Enhancements', () => {
|
|||||||
test('should return empty array if player has no partnership data', async () => {
|
test('should return empty array if player has no partnership data', async () => {
|
||||||
const mockPlayer = createMockPlayer(1, 'Test Player');
|
const mockPlayer = createMockPlayer(1, 'Test Player');
|
||||||
|
|
||||||
vi.mocked(prisma.player.findUnique).mockResolvedValue(mockPlayer);
|
playerFindUniqueMock.mockImplementation(async () => mockPlayer);
|
||||||
vi.mocked(prisma.partnershipStat.findMany).mockResolvedValue([]);
|
partnershipStatFindManyMock.mockImplementation(async () => []);
|
||||||
|
|
||||||
const partnershipStats = await prisma.partnershipStat.findMany({
|
const partnershipStats = await prisma.partnershipStat.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -5,33 +5,33 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
||||||
import { recalculateAllElo } from '@/lib/elo-utils';
|
import { recalculateAllElo } from '@/lib/elo-utils';
|
||||||
|
|
||||||
// Mock Prisma client
|
// Mock Prisma client
|
||||||
const mockPrisma = {
|
const mockPrisma = {
|
||||||
player: {
|
player: {
|
||||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
updateMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||||
update: vi.fn().mockResolvedValue({}),
|
update: mock(() => {}).mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
eloSnapshot: {
|
eloSnapshot: {
|
||||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||||
create: vi.fn().mockResolvedValue({}),
|
create: mock(() => {}).mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
partnershipStat: {
|
partnershipStat: {
|
||||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }),
|
||||||
findFirst: vi.fn().mockResolvedValue(null), // No existing stats initially
|
findFirst: mock(() => {}).mockResolvedValue(null), // No existing stats initially
|
||||||
update: vi.fn().mockResolvedValue({}),
|
update: mock(() => {}).mockResolvedValue({}),
|
||||||
create: vi.fn().mockResolvedValue({}),
|
create: mock(() => {}).mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
match: {
|
match: {
|
||||||
findMany: vi.fn().mockResolvedValue([]),
|
findMany: mock(() => {}).mockResolvedValue([]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('recalculateAllElo', () => {
|
describe('recalculateAllElo', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
mock.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reset all player stats to zero', async () => {
|
test('should reset all player stats to zero', async () => {
|
||||||
|
|||||||
@@ -5,25 +5,31 @@
|
|||||||
* Regression tests for the issue where tournament_admin users were redirected to login
|
* Regression tests for the issue where tournament_admin users were redirected to login
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
||||||
import { canManageTournament, ownsTournament, getManageableTournaments } from '@/lib/permissions';
|
import { canManageTournament, ownsTournament, getManageableTournaments } from '@/lib/permissions';
|
||||||
import { getSession } from '@/lib/auth-simple';
|
import { getSession } from '@/lib/auth-simple';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import type { User, Event } from '@prisma/client';
|
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 the getSession and prisma functions
|
||||||
vi.mock('@/lib/auth-simple', () => ({
|
mock.module('@/lib/auth-simple', () => ({
|
||||||
getSession: vi.fn(),
|
getSession: getSessionMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/lib/prisma', () => ({
|
mock.module('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
user: {
|
user: {
|
||||||
findUnique: vi.fn(),
|
findUnique: userFindUniqueMock,
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
findUnique: vi.fn(),
|
findUnique: eventFindUniqueMock,
|
||||||
findMany: vi.fn(),
|
findMany: eventFindManyMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -61,16 +67,21 @@ const createMockTournament = (id: number, ownerId: string | null): Event => ({
|
|||||||
|
|
||||||
describe('Tournament Permissions', () => {
|
describe('Tournament Permissions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
// 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canManageTournament', () => {
|
describe('canManageTournament', () => {
|
||||||
test('should allow club_admin to manage any tournament', async () => {
|
test('should allow club_admin to manage any tournament', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -79,14 +90,14 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should allow tournament_admin to manage their own tournament', async () => {
|
test('should allow tournament_admin to manage their own tournament', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockTournament(1, 'tour-admin-1')
|
createMockTournament(1, 'tour-admin-1')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,14 +106,14 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny tournament_admin from managing other users tournaments', async () => {
|
test('should deny tournament_admin from managing other users tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockTournament(1, 'other-user-1')
|
createMockTournament(1, 'other-user-1')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,11 +123,11 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny player from managing tournaments', async () => {
|
test('should deny player from managing tournaments', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'player-1', email: 'player@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,7 +137,7 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should deny unauthenticated user', async () => {
|
test('should deny unauthenticated user', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue(null);
|
getSessionMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const result = await canManageTournament(999);
|
const result = await canManageTournament(999);
|
||||||
expect(result.allowed).toBe(false);
|
expect(result.allowed).toBe(false);
|
||||||
@@ -136,11 +147,11 @@ describe('Tournament Permissions', () => {
|
|||||||
|
|
||||||
describe('ownsTournament', () => {
|
describe('ownsTournament', () => {
|
||||||
test('should return true if user owns tournament', async () => {
|
test('should return true if user owns tournament', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'owner-1', email: 'owner@example.com' },
|
user: { id: 'owner-1', email: 'owner@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockTournament(1, 'owner-1')
|
createMockTournament(1, 'owner-1')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -149,11 +160,11 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return false if user does not own tournament', async () => {
|
test('should return false if user does not own tournament', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'non-owner-1', email: 'nonowner@example.com' },
|
user: { id: 'non-owner-1', email: 'nonowner@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockTournament(1, 'owner-1')
|
createMockTournament(1, 'owner-1')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -165,11 +176,11 @@ describe('Tournament Permissions', () => {
|
|||||||
|
|
||||||
describe('getManageableTournaments', () => {
|
describe('getManageableTournaments', () => {
|
||||||
test('should return all tournaments for club_admin', async () => {
|
test('should return all tournaments for club_admin', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,11 +189,11 @@ describe('Tournament Permissions', () => {
|
|||||||
createMockTournament(2, 'user-2'),
|
createMockTournament(2, 'user-2'),
|
||||||
createMockTournament(3, 'user-3'),
|
createMockTournament(3, 'user-3'),
|
||||||
];
|
];
|
||||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||||
|
|
||||||
const result = await getManageableTournaments();
|
const result = await getManageableTournaments();
|
||||||
expect(result).toEqual(mockTournaments);
|
expect(result).toEqual(mockTournaments);
|
||||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||||
where: { eventType: 'tournament' },
|
where: { eventType: 'tournament' },
|
||||||
include: { participants: true },
|
include: { participants: true },
|
||||||
orderBy: { createdAt: 'desc' }
|
orderBy: { createdAt: 'desc' }
|
||||||
@@ -190,11 +201,11 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return only owned tournaments for tournament_admin', async () => {
|
test('should return only owned tournaments for tournament_admin', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,11 +213,11 @@ describe('Tournament Permissions', () => {
|
|||||||
createMockTournament(1, 'tour-admin-1'),
|
createMockTournament(1, 'tour-admin-1'),
|
||||||
createMockTournament(2, 'tour-admin-1'),
|
createMockTournament(2, 'tour-admin-1'),
|
||||||
];
|
];
|
||||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||||
|
|
||||||
const result = await getManageableTournaments();
|
const result = await getManageableTournaments();
|
||||||
expect(result).toEqual(mockTournaments);
|
expect(result).toEqual(mockTournaments);
|
||||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
eventType: 'tournament',
|
eventType: 'tournament',
|
||||||
ownerId: 'tour-admin-1'
|
ownerId: 'tour-admin-1'
|
||||||
@@ -217,11 +228,11 @@ describe('Tournament Permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return only non-draft tournaments for players', async () => {
|
test('should return only non-draft tournaments for players', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'player-1', email: 'player@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,11 +240,11 @@ describe('Tournament Permissions', () => {
|
|||||||
createMockTournament(1, 'user-1'),
|
createMockTournament(1, 'user-1'),
|
||||||
createMockTournament(2, 'user-2'),
|
createMockTournament(2, 'user-2'),
|
||||||
];
|
];
|
||||||
vi.mocked(prisma.event.findMany).mockResolvedValue(mockTournaments);
|
eventFindManyMock.mockImplementation(async () => mockTournaments);
|
||||||
|
|
||||||
const result = await getManageableTournaments();
|
const result = await getManageableTournaments();
|
||||||
expect(result).toEqual(mockTournaments);
|
expect(result).toEqual(mockTournaments);
|
||||||
expect(prisma.event.findMany).toHaveBeenCalledWith({
|
expect(eventFindManyMock).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
eventType: 'tournament',
|
eventType: 'tournament',
|
||||||
status: { not: 'draft' }
|
status: { not: 'draft' }
|
||||||
@@ -249,14 +260,14 @@ describe('Tournament Permissions', () => {
|
|||||||
// This simulates the scenario where a tournament_admin user
|
// This simulates the scenario where a tournament_admin user
|
||||||
// clicks "Edit" on a tournament they own
|
// clicks "Edit" on a tournament they own
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue(
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockTournament(1, 'tour-admin-1')
|
createMockTournament(1, 'tour-admin-1')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -271,11 +282,11 @@ describe('Tournament Permissions', () => {
|
|||||||
test('club_admin should still be able to manage any tournament', async () => {
|
test('club_admin should still be able to manage any tournament', async () => {
|
||||||
// This ensures we didn't break the existing club_admin functionality
|
// This ensures we didn't break the existing club_admin functionality
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'club-admin-1', email: 'club@example.com' },
|
user: { id: 'club-admin-1', email: 'club@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('club-admin-1', 'club@example.com', 'club_admin')
|
createMockUser('club-admin-1', 'club@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -286,13 +297,16 @@ describe('Tournament Permissions', () => {
|
|||||||
test('players should still be denied from managing tournaments', async () => {
|
test('players should still be denied from managing tournaments', async () => {
|
||||||
// This ensures we didn't accidentally grant players access
|
// This ensures we didn't accidentally grant players access
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'player-1', email: 'player@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
eventFindUniqueMock.mockImplementation(async () =>
|
||||||
|
createMockTournament(1, 'other-user-1')
|
||||||
|
);
|
||||||
|
|
||||||
const result = await canManageTournament(1);
|
const result = await canManageTournament(1);
|
||||||
expect(result.allowed).toBe(false);
|
expect(result.allowed).toBe(false);
|
||||||
|
|||||||
@@ -3,23 +3,33 @@
|
|||||||
* Tests the allowTies field is properly saved when updating tournaments
|
* Tests the allowTies field is properly saved when updating tournaments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, mock, beforeEach,} from 'bun:test';
|
||||||
import { prisma } from '@/lib/prisma';
|
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 the prisma client
|
||||||
vi.mock('@/lib/prisma', () => ({
|
mock.module('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
event: {
|
event: {
|
||||||
findUnique: vi.fn(),
|
findUnique: eventFindUniqueMock,
|
||||||
update: vi.fn(),
|
update: eventUpdateMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the permissions module
|
// Mock the permissions module
|
||||||
vi.mock('@/lib/permissions', () => ({
|
mock.module('@/lib/permissions', () => ({
|
||||||
canManageTournament: vi.fn().mockResolvedValue({ allowed: true }),
|
canManageTournament: defaultCanManageTournament,
|
||||||
canDeleteTournament: vi.fn().mockResolvedValue({ allowed: true }),
|
canDeleteTournament: defaultCanDeleteTournament,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Import the route handler after mocking
|
// Import the route handler after mocking
|
||||||
@@ -27,12 +37,16 @@ import { PUT } from '@/app/api/tournaments/[id]/route';
|
|||||||
|
|
||||||
describe('Tournament Update API', () => {
|
describe('Tournament Update API', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
// Clear all mock history before each test
|
||||||
|
eventFindUniqueMock.mockClear();
|
||||||
|
eventUpdateMock.mockClear();
|
||||||
|
canManageTournamentMock.mockClear();
|
||||||
|
canDeleteTournamentMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update allowTies field when provided', async () => {
|
it('should update allowTies field when provided', async () => {
|
||||||
// Mock existing tournament
|
// Mock existing tournament
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
eventFindUniqueMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Tournament',
|
name: 'Test Tournament',
|
||||||
allowTies: false,
|
allowTies: false,
|
||||||
@@ -46,10 +60,10 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
// Mock successful update
|
// Mock successful update
|
||||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
eventUpdateMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Tournament',
|
name: 'Test Tournament',
|
||||||
allowTies: true,
|
allowTies: true,
|
||||||
@@ -63,7 +77,7 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
const request = new Request('http://localhost/api/tournaments/1', {
|
const request = new Request('http://localhost/api/tournaments/1', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -78,7 +92,7 @@ describe('Tournament Update API', () => {
|
|||||||
const response = await PUT(request, { params });
|
const response = await PUT(request, { params });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(vi.mocked(prisma.event.update)).toHaveBeenCalledWith(
|
expect(prisma.event.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
allowTies: true,
|
allowTies: true,
|
||||||
@@ -89,7 +103,7 @@ describe('Tournament Update API', () => {
|
|||||||
|
|
||||||
it('should default allowTies to false when not provided', async () => {
|
it('should default allowTies to false when not provided', async () => {
|
||||||
// Mock existing tournament
|
// Mock existing tournament
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
eventFindUniqueMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Tournament',
|
name: 'Test Tournament',
|
||||||
allowTies: true,
|
allowTies: true,
|
||||||
@@ -103,10 +117,10 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
// Mock successful update
|
// Mock successful update
|
||||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
eventUpdateMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Tournament',
|
name: 'Test Tournament',
|
||||||
allowTies: false,
|
allowTies: false,
|
||||||
@@ -120,7 +134,7 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
const request = new Request('http://localhost/api/tournaments/1', {
|
const request = new Request('http://localhost/api/tournaments/1', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -135,7 +149,7 @@ describe('Tournament Update API', () => {
|
|||||||
const response = await PUT(request, { params });
|
const response = await PUT(request, { params });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(vi.mocked(prisma.event.update)).toHaveBeenCalledWith(
|
expect(prisma.event.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
allowTies: false, // Should default to false
|
allowTies: false, // Should default to false
|
||||||
@@ -146,7 +160,7 @@ describe('Tournament Update API', () => {
|
|||||||
|
|
||||||
it('should preserve allowTies value when updating other fields', async () => {
|
it('should preserve allowTies value when updating other fields', async () => {
|
||||||
// Mock existing tournament with allowTies = true
|
// Mock existing tournament with allowTies = true
|
||||||
vi.mocked(prisma.event.findUnique).mockResolvedValue({
|
eventFindUniqueMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Tournament',
|
name: 'Test Tournament',
|
||||||
allowTies: true,
|
allowTies: true,
|
||||||
@@ -160,10 +174,10 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
// Mock successful update
|
// Mock successful update
|
||||||
vi.mocked(prisma.event.update).mockResolvedValue({
|
eventUpdateMock.mockImplementation(async () => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Updated Tournament Name',
|
name: 'Updated Tournament Name',
|
||||||
allowTies: true,
|
allowTies: true,
|
||||||
@@ -177,7 +191,7 @@ describe('Tournament Update API', () => {
|
|||||||
ownerId: null,
|
ownerId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any);
|
} as any));
|
||||||
|
|
||||||
const request = new Request('http://localhost/api/tournaments/1', {
|
const request = new Request('http://localhost/api/tournaments/1', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -192,7 +206,7 @@ describe('Tournament Update API', () => {
|
|||||||
const response = await PUT(request, { params });
|
const response = await PUT(request, { params });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const updateCall = vi.mocked(prisma.event.update).mock.calls[0][0];
|
const updateCall = eventUpdateMock.mock.calls[0][0];
|
||||||
expect(updateCall.data.allowTies).toBe(true);
|
expect(updateCall.data.allowTies).toBe(true);
|
||||||
expect(updateCall.data.name).toBe('Updated Tournament Name');
|
expect(updateCall.data.name).toBe('Updated Tournament Name');
|
||||||
expect(updateCall.data.targetScore).toBe(10);
|
expect(updateCall.data.targetScore).toBe(10);
|
||||||
|
|||||||
@@ -4,25 +4,31 @@
|
|||||||
* Tests for user name editing and profile management
|
* Tests for user name editing and profile management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
import { describe, test, expect, mock, beforeEach,} from 'bun:test';
|
||||||
import { hasRole } from '@/lib/permissions';
|
import { hasRole } from '@/lib/permissions';
|
||||||
import { getSession } from '@/lib/auth-simple';
|
import { getSession } from '@/lib/auth-simple';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import type { User, Player } from '@prisma/client';
|
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 the getSession and prisma functions
|
||||||
vi.mock('@/lib/auth-simple', () => ({
|
mock.module('@/lib/auth-simple', () => ({
|
||||||
getSession: vi.fn(),
|
getSession: getSessionMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/lib/prisma', () => ({
|
mock.module('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
user: {
|
user: {
|
||||||
findUnique: vi.fn(),
|
findUnique: userFindUniqueMock,
|
||||||
update: vi.fn(),
|
update: userUpdateMock,
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
findUnique: vi.fn(),
|
findUnique: playerFindUniqueMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -56,16 +62,20 @@ const createMockPlayer = (id: number, name: string): Player => ({
|
|||||||
|
|
||||||
describe('User Management', () => {
|
describe('User Management', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
// Reset mock implementations to default (no-op) before each test
|
||||||
|
getSessionMock.mockImplementation(() => undefined);
|
||||||
|
userFindUniqueMock.mockImplementation(() => undefined);
|
||||||
|
userUpdateMock.mockImplementation(() => undefined);
|
||||||
|
playerFindUniqueMock.mockImplementation(() => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('User Name Editing', () => {
|
describe('User Name Editing', () => {
|
||||||
test('club_admin should be able to edit any user name', async () => {
|
test('club_admin should be able to edit any user name', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
createMockUser('admin-1', 'admin@example.com', 'club_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,11 +84,11 @@ describe('User Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('tournament_admin should NOT be able to edit user names', async () => {
|
test('tournament_admin should NOT be able to edit user names', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'tour-admin-1', email: 'tour@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,11 +97,11 @@ describe('User Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('player should NOT be able to edit user names', async () => {
|
test('player should NOT be able to edit user names', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'player-1', email: 'player@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(
|
userFindUniqueMock.mockImplementation(async () =>
|
||||||
createMockUser('player-1', 'player@example.com', 'player')
|
createMockUser('player-1', 'player@example.com', 'player')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,7 +110,7 @@ describe('User Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('unauthenticated user should NOT be able to edit user names', async () => {
|
test('unauthenticated user should NOT be able to edit user names', async () => {
|
||||||
vi.mocked(getSession).mockResolvedValue(null);
|
getSessionMock.mockImplementation(async () => null);
|
||||||
|
|
||||||
const result = await hasRole('club_admin');
|
const result = await hasRole('club_admin');
|
||||||
expect(result.allowed).toBe(false);
|
expect(result.allowed).toBe(false);
|
||||||
@@ -111,11 +121,11 @@ describe('User Management', () => {
|
|||||||
test('user should be able to view their own profile', async () => {
|
test('user should be able to view their own profile', async () => {
|
||||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'user-1', email: 'user@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||||
|
|
||||||
// In the actual implementation, this would check if session.user.id === params.id
|
// In the actual implementation, this would check if session.user.id === params.id
|
||||||
const canViewOwnProfile = true; // This logic is in the API route
|
const canViewOwnProfile = true; // This logic is in the API route
|
||||||
@@ -126,11 +136,11 @@ describe('User Management', () => {
|
|||||||
const mockAdmin = createMockUser('admin-1', 'admin@example.com', 'club_admin');
|
const mockAdmin = createMockUser('admin-1', 'admin@example.com', 'club_admin');
|
||||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'admin-1', email: 'admin@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique)
|
(userFindUniqueMock)
|
||||||
.mockResolvedValueOnce(mockAdmin) // For the requesting user
|
.mockResolvedValueOnce(mockAdmin) // For the requesting user
|
||||||
.mockResolvedValueOnce(mockUser); // For the target user
|
.mockResolvedValueOnce(mockUser); // For the target user
|
||||||
|
|
||||||
@@ -142,11 +152,11 @@ describe('User Management', () => {
|
|||||||
test('non-admin should NOT be able to view other user profiles', async () => {
|
test('non-admin should NOT be able to view other user profiles', async () => {
|
||||||
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
const mockUser = createMockUser('user-1', 'user@example.com', 'player');
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'user-1', email: 'user@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||||
|
|
||||||
// In the actual implementation, this would check if session.user.id === params.id
|
// In the actual implementation, this would check if session.user.id === params.id
|
||||||
const canViewOtherProfile = false; // This logic is in the API route
|
const canViewOtherProfile = false; // This logic is in the API route
|
||||||
@@ -159,11 +169,11 @@ describe('User Management', () => {
|
|||||||
const mockUser = createMockUser('user-1', 'user@example.com', 'club_admin');
|
const mockUser = createMockUser('user-1', 'user@example.com', 'club_admin');
|
||||||
const mockPlayer = createMockPlayer(1, 'Old Name');
|
const mockPlayer = createMockPlayer(1, 'Old Name');
|
||||||
|
|
||||||
vi.mocked(getSession).mockResolvedValue({
|
getSessionMock.mockImplementation(async () => ({
|
||||||
user: { id: 'user-1', email: 'user@example.com' },
|
user: { id: 'admin-1', email: 'admin@example.com' },
|
||||||
session: { token: 'test', expiresAt: new Date() }
|
session: { token: 'test', expiresAt: new Date() }
|
||||||
});
|
}));
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
userFindUniqueMock.mockImplementation(async () => mockUser);
|
||||||
|
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
...mockUser,
|
...mockUser,
|
||||||
@@ -171,7 +181,7 @@ describe('User Management', () => {
|
|||||||
player: { ...mockPlayer, name: 'New Name', normalizedName: 'new name' }
|
player: { ...mockPlayer, name: 'New Name', normalizedName: 'new name' }
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.user.update).mockResolvedValue(updatedUser);
|
userUpdateMock.mockImplementation(async () => updatedUser);
|
||||||
|
|
||||||
// The API route should update both user.name and player.name
|
// The API route should update both user.name and player.name
|
||||||
expect(updatedUser.name).toBe('New Name');
|
expect(updatedUser.name).toBe('New Name');
|
||||||
|
|||||||
+11
-4
@@ -1,8 +1,7 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
import dotenv from 'dotenv'
|
|
||||||
|
|
||||||
// Load .env file if it exists
|
// Load .env file if it exists
|
||||||
dotenv.config()
|
require('dotenv').config()
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
const globalForPrisma = globalThis as unknown as {
|
||||||
prisma: PrismaClient | undefined
|
prisma: PrismaClient | undefined
|
||||||
@@ -10,16 +9,24 @@ const globalForPrisma = globalThis as unknown as {
|
|||||||
|
|
||||||
// Detect database provider from environment (default to sqlite for local development)
|
// Detect database provider from environment (default to sqlite for local development)
|
||||||
const databaseProvider = process.env.DATABASE_PROVIDER || 'sqlite'
|
const databaseProvider = process.env.DATABASE_PROVIDER || 'sqlite'
|
||||||
|
const databaseUrl = process.env.DATABASE_URL
|
||||||
|
|
||||||
// Create PrismaClient with appropriate adapter
|
// Create PrismaClient with appropriate adapter
|
||||||
const createPrismaClient = () => {
|
const createPrismaClient = () => {
|
||||||
let client: PrismaClient
|
let client: PrismaClient
|
||||||
|
|
||||||
if (databaseProvider === 'postgresql') {
|
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
|
// Use PrismaPg adapter for PostgreSQL
|
||||||
const { PrismaPg } = require('@prisma/adapter-pg')
|
const { PrismaPg } = require('@prisma/adapter-pg')
|
||||||
const pg = require('pg')
|
const adapter = new PrismaPg({ connectionString: databaseUrl })
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
|
||||||
client = new PrismaClient({ adapter })
|
client = new PrismaClient({ adapter })
|
||||||
} else {
|
} else {
|
||||||
// No adapter needed for SQLite
|
// No adapter needed for SQLite
|
||||||
|
|||||||
Reference in New Issue
Block a user