548 lines
15 KiB
TypeScript
548 lines
15 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
|
||
|
|
// Mock dependencies before imports
|
||
|
|
vi.mock('drizzle-orm/postgres-js', () => ({
|
||
|
|
drizzle: vi.fn(() => mockDb),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock('postgres', () => ({
|
||
|
|
default: vi.fn(() => ({})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock('../socket', () => ({
|
||
|
|
getSocketIO: vi.fn(() => mockSocketIO),
|
||
|
|
}));
|
||
|
|
|
||
|
|
let mockDb: any;
|
||
|
|
let mockSocketIO: any;
|
||
|
|
|
||
|
|
// Setup mocks
|
||
|
|
beforeEach(() => {
|
||
|
|
mockSocketIO = {
|
||
|
|
to: vi.fn().mockReturnThis(),
|
||
|
|
emit: vi.fn(),
|
||
|
|
};
|
||
|
|
|
||
|
|
mockDb = {
|
||
|
|
query: {
|
||
|
|
seasons: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
findFirst: vi.fn(),
|
||
|
|
},
|
||
|
|
draftSlots: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
},
|
||
|
|
draftTimers: {
|
||
|
|
findFirst: vi.fn(),
|
||
|
|
},
|
||
|
|
autodraftSettings: {
|
||
|
|
findFirst: vi.fn(),
|
||
|
|
},
|
||
|
|
draftPicks: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
findFirst: vi.fn(),
|
||
|
|
},
|
||
|
|
draftQueue: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
},
|
||
|
|
participants: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
},
|
||
|
|
seasonTemplateSports: {
|
||
|
|
findMany: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
update: vi.fn().mockReturnThis(),
|
||
|
|
set: vi.fn().mockReturnThis(),
|
||
|
|
where: vi.fn().mockReturnThis(),
|
||
|
|
returning: vi.fn(),
|
||
|
|
insert: vi.fn().mockReturnThis(),
|
||
|
|
values: vi.fn().mockReturnThis(),
|
||
|
|
delete: vi.fn().mockReturnThis(),
|
||
|
|
select: vi.fn().mockReturnThis(),
|
||
|
|
from: vi.fn().mockReturnThis(),
|
||
|
|
innerJoin: vi.fn().mockReturnThis(),
|
||
|
|
orderBy: vi.fn().mockReturnThis(),
|
||
|
|
limit: vi.fn().mockReturnThis(),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Timer Autodraft Integration', () => {
|
||
|
|
describe('Autodraft Settings Check', () => {
|
||
|
|
it('should check autodraft settings when timer expires', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock active draft
|
||
|
|
mockDb.query.seasons.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: seasonId,
|
||
|
|
status: 'draft',
|
||
|
|
draftPaused: false,
|
||
|
|
currentPickNumber: 1,
|
||
|
|
draftInitialTime: 120,
|
||
|
|
draftIncrementTime: 30,
|
||
|
|
draftRounds: 10,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock draft slots
|
||
|
|
mockDb.query.draftSlots.findMany.mockResolvedValue([
|
||
|
|
{ id: 'slot-1', teamId, draftOrder: 1 },
|
||
|
|
{ id: 'slot-2', teamId: 'team-2', draftOrder: 2 },
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock timer at 0
|
||
|
|
mockDb.query.draftTimers.findFirst.mockResolvedValue({
|
||
|
|
id: 'timer-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
timeRemaining: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Mock autodraft settings enabled
|
||
|
|
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
|
||
|
|
// Mock no existing pick
|
||
|
|
mockDb.query.draftPicks.findFirst.mockResolvedValue(null);
|
||
|
|
|
||
|
|
// Mock queue
|
||
|
|
mockDb.query.draftQueue.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'queue-1',
|
||
|
|
participantId: 'participant-1',
|
||
|
|
queuePosition: 1,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock drafted picks
|
||
|
|
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
|
||
|
|
|
||
|
|
// Mock insert returning created pick
|
||
|
|
mockDb.returning.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'pick-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
participantId: 'participant-1',
|
||
|
|
pickNumber: 1,
|
||
|
|
round: 1,
|
||
|
|
pickInRound: 1,
|
||
|
|
pickedByUserId: '',
|
||
|
|
pickedByType: 'auto',
|
||
|
|
timeUsed: 120,
|
||
|
|
createdAt: new Date(),
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock select for complete pick
|
||
|
|
mockDb.limit.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'pick-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
participantId: 'participant-1',
|
||
|
|
pickNumber: 1,
|
||
|
|
round: 1,
|
||
|
|
pickInRound: 1,
|
||
|
|
pickedByUserId: '',
|
||
|
|
pickedByType: 'auto',
|
||
|
|
timeUsed: 120,
|
||
|
|
createdAt: new Date(),
|
||
|
|
team: { id: teamId, name: 'Test Team' },
|
||
|
|
participant: { id: 'participant-1', name: 'Test Participant' },
|
||
|
|
sport: { id: 'sport-1', name: 'Test Sport' },
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// The actual timer logic would be tested here
|
||
|
|
// This test verifies the mocks are set up correctly
|
||
|
|
expect(mockDb.query.autodraftSettings.findFirst).toBeDefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should use regular auto-pick when autodraft is disabled', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock autodraft settings disabled
|
||
|
|
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: false,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
|
||
|
|
const settings = await mockDb.query.autodraftSettings.findFirst();
|
||
|
|
expect(settings.isEnabled).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle missing autodraft settings', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock no autodraft settings
|
||
|
|
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
|
||
|
|
|
||
|
|
const settings = await mockDb.query.autodraftSettings.findFirst();
|
||
|
|
expect(settings).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Autodraft Mode Handling', () => {
|
||
|
|
it('should disable autodraft after pick when mode is next_pick', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
const autodraftSettings = {
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'next_pick',
|
||
|
|
};
|
||
|
|
|
||
|
|
// Mock update
|
||
|
|
mockDb.returning.mockResolvedValue([
|
||
|
|
{
|
||
|
|
...autodraftSettings,
|
||
|
|
isEnabled: false,
|
||
|
|
updatedAt: new Date(),
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Simulate disabling after pick
|
||
|
|
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
|
||
|
|
await mockDb.update();
|
||
|
|
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
|
||
|
|
await mockDb.where();
|
||
|
|
const result = await mockDb.returning();
|
||
|
|
|
||
|
|
expect(result[0].isEnabled).toBe(false);
|
||
|
|
expect(mockSocketIO.to).toBeDefined();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should keep autodraft enabled when mode is while_on', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
const autodraftSettings = {
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId: _seasonId,
|
||
|
|
teamId: _teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'while_on',
|
||
|
|
};
|
||
|
|
|
||
|
|
// Should not disable when mode is while_on
|
||
|
|
if (autodraftSettings.mode === 'while_on') {
|
||
|
|
expect(autodraftSettings.isEnabled).toBe(true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Autodraft Pick Logic', () => {
|
||
|
|
it('should pick from queue first when autodraft is enabled', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock queue with available participant
|
||
|
|
mockDb.query.draftQueue.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'queue-1',
|
||
|
|
participantId: 'participant-1',
|
||
|
|
queuePosition: 1,
|
||
|
|
participant: {
|
||
|
|
id: 'participant-1',
|
||
|
|
name: 'Queued Participant',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'queue-2',
|
||
|
|
participantId: 'participant-2',
|
||
|
|
queuePosition: 2,
|
||
|
|
participant: {
|
||
|
|
id: 'participant-2',
|
||
|
|
name: 'Second Queued',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock no drafted picks
|
||
|
|
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
|
||
|
|
|
||
|
|
const queueItems = await mockDb.query.draftQueue.findMany();
|
||
|
|
const draftedPicks = await mockDb.query.draftPicks.findMany();
|
||
|
|
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||
|
|
|
||
|
|
// Find first non-drafted participant from queue
|
||
|
|
const availableQueueItem = queueItems.find(
|
||
|
|
(item: any) => !draftedParticipantIds.includes(item.participantId)
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(availableQueueItem).toBeDefined();
|
||
|
|
expect(availableQueueItem.participantId).toBe('participant-1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should skip drafted participants in queue', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock queue
|
||
|
|
mockDb.query.draftQueue.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'queue-1',
|
||
|
|
participantId: 'participant-1',
|
||
|
|
queuePosition: 1,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'queue-2',
|
||
|
|
participantId: 'participant-2',
|
||
|
|
queuePosition: 2,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock first participant already drafted
|
||
|
|
mockDb.query.draftPicks.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'pick-1',
|
||
|
|
participantId: 'participant-1',
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
const queueItems = await mockDb.query.draftQueue.findMany();
|
||
|
|
const draftedPicks = await mockDb.query.draftPicks.findMany();
|
||
|
|
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||
|
|
|
||
|
|
// Should pick second participant
|
||
|
|
const availableQueueItem = queueItems.find(
|
||
|
|
(item: any) => !draftedParticipantIds.includes(item.participantId)
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(availableQueueItem.participantId).toBe('participant-2');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should fall back to highest EV when queue is empty or all drafted', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock empty queue
|
||
|
|
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
|
||
|
|
|
||
|
|
// Mock season template sports
|
||
|
|
mockDb.query.seasonTemplateSports.findMany.mockResolvedValue([
|
||
|
|
{ sportsSeasonId: 'sports-season-1' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Mock available participants sorted by EV
|
||
|
|
mockDb.query.participants.findMany.mockResolvedValue([
|
||
|
|
{
|
||
|
|
id: 'participant-high-ev',
|
||
|
|
name: 'High EV Participant',
|
||
|
|
expectedValue: 1000,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
const queueItems = await mockDb.query.draftQueue.findMany();
|
||
|
|
expect(queueItems.length).toBe(0);
|
||
|
|
|
||
|
|
const availableParticipants = await mockDb.query.participants.findMany();
|
||
|
|
expect(availableParticipants[0].expectedValue).toBe(1000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Socket Event Emission', () => {
|
||
|
|
it('should emit autodraft-updated event when disabling next_pick mode', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
|
||
|
|
teamId,
|
||
|
|
isEnabled: false,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
|
||
|
|
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
|
||
|
|
teamId,
|
||
|
|
isEnabled: false,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should emit pick-made event after autodraft pick', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
|
||
|
|
const pickData = {
|
||
|
|
pick: {
|
||
|
|
id: 'pick-1',
|
||
|
|
participantId: 'participant-1',
|
||
|
|
pickNumber: 1,
|
||
|
|
},
|
||
|
|
nextPickNumber: 2,
|
||
|
|
isDraftComplete: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
mockSocketIO.to(`draft-${seasonId}`).emit('pick-made', pickData);
|
||
|
|
|
||
|
|
expect(mockSocketIO.emit).toHaveBeenCalledWith('pick-made', pickData);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Back-to-Back Picks', () => {
|
||
|
|
it('should only autodraft first pick when mode is next_pick and team has consecutive picks', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock autodraft settings with next_pick mode
|
||
|
|
const autodraftSettings = {
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'next_pick',
|
||
|
|
};
|
||
|
|
|
||
|
|
// Simulate first pick (pick #1)
|
||
|
|
const _firstPickNumber = 1;
|
||
|
|
|
||
|
|
// After first pick, autodraft should be disabled
|
||
|
|
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
|
||
|
|
// Simulate disabling autodraft after first pick
|
||
|
|
const updatedSettings = {
|
||
|
|
...autodraftSettings,
|
||
|
|
isEnabled: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(updatedSettings.isEnabled).toBe(false);
|
||
|
|
expect(updatedSettings.mode).toBe('next_pick');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Simulate second pick (pick #2) - should NOT autodraft
|
||
|
|
const _secondPickNumber = 2;
|
||
|
|
|
||
|
|
// Autodraft should now be disabled, so second pick requires manual action
|
||
|
|
const settingsForSecondPick = {
|
||
|
|
...autodraftSettings,
|
||
|
|
isEnabled: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(settingsForSecondPick.isEnabled).toBe(false);
|
||
|
|
|
||
|
|
// Verify that autodraft-updated event would be emitted after first pick
|
||
|
|
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
|
||
|
|
teamId,
|
||
|
|
isEnabled: false,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
|
||
|
|
teamId,
|
||
|
|
isEnabled: false,
|
||
|
|
mode: 'next_pick',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should continue autodrafting consecutive picks when mode is while_on', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
// Mock autodraft settings with while_on mode
|
||
|
|
const autodraftSettings = {
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'while_on',
|
||
|
|
};
|
||
|
|
|
||
|
|
// Simulate first pick (pick #1)
|
||
|
|
const _firstPickNumber = 1;
|
||
|
|
|
||
|
|
// After first pick, autodraft should REMAIN enabled for while_on mode
|
||
|
|
if (autodraftSettings.mode === 'while_on') {
|
||
|
|
expect(autodraftSettings.isEnabled).toBe(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Simulate second pick (pick #2) - SHOULD autodraft
|
||
|
|
const _secondPickNumber = 2;
|
||
|
|
|
||
|
|
// Autodraft should still be enabled for second pick
|
||
|
|
expect(autodraftSettings.isEnabled).toBe(true);
|
||
|
|
expect(autodraftSettings.mode).toBe('while_on');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle snake draft back-to-back picks correctly', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
const _totalTeams = 10;
|
||
|
|
|
||
|
|
// In a snake draft with 10 teams:
|
||
|
|
// Pick 10 (end of round 1) and Pick 11 (start of round 2) are back-to-back for same team
|
||
|
|
const _firstPick = 10; // Last pick of round 1
|
||
|
|
const _secondPick = 11; // First pick of round 2 (same team in snake draft)
|
||
|
|
|
||
|
|
// Mock autodraft with next_pick mode
|
||
|
|
const autodraftSettings = {
|
||
|
|
id: 'settings-1',
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
isEnabled: true,
|
||
|
|
mode: 'next_pick',
|
||
|
|
};
|
||
|
|
|
||
|
|
// After pick 10, autodraft should be disabled
|
||
|
|
const settingsAfterFirstPick = {
|
||
|
|
...autodraftSettings,
|
||
|
|
isEnabled: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(settingsAfterFirstPick.isEnabled).toBe(false);
|
||
|
|
|
||
|
|
// Pick 11 should NOT autodraft because autodraft was disabled after pick 10
|
||
|
|
// This ensures the team owner has a chance to make their second pick manually
|
||
|
|
expect(settingsAfterFirstPick.mode).toBe('next_pick');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Time Increment', () => {
|
||
|
|
it('should add increment time after autodraft pick', async () => {
|
||
|
|
const _seasonId = 'season-123';
|
||
|
|
const _teamId = 'team-456';
|
||
|
|
const incrementTime = 30;
|
||
|
|
const currentTime = 0;
|
||
|
|
|
||
|
|
const newTimeRemaining = currentTime + incrementTime;
|
||
|
|
|
||
|
|
expect(newTimeRemaining).toBe(30);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should emit timer-update after adding increment', async () => {
|
||
|
|
const seasonId = 'season-123';
|
||
|
|
const teamId = 'team-456';
|
||
|
|
|
||
|
|
mockSocketIO.to(`draft-${seasonId}`).emit('timer-update', {
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
timeRemaining: 30,
|
||
|
|
currentPickNumber: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-update', {
|
||
|
|
seasonId,
|
||
|
|
teamId,
|
||
|
|
timeRemaining: 30,
|
||
|
|
currentPickNumber: 1,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|