Files
euchre_camp/src/__tests__/unit/tournament-permissions.test.ts
T
david bb6be245b7
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 18s
feat: Implement tournament schedule tab and fix E2E tests (#27)
## Summary

This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures.

### Changes Included

1. **Tournament Schedule Feature**
   - Added tournament schedule page at `/admin/tournaments/[id]/schedule`
   - Implemented "Generate Schedule" button functionality
   - Added schedule generation logic for round-robin tournaments

2. **E2E Test Fixes**
   - Fixed database connection issues in production builds
   - Improved test reliability with better error handling and debugging
   - Updated test infrastructure to use environment variables instead of hardcoded values

3. **CI/CD Updates**
   - Added E2E test job to PR workflow
   - Configured tests to run against development database
   - Moved database password to Gitea secrets

4. **Code Quality**
   - Removed hardcoded passwords from codebase
   - Improved Prisma client configuration
   - Enhanced authentication and navigation components

### Test Results
All 16 E2E test scenarios are now passing:
- Authentication tests: 
- Registration tests: 
- Tournament schedule tests: 
- Player schedule tests: 
- Admin navigation tests: 

### Database Configuration
- Tests run against `euchre_camp_dev` database
- Production builds use environment variables for database configuration
- Database password stored in Gitea secrets as `DB_PASSWORD`

### CI Pipeline
The PR workflow now includes:
1. Unit tests
2. E2E tests (using production build)
3. Version bump analysis

E2E tests must pass before PR can be merged.

Reviewed-on: #27
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
2026-04-27 01:59:16 +00:00

323 lines
11 KiB
TypeScript

/**
* Unit Tests: Tournament Permissions
*
* Tests the permission system for tournament management
* Regression tests for the issue where tournament_admin users were redirected to login
*/
import { describe, test, expect, mock, beforeEach } from 'bun:test';
import { canManageTournament, ownsTournament, getManageableTournaments } from '@/lib/permissions';
import { getSession } from '@/lib/auth-simple';
import { prisma } from '@/lib/prisma';
import type { User, Event } from '@prisma/client';
// Create mock functions first
const getSessionMock = mock(() => {});
const userFindUniqueMock = mock(() => {});
const eventFindUniqueMock = mock(() => {});
const eventFindManyMock = mock(() => {});
// Mock the getSession and prisma functions
mock.module('@/lib/auth-simple', () => ({
getSession: getSessionMock,
}));
mock.module('@/lib/prisma', () => ({
prisma: {
user: {
findUnique: userFindUniqueMock,
},
event: {
findUnique: eventFindUniqueMock,
findMany: eventFindManyMock,
},
},
}));
// Helper to create mock user
const createMockUser = (id: string, email: string, role: string): User => ({
id,
email,
role,
emailVerified: false,
name: null,
image: null,
playerId: null,
createdAt: new Date(),
updatedAt: new Date(),
});
// Helper to create mock tournament
const createMockTournament = (id: number, ownerId: string | null): Event => ({
id,
event_id: null,
name: 'Test Tournament',
description: null,
eventDate: new Date(),
eventType: 'tournament',
tournamentType: 'individual',
format: 'round_robin',
status: 'planned',
maxParticipants: null,
ownerId,
targetScore: null,
allowTies: false,
createdAt: new Date(),
updatedAt: new Date(),
teamDurability: 'permanent',
partnerRotation: 'none',
allowByes: true,
teamConfiguration: null,
maxRosterChanges: null,
requireAdminVerify: false,
});
describe('Tournament Permissions', () => {
beforeEach(() => {
// Reset mock implementations to default (no-op) before each test
// This prevents pollution from previous tests
getSessionMock.mockImplementation(() => undefined);
userFindUniqueMock.mockImplementation(() => undefined);
eventFindUniqueMock.mockImplementation(() => undefined);
eventFindManyMock.mockImplementation(() => undefined);
});
describe('canManageTournament', () => {
test('should allow club_admin to manage any tournament', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('admin-1', 'admin@example.com', 'club_admin')
);
const result = await canManageTournament(999);
expect(result.allowed).toBe(true);
});
test('should allow tournament_admin to manage their own tournament', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'tour-admin-1', email: 'tour@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
);
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'tour-admin-1')
);
const result = await canManageTournament(1);
expect(result.allowed).toBe(true);
});
test('should deny tournament_admin from managing other users tournaments', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'tour-admin-1', email: 'tour@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
);
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'other-user-1')
);
const result = await canManageTournament(1);
expect(result.allowed).toBe(false);
expect(result.reason).toContain('does not own this tournament');
});
test('should deny player from managing tournaments', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'player-1', email: 'player@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('player-1', 'player@example.com', 'player')
);
const result = await canManageTournament(999);
expect(result.allowed).toBe(false);
expect(result.reason).toContain('Insufficient permissions');
});
test('should deny unauthenticated user', async () => {
getSessionMock.mockImplementation(async () => null);
const result = await canManageTournament(999);
expect(result.allowed).toBe(false);
expect(result.reason).toContain('Not authenticated');
});
});
describe('ownsTournament', () => {
test('should return true if user owns tournament', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'owner-1', email: 'owner@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'owner-1')
);
const result = await ownsTournament(1);
expect(result.allowed).toBe(true);
});
test('should return false if user does not own tournament', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'non-owner-1', email: 'nonowner@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'owner-1')
);
const result = await ownsTournament(1);
expect(result.allowed).toBe(false);
expect(result.reason).toContain('does not own this tournament');
});
});
describe('getManageableTournaments', () => {
test('should return all tournaments for club_admin', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('admin-1', 'admin@example.com', 'club_admin')
);
const mockTournaments = [
{ ...createMockTournament(1, 'user-1'), participants: [] },
{ ...createMockTournament(2, 'user-2'), participants: [] },
{ ...createMockTournament(3, 'user-3'), participants: [] },
];
eventFindManyMock.mockImplementation(async () => mockTournaments);
const result = await getManageableTournaments();
expect(result).toEqual(mockTournaments);
expect(eventFindManyMock).toHaveBeenCalledWith({
where: { eventType: 'tournament' },
include: { participants: true },
orderBy: { createdAt: 'desc' }
});
});
test('should return only owned tournaments for tournament_admin', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'tour-admin-1', email: 'tour@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
);
const mockTournaments = [
{ ...createMockTournament(1, 'tour-admin-1'), participants: [] },
{ ...createMockTournament(2, 'tour-admin-1'), participants: [] },
];
eventFindManyMock.mockImplementation(async () => mockTournaments);
const result = await getManageableTournaments();
expect(result).toEqual(mockTournaments);
expect(eventFindManyMock).toHaveBeenCalledWith({
where: {
eventType: 'tournament',
ownerId: 'tour-admin-1'
},
include: { participants: true },
orderBy: { createdAt: 'desc' }
});
});
test('should return only non-draft tournaments for players', async () => {
getSessionMock.mockImplementation(async () => ({
user: { id: 'player-1', email: 'player@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('player-1', 'player@example.com', 'player')
);
const mockTournaments = [
{ ...createMockTournament(1, 'user-1'), participants: [] },
{ ...createMockTournament(2, 'user-2'), participants: [] },
];
eventFindManyMock.mockImplementation(async () => mockTournaments);
const result = await getManageableTournaments();
expect(result).toEqual(mockTournaments);
expect(eventFindManyMock).toHaveBeenCalledWith({
where: {
eventType: 'tournament',
status: { not: 'draft' }
},
include: { participants: true },
orderBy: { createdAt: 'desc' }
});
});
});
describe('Regression Tests', () => {
test('tournament_admin should be able to access edit page for their tournament', async () => {
// This simulates the scenario where a tournament_admin user
// clicks "Edit" on a tournament they own
getSessionMock.mockImplementation(async () => ({
user: { id: 'tour-admin-1', email: 'tour@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('tour-admin-1', 'tour@example.com', 'tournament_admin')
);
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'tour-admin-1')
);
// This is the permission check that happens on the edit page
const result = await canManageTournament(1);
// Before the fix, this would return false because the page only checked for club_admin
// After the fix, it should return true because the user owns the tournament
expect(result.allowed).toBe(true);
});
test('club_admin should still be able to manage any tournament', async () => {
// This ensures we didn't break the existing club_admin functionality
getSessionMock.mockImplementation(async () => ({
user: { id: 'club-admin-1', email: 'club@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('club-admin-1', 'club@example.com', 'club_admin')
);
const result = await canManageTournament(999);
expect(result.allowed).toBe(true);
});
test('players should still be denied from managing tournaments', async () => {
// This ensures we didn't accidentally grant players access
getSessionMock.mockImplementation(async () => ({
user: { id: 'player-1', email: 'player@example.com' },
session: { token: 'test', expiresAt: new Date() }
}));
userFindUniqueMock.mockImplementation(async () =>
createMockUser('player-1', 'player@example.com', 'player')
);
eventFindUniqueMock.mockImplementation(async () =>
createMockTournament(1, 'other-user-1')
);
const result = await canManageTournament(1);
expect(result.allowed).toBe(false);
});
});
});