feat: add admin draft reset functionality with queue and pick deletion
This commit is contained in:
parent
4ce71d05d2
commit
86ae5014ec
5 changed files with 578 additions and 7 deletions
|
|
@ -63,3 +63,10 @@ export async function isParticipantDrafted(seasonId: string, participantId: stri
|
||||||
);
|
);
|
||||||
return !!pick;
|
return !!pick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteAllDraftPicks(seasonId: string) {
|
||||||
|
const db = database();
|
||||||
|
await db
|
||||||
|
.delete(schema.draftPicks)
|
||||||
|
.where(eq(schema.draftPicks.seasonId, seasonId));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,3 +74,10 @@ export async function removeParticipantFromQueue(teamId: string, participantId:
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function clearAllQueuesForSeason(seasonId: string) {
|
||||||
|
const db = database();
|
||||||
|
await db
|
||||||
|
.delete(schema.draftQueue)
|
||||||
|
.where(eq(schema.draftQueue.seasonId, seasonId));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { findAllUsers, findUserByClerkId, isUserAdminByClerkId } from "~/models/
|
||||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
import { findAllSportsSeasons } from "~/models/sports-season";
|
||||||
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
|
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
|
||||||
|
import { deleteAllDraftPicks } from "~/models/draft-pick";
|
||||||
|
import { clearAllQueuesForSeason } from "~/models/draft-queue";
|
||||||
import type { Route } from "./+types/$leagueId.settings";
|
import type { Route } from "./+types/$leagueId.settings";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
|
|
@ -275,6 +277,35 @@ export async function action(args: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "reset-draft") {
|
||||||
|
// Check if user is admin (only admins can reset draft)
|
||||||
|
const isAdmin = await isUserAdminByClerkId(userId);
|
||||||
|
if (!isAdmin) {
|
||||||
|
return { error: "Only admins can reset the draft" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if season status is draft or completed
|
||||||
|
if (season.status !== "draft" && season.status !== "completed" && season.status !== "active") {
|
||||||
|
return { error: "Draft can only be reset after it has started" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete all draft picks
|
||||||
|
await deleteAllDraftPicks(season.id);
|
||||||
|
|
||||||
|
// Clear all draft queues
|
||||||
|
await clearAllQueuesForSeason(season.id);
|
||||||
|
|
||||||
|
// Set season status back to pre_draft (keeps draft order intact)
|
||||||
|
await updateSeason(season.id, { status: "pre_draft" });
|
||||||
|
|
||||||
|
return { success: true, message: "Draft has been reset successfully. Draft order preserved." };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resetting draft:", error);
|
||||||
|
return { error: "Failed to reset draft. Please try again." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "delete") {
|
if (intent === "delete") {
|
||||||
// Delete the league
|
// Delete the league
|
||||||
await deleteLeague(leagueId);
|
await deleteLeague(leagueId);
|
||||||
|
|
@ -984,8 +1015,49 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
Irreversible and destructive actions
|
Irreversible and destructive actions
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
{/* Reset Draft - Admin Only */}
|
||||||
|
{isAdmin && season && (season.status === "draft" || season.status === "completed" || season.status === "active") && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-medium">Reset Draft</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Delete all draft picks and reset the season to pre-draft status. The draft order will be preserved.
|
||||||
|
</p>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" className="w-full">
|
||||||
|
Reset Draft
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Reset the draft?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will delete all draft picks and draft queues, and set the season back to pre-draft status.
|
||||||
|
The draft order will remain intact. This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="intent" value="reset-draft" />
|
||||||
|
<AlertDialogAction type="submit" className="bg-destructive hover:bg-destructive/90">
|
||||||
|
Reset Draft
|
||||||
|
</AlertDialogAction>
|
||||||
|
</Form>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete League */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-medium">Delete League</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Permanently delete this league and all associated data.
|
||||||
|
</p>
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" className="w-full">
|
<Button variant="destructive" className="w-full">
|
||||||
Delete League
|
Delete League
|
||||||
|
|
@ -1011,6 +1083,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
# Team Management Integration Tests
|
# League Settings Integration Tests
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This directory contains integration tests for the team management feature in league settings. The tests are written using Vitest and cover the following functionality:
|
This directory contains integration tests for league settings features. The tests are written using Vitest and cover the following functionality:
|
||||||
|
|
||||||
|
### Team Management
|
||||||
- **Remove Team Owner**: Commissioners can remove owners from teams
|
- **Remove Team Owner**: Commissioners can remove owners from teams
|
||||||
- **Assign Team Owner**: Admins can assign users to teams via dropdown
|
- **Assign Team Owner**: Admins can assign users to teams via dropdown
|
||||||
- **Authorization**: Proper access control for commissioners vs admins
|
- **Authorization**: Proper access control for commissioners vs admins
|
||||||
- **Edge Cases**: Handling invalid inputs and error scenarios
|
- **Edge Cases**: Handling invalid inputs and error scenarios
|
||||||
- **Integration Scenarios**: Complete workflows and multi-team operations
|
- **Integration Scenarios**: Complete workflows and multi-team operations
|
||||||
|
|
||||||
|
### Draft Reset
|
||||||
|
- **Admin-Only Access**: Only admins can reset drafts
|
||||||
|
- **Delete Operations**: Clearing picks and queues
|
||||||
|
- **Status Updates**: Resetting season to pre_draft
|
||||||
|
- **Draft Order Preservation**: Ensuring draft slots remain intact
|
||||||
|
- **Data Integrity**: Maintaining referential integrity during reset
|
||||||
|
|
||||||
## Test Structure
|
## Test Structure
|
||||||
|
|
||||||
### Test File
|
### Test Files
|
||||||
- `team-management.test.ts` - Main test suite for team management features
|
- `team-management.test.ts` - Team management feature tests
|
||||||
|
- `draft-reset.test.ts` - Draft reset feature tests
|
||||||
|
|
||||||
### Fixtures
|
### Fixtures
|
||||||
- `app/test/fixtures/user.ts` - Mock user data including admin and regular users
|
- `app/test/fixtures/user.ts` - Mock user data including admin and regular users
|
||||||
|
|
@ -89,6 +98,50 @@ The test suite covers:
|
||||||
|
|
||||||
**Total: 24 tests**
|
**Total: 24 tests**
|
||||||
|
|
||||||
|
## Draft Reset Test Coverage
|
||||||
|
|
||||||
|
### 1. Authorization (3 tests)
|
||||||
|
- ✅ Only allow admins to reset draft
|
||||||
|
- ✅ Reject non-admin users from resetting draft
|
||||||
|
- ✅ Reject commissioners who are not admins
|
||||||
|
|
||||||
|
### 2. Delete Operations (4 tests)
|
||||||
|
- ✅ Delete all draft picks for a season
|
||||||
|
- ✅ Clear all draft queues for a season
|
||||||
|
- ✅ Handle errors when deleting draft picks fails
|
||||||
|
- ✅ Handle errors when clearing queues fails
|
||||||
|
|
||||||
|
### 3. Season Status Update (2 tests)
|
||||||
|
- ✅ Update season status to pre_draft
|
||||||
|
- ✅ Handle errors when updating season status fails
|
||||||
|
|
||||||
|
### 4. Status Validation (4 tests)
|
||||||
|
- ✅ Allow reset when season status is draft
|
||||||
|
- ✅ Allow reset when season status is completed
|
||||||
|
- ✅ Allow reset when season status is active
|
||||||
|
- ✅ Not allow reset when season status is pre_draft
|
||||||
|
|
||||||
|
### 5. Complete Workflow (3 tests)
|
||||||
|
- ✅ Execute complete reset workflow in correct order
|
||||||
|
- ✅ Not proceed with reset if admin check fails
|
||||||
|
- ✅ Handle partial failure gracefully
|
||||||
|
|
||||||
|
### 6. Draft Order Preservation (2 tests)
|
||||||
|
- ✅ Not delete or modify draft slots during reset
|
||||||
|
- ✅ Preserve draft order after reset
|
||||||
|
|
||||||
|
### 7. Edge Cases (4 tests)
|
||||||
|
- ✅ Handle reset when no draft picks exist
|
||||||
|
- ✅ Handle reset when no draft queues exist
|
||||||
|
- ✅ Handle invalid season ID gracefully
|
||||||
|
- ✅ Handle concurrent reset attempts
|
||||||
|
|
||||||
|
### 8. Data Integrity (2 tests)
|
||||||
|
- ✅ Ensure all picks are deleted before updating status
|
||||||
|
- ✅ Maintain referential integrity after reset
|
||||||
|
|
||||||
|
**Total: 24 tests**
|
||||||
|
|
||||||
## Key Features Tested
|
## Key Features Tested
|
||||||
|
|
||||||
### Commissioner Capabilities
|
### Commissioner Capabilities
|
||||||
|
|
@ -99,11 +152,17 @@ The test suite covers:
|
||||||
- Remove owners from any team
|
- Remove owners from any team
|
||||||
- Assign any user to any team via dropdown
|
- Assign any user to any team via dropdown
|
||||||
- Access to full user list for assignment
|
- Access to full user list for assignment
|
||||||
|
- **Reset draft** (admin-only, not commissioners)
|
||||||
|
- Delete all draft picks
|
||||||
|
- Clear all draft queues
|
||||||
|
- Reset season to pre_draft status
|
||||||
|
- Preserve draft order
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- Admin-only check enforced for assignments
|
- Admin-only check enforced for assignments and draft resets
|
||||||
- Commissioner access verified for removals
|
- Commissioner access verified for removals
|
||||||
- Proper error handling for unauthorized access
|
- Proper error handling for unauthorized access
|
||||||
|
- Status validation for draft reset operations
|
||||||
|
|
||||||
## Mocking Strategy
|
## Mocking Strategy
|
||||||
|
|
||||||
|
|
|
||||||
425
app/routes/leagues/__tests__/draft-reset.test.ts
Normal file
425
app/routes/leagues/__tests__/draft-reset.test.ts
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { deleteAllDraftPicks } from '~/models/draft-pick';
|
||||||
|
import { clearAllQueuesForSeason } from '~/models/draft-queue';
|
||||||
|
import { updateSeason } from '~/models/season';
|
||||||
|
import { isUserAdminByClerkId } 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,
|
||||||
|
currentPickNumber: null,
|
||||||
|
draftStartedAt: null,
|
||||||
|
draftPaused: false,
|
||||||
|
inviteCode: 'ABC123',
|
||||||
|
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/season', () => ({
|
||||||
|
updateSeason: vi.fn(),
|
||||||
|
findCurrentSeasonWithSports: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('~/models/user', () => ({
|
||||||
|
isUserAdminByClerkId: 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(isUserAdminByClerkId).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const isAdmin = await isUserAdminByClerkId(userId);
|
||||||
|
|
||||||
|
expect(isAdmin).toBe(true);
|
||||||
|
expect(isUserAdminByClerkId).toHaveBeenCalledWith(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject non-admin users from resetting draft', async () => {
|
||||||
|
const userId = 'regular-user-id';
|
||||||
|
|
||||||
|
vi.mocked(isUserAdminByClerkId).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const isAdmin = await isUserAdminByClerkId(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(isUserAdminByClerkId).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const isAdmin = await isUserAdminByClerkId(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 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 allowedStatuses = ['draft', 'completed', 'active'];
|
||||||
|
|
||||||
|
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow reset when season status is completed', () => {
|
||||||
|
const seasonStatus = 'completed';
|
||||||
|
const allowedStatuses = ['draft', 'completed', 'active'];
|
||||||
|
|
||||||
|
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow reset when season status is active', () => {
|
||||||
|
const seasonStatus = 'active';
|
||||||
|
const allowedStatuses = ['draft', 'completed', 'active'];
|
||||||
|
|
||||||
|
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow reset when season status is pre_draft', () => {
|
||||||
|
const seasonStatus = 'pre_draft';
|
||||||
|
const allowedStatuses = ['draft', 'completed', 'active'];
|
||||||
|
|
||||||
|
expect(allowedStatuses.includes(seasonStatus)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(isUserAdminByClerkId).mockResolvedValue(true);
|
||||||
|
const isAdmin = await isUserAdminByClerkId(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: Update season status
|
||||||
|
const mockUpdatedSeason = createMockSeason({ id: seasonId, status: 'pre_draft' });
|
||||||
|
|
||||||
|
vi.mocked(updateSeason).mockResolvedValue(mockUpdatedSeason);
|
||||||
|
const result = await updateSeason(seasonId, { status: 'pre_draft' });
|
||||||
|
|
||||||
|
expect(result.status).toBe('pre_draft');
|
||||||
|
expect(updateSeason).toHaveBeenCalledWith(seasonId, { status: 'pre_draft' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not proceed with reset if admin check fails', async () => {
|
||||||
|
const userId = 'regular-user-id';
|
||||||
|
|
||||||
|
vi.mocked(isUserAdminByClerkId).mockResolvedValue(false);
|
||||||
|
const isAdmin = await isUserAdminByClerkId(userId);
|
||||||
|
|
||||||
|
expect(isAdmin).toBe(false);
|
||||||
|
|
||||||
|
// Should not call any reset functions
|
||||||
|
expect(deleteAllDraftPicks).not.toHaveBeenCalled();
|
||||||
|
expect(clearAllQueuesForSeason).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(updateSeason).mockImplementation(async () => {
|
||||||
|
callOrder.push('updateSeason');
|
||||||
|
return createMockSeason({ id: seasonId, status: 'pre_draft' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute in correct order
|
||||||
|
await deleteAllDraftPicks(seasonId);
|
||||||
|
await clearAllQueuesForSeason(seasonId);
|
||||||
|
await updateSeason(seasonId, { status: 'pre_draft' });
|
||||||
|
|
||||||
|
// Verify order
|
||||||
|
expect(callOrder).toEqual([
|
||||||
|
'deleteAllDraftPicks',
|
||||||
|
'clearAllQueuesForSeason',
|
||||||
|
'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(updateSeason).mockResolvedValue(
|
||||||
|
createMockSeason({ id: seasonId, status: 'pre_draft' })
|
||||||
|
);
|
||||||
|
|
||||||
|
await deleteAllDraftPicks(seasonId);
|
||||||
|
await clearAllQueuesForSeason(seasonId);
|
||||||
|
await updateSeason(seasonId, { status: 'pre_draft' });
|
||||||
|
|
||||||
|
// All operations used the same season ID
|
||||||
|
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
|
||||||
|
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
|
||||||
|
expect(updateSeason).toHaveBeenCalledWith(seasonId, { status: 'pre_draft' });
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue