brackt/app/routes/leagues/__tests__/draft-reset.test.ts

504 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { deleteAllDraftPicks } from '~/models/draft-pick';
import { clearAllQueuesForSeason } from '~/models/draft-queue';
import { deleteSeasonTimers } from '~/models/draft-timer';
import { updateSeason } from '~/models/season';
import { isUserAdmin } from '~/models/user';
// Helper function to create mock season objects with all required properties
const createMockSeason = (overrides: {
id: string;
status: 'pre_draft' | 'draft' | 'active' | 'completed';
leagueId?: string;
year?: number;
}) => ({
id: overrides.id,
leagueId: overrides.leagueId || 'league-1',
year: overrides.year || 2025,
status: overrides.status,
templateId: null,
draftRounds: 20,
flexSpots: 0,
draftDateTime: null,
draftInitialTime: 120,
draftIncrementTime: 15,
draftTimerMode: 'chess_clock' as const,
currentPickNumber: null,
draftStartedAt: null,
draftCompletedAt: null,
draftPaused: false,
overnightPauseMode: 'none' as const,
overnightPauseStart: null,
overnightPauseEnd: null,
overnightPauseTimezone: null,
inviteCode: 'ABC123',
pointsFor1st: 100,
pointsFor2nd: 70,
pointsFor3rd: 50,
pointsFor4th: 40,
pointsFor5th: 25,
pointsFor6th: 25,
pointsFor7th: 15,
pointsFor8th: 15,
createdAt: new Date(),
updatedAt: new Date(),
});
// Mock the models
vi.mock('~/models/draft-pick', () => ({
deleteAllDraftPicks: vi.fn(),
}));
vi.mock('~/models/draft-queue', () => ({
clearAllQueuesForSeason: vi.fn(),
}));
vi.mock('~/models/draft-timer', () => ({
deleteSeasonTimers: vi.fn(),
}));
vi.mock('~/models/season', () => ({
updateSeason: vi.fn(),
findCurrentSeasonWithSports: vi.fn(),
}));
vi.mock('~/models/user', () => ({
isUserAdmin: vi.fn(),
}));
describe('Draft Reset - Authorization', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should only allow admins to reset draft', async () => {
const userId = 'admin-user-id';
vi.mocked(isUserAdmin).mockResolvedValue(true);
const isAdmin = await isUserAdmin(userId);
expect(isAdmin).toBe(true);
expect(isUserAdmin).toHaveBeenCalledWith(userId);
});
it('should reject non-admin users from resetting draft', async () => {
const userId = 'regular-user-id';
vi.mocked(isUserAdmin).mockResolvedValue(false);
const isAdmin = await isUserAdmin(userId);
expect(isAdmin).toBe(false);
// Simulate action handler logic - should not proceed with reset
if (!isAdmin) {
expect(deleteAllDraftPicks).not.toHaveBeenCalled();
expect(clearAllQueuesForSeason).not.toHaveBeenCalled();
expect(updateSeason).not.toHaveBeenCalled();
}
});
it('should reject commissioners who are not admins', async () => {
const commissionerUserId = 'commissioner-user-id';
// Commissioner but not admin
vi.mocked(isUserAdmin).mockResolvedValue(false);
const isAdmin = await isUserAdmin(commissionerUserId);
expect(isAdmin).toBe(false);
});
});
describe('Draft Reset - Delete Operations', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should delete all draft picks for a season', async () => {
const seasonId = 'season-1';
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
expect(deleteAllDraftPicks).toHaveBeenCalledTimes(1);
});
it('should clear all draft queues for a season', async () => {
const seasonId = 'season-1';
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
await clearAllQueuesForSeason(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledTimes(1);
});
it('should delete all draft timers for a season', async () => {
const seasonId = 'season-1';
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
await deleteSeasonTimers(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledTimes(1);
});
it('should handle errors when deleting draft picks fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
vi.mocked(deleteAllDraftPicks).mockRejectedValue(error);
await expect(deleteAllDraftPicks(seasonId)).rejects.toThrow('Database error');
});
it('should handle errors when clearing queues fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
vi.mocked(clearAllQueuesForSeason).mockRejectedValue(error);
await expect(clearAllQueuesForSeason(seasonId)).rejects.toThrow('Database error');
});
it('should handle errors when deleting timers fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
vi.mocked(deleteSeasonTimers).mockRejectedValue(error);
await expect(deleteSeasonTimers(seasonId)).rejects.toThrow('Database error');
});
});
describe('Draft Reset - Season Status Update', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should update season status to pre_draft', async () => {
const seasonId = 'season-1';
const mockUpdatedSeason = createMockSeason({ id: seasonId, status: 'pre_draft' });
vi.mocked(updateSeason).mockResolvedValue(mockUpdatedSeason);
const result = await updateSeason(seasonId, { status: 'pre_draft' });
expect(updateSeason).toHaveBeenCalledWith(seasonId, { status: 'pre_draft' });
expect(result.status).toBe('pre_draft');
});
it('should handle errors when updating season status fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
vi.mocked(updateSeason).mockRejectedValue(error);
await expect(updateSeason(seasonId, { status: 'pre_draft' })).rejects.toThrow('Database error');
});
});
describe('Draft Reset - Status Validation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should allow reset when season status is draft', () => {
const seasonStatus = 'draft';
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should allow reset when season status is completed', () => {
const seasonStatus = 'completed';
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should allow reset when season status is active', () => {
const seasonStatus = 'active';
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should allow reset when season status is pre_draft', () => {
const seasonStatus = 'pre_draft';
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
});
describe('Draft Reset - Complete Workflow', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should execute complete reset workflow in correct order', async () => {
const seasonId = 'season-1';
const userId = 'admin-user-id';
// Step 1: Verify admin
vi.mocked(isUserAdmin).mockResolvedValue(true);
const isAdmin = await isUserAdmin(userId);
expect(isAdmin).toBe(true);
// Step 2: Delete draft picks
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
// Step 3: Clear draft queues
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
await clearAllQueuesForSeason(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
// Step 4: Delete draft timers
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
await deleteSeasonTimers(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
// Step 5: Update season status and reset draft state
const mockUpdatedSeason = createMockSeason({ id: seasonId, status: 'pre_draft' });
vi.mocked(updateSeason).mockResolvedValue(mockUpdatedSeason);
const result = await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
expect(result.status).toBe('pre_draft');
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
});
it('should not proceed with reset if admin check fails', async () => {
const userId = 'regular-user-id';
vi.mocked(isUserAdmin).mockResolvedValue(false);
const isAdmin = await isUserAdmin(userId);
expect(isAdmin).toBe(false);
// Should not call any reset functions
expect(deleteAllDraftPicks).not.toHaveBeenCalled();
expect(clearAllQueuesForSeason).not.toHaveBeenCalled();
expect(deleteSeasonTimers).not.toHaveBeenCalled();
expect(updateSeason).not.toHaveBeenCalled();
});
it('should handle partial failure gracefully', async () => {
const seasonId = 'season-1';
// First operation succeeds
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
expect(deleteAllDraftPicks).toHaveBeenCalled();
// Second operation fails
const error = new Error('Queue clear failed');
vi.mocked(clearAllQueuesForSeason).mockRejectedValue(error);
await expect(clearAllQueuesForSeason(seasonId)).rejects.toThrow('Queue clear failed');
// Should have attempted both operations
expect(deleteAllDraftPicks).toHaveBeenCalledTimes(1);
expect(clearAllQueuesForSeason).toHaveBeenCalledTimes(1);
});
});
describe('Draft Reset - Draft Order Preservation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should not delete or modify draft slots during reset', async () => {
const seasonId = 'season-1';
// Reset operations should not touch draft slots
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
// Verify only picks and queues are deleted, not slots
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
// Draft slots should remain untouched (no delete function called for them)
});
it('should preserve draft order after reset', async () => {
const seasonId = 'season-1';
// Simulate that draft slots exist before reset
const mockDraftSlots = [
{ id: 'slot-1', seasonId, teamId: 'team-1', draftOrder: 1 },
{ id: 'slot-2', seasonId, teamId: 'team-2', draftOrder: 2 },
{ id: 'slot-3', seasonId, teamId: 'team-3', draftOrder: 3 },
];
// After reset, draft slots should still exist with same order
// (This is implicit - we're not deleting them)
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
// Draft order (slots) remain unchanged
expect(mockDraftSlots).toHaveLength(3);
expect(mockDraftSlots[0].draftOrder).toBe(1);
expect(mockDraftSlots[1].draftOrder).toBe(2);
expect(mockDraftSlots[2].draftOrder).toBe(3);
});
});
describe('Draft Reset - Edge Cases', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should handle reset when no draft picks exist', async () => {
const seasonId = 'season-1';
// No picks to delete, but operation should still succeed
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
await deleteAllDraftPicks(seasonId);
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
});
it('should handle reset when no draft queues exist', async () => {
const seasonId = 'season-1';
// No queues to clear, but operation should still succeed
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
await clearAllQueuesForSeason(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
});
it('should handle invalid season ID gracefully', async () => {
const invalidSeasonId = 'invalid-season-id';
const error = new Error('Season not found');
vi.mocked(updateSeason).mockRejectedValue(error);
await expect(updateSeason(invalidSeasonId, { status: 'pre_draft' })).rejects.toThrow('Season not found');
});
it('should handle concurrent reset attempts', async () => {
const seasonId = 'season-1';
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
// Simulate two concurrent reset attempts
const reset1 = deleteAllDraftPicks(seasonId);
const reset2 = deleteAllDraftPicks(seasonId);
await Promise.all([reset1, reset2]);
// Both should complete (database should handle concurrency)
expect(deleteAllDraftPicks).toHaveBeenCalledTimes(2);
});
});
describe('Draft Reset - Data Integrity', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should ensure all picks are deleted before updating status', async () => {
const seasonId = 'season-1';
const callOrder: string[] = [];
vi.mocked(deleteAllDraftPicks).mockImplementation(async () => {
callOrder.push('deleteAllDraftPicks');
});
vi.mocked(clearAllQueuesForSeason).mockImplementation(async () => {
callOrder.push('clearAllQueuesForSeason');
});
vi.mocked(deleteSeasonTimers).mockImplementation(async () => {
callOrder.push('deleteSeasonTimers');
});
vi.mocked(updateSeason).mockImplementation(async () => {
callOrder.push('updateSeason');
return createMockSeason({ id: seasonId, status: 'pre_draft' });
});
// Execute in correct order
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
await deleteSeasonTimers(seasonId);
await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
// Verify order
expect(callOrder).toEqual([
'deleteAllDraftPicks',
'clearAllQueuesForSeason',
'deleteSeasonTimers',
'updateSeason',
]);
});
it('should maintain referential integrity after reset', async () => {
const seasonId = 'season-1';
// All operations reference the same season
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
vi.mocked(updateSeason).mockResolvedValue(
createMockSeason({ id: seasonId, status: 'pre_draft' })
);
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
await deleteSeasonTimers(seasonId);
await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
// All operations used the same season ID
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
});
});