/** * 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', format: 'round_robin', status: 'planned', maxParticipants: null, ownerId, targetScore: null, allowTies: false, createdAt: new Date(), updatedAt: new Date(), }); 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'), createMockTournament(2, 'user-2'), createMockTournament(3, 'user-3'), ]; 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'), createMockTournament(2, 'tour-admin-1'), ]; 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'), createMockTournament(2, 'user-2'), ]; 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); }); }); });