2025-10-21 22:34:26 -07:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { deleteAllDraftPicks } from '~/models/draft-pick';
|
|
|
|
|
import { clearAllQueuesForSeason } from '~/models/draft-queue';
|
2025-10-24 21:41:30 -07:00
|
|
|
import { deleteSeasonTimers } from '~/models/draft-timer';
|
2025-10-21 22:34:26 -07:00
|
|
|
import { updateSeason } from '~/models/season';
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
import { isUserAdmin } from '~/models/user';
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
// 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,
|
2026-03-20 21:36:39 -07:00
|
|
|
draftTimerMode: 'chess_clock' as const,
|
2025-10-21 22:34:26 -07:00
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
inviteCode: 'ABC123',
|
2025-10-28 23:39:34 -07:00
|
|
|
pointsFor1st: 100,
|
|
|
|
|
pointsFor2nd: 70,
|
|
|
|
|
pointsFor3rd: 50,
|
|
|
|
|
pointsFor4th: 40,
|
|
|
|
|
pointsFor5th: 25,
|
|
|
|
|
pointsFor6th: 25,
|
|
|
|
|
pointsFor7th: 15,
|
|
|
|
|
pointsFor8th: 15,
|
2025-10-21 22:34:26 -07:00
|
|
|
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(),
|
|
|
|
|
}));
|
|
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
vi.mock('~/models/draft-timer', () => ({
|
|
|
|
|
deleteSeasonTimers: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
2025-10-21 22:34:26 -07:00
|
|
|
vi.mock('~/models/season', () => ({
|
|
|
|
|
updateSeason: vi.fn(),
|
|
|
|
|
findCurrentSeasonWithSports: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('~/models/user', () => ({
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
isUserAdmin: vi.fn(),
|
2025-10-21 22:34:26 -07:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
describe('Draft Reset - Authorization', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should only allow admins to reset draft', async () => {
|
|
|
|
|
const userId = 'admin-user-id';
|
|
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
vi.mocked(isUserAdmin).mockResolvedValue(true);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
const isAdmin = await isUserAdmin(userId);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
expect(isAdmin).toBe(true);
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
expect(isUserAdmin).toHaveBeenCalledWith(userId);
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject non-admin users from resetting draft', async () => {
|
|
|
|
|
const userId = 'regular-user-id';
|
|
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
vi.mocked(isUserAdmin).mockResolvedValue(false);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
const isAdmin = await isUserAdmin(userId);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
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
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
vi.mocked(isUserAdmin).mockResolvedValue(false);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
const isAdmin = await isUserAdmin(commissionerUserId);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-21 22:34:26 -07:00
|
|
|
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');
|
|
|
|
|
});
|
2025-10-24 21:41:30 -07:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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';
|
2025-10-24 21:41:30 -07:00
|
|
|
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
|
2025-10-21 22:34:26 -07:00
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(validStatuses.includes(seasonStatus)).toBe(true);
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should allow reset when season status is completed', () => {
|
|
|
|
|
const seasonStatus = 'completed';
|
2025-10-24 21:41:30 -07:00
|
|
|
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
|
2025-10-21 22:34:26 -07:00
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(validStatuses.includes(seasonStatus)).toBe(true);
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should allow reset when season status is active', () => {
|
|
|
|
|
const seasonStatus = 'active';
|
2025-10-24 21:41:30 -07:00
|
|
|
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
|
2025-10-21 22:34:26 -07:00
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(validStatuses.includes(seasonStatus)).toBe(true);
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
it('should allow reset when season status is pre_draft', () => {
|
2025-10-21 22:34:26 -07:00
|
|
|
const seasonStatus = 'pre_draft';
|
2025-10-24 21:41:30 -07:00
|
|
|
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
|
2025-10-21 22:34:26 -07:00
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(validStatuses.includes(seasonStatus)).toBe(true);
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
vi.mocked(isUserAdmin).mockResolvedValue(true);
|
|
|
|
|
const isAdmin = await isUserAdmin(userId);
|
2025-10-21 22:34:26 -07:00
|
|
|
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);
|
|
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
// 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
|
2025-10-21 22:34:26 -07:00
|
|
|
const mockUpdatedSeason = createMockSeason({ id: seasonId, status: 'pre_draft' });
|
|
|
|
|
|
|
|
|
|
vi.mocked(updateSeason).mockResolvedValue(mockUpdatedSeason);
|
2025-10-24 21:41:30 -07:00
|
|
|
const result = await updateSeason(seasonId, {
|
|
|
|
|
status: 'pre_draft',
|
|
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
expect(result.status).toBe('pre_draft');
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
|
|
|
|
|
status: 'pre_draft',
|
|
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not proceed with reset if admin check fails', async () => {
|
|
|
|
|
const userId = 'regular-user-id';
|
|
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
vi.mocked(isUserAdmin).mockResolvedValue(false);
|
|
|
|
|
const isAdmin = await isUserAdmin(userId);
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
expect(isAdmin).toBe(false);
|
|
|
|
|
|
|
|
|
|
// Should not call any reset functions
|
|
|
|
|
expect(deleteAllDraftPicks).not.toHaveBeenCalled();
|
|
|
|
|
expect(clearAllQueuesForSeason).not.toHaveBeenCalled();
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(deleteSeasonTimers).not.toHaveBeenCalled();
|
2025-10-21 22:34:26 -07:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-24 21:41:30 -07:00
|
|
|
vi.mocked(deleteSeasonTimers).mockImplementation(async () => {
|
|
|
|
|
callOrder.push('deleteSeasonTimers');
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-21 22:34:26 -07:00
|
|
|
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);
|
2025-10-24 21:41:30 -07:00
|
|
|
await deleteSeasonTimers(seasonId);
|
|
|
|
|
await updateSeason(seasonId, {
|
|
|
|
|
status: 'pre_draft',
|
|
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
// Verify order
|
|
|
|
|
expect(callOrder).toEqual([
|
|
|
|
|
'deleteAllDraftPicks',
|
|
|
|
|
'clearAllQueuesForSeason',
|
2025-10-24 21:41:30 -07:00
|
|
|
'deleteSeasonTimers',
|
2025-10-21 22:34:26 -07:00
|
|
|
'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);
|
2025-10-24 21:41:30 -07:00
|
|
|
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
|
2025-10-21 22:34:26 -07:00
|
|
|
vi.mocked(updateSeason).mockResolvedValue(
|
|
|
|
|
createMockSeason({ id: seasonId, status: 'pre_draft' })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await deleteAllDraftPicks(seasonId);
|
|
|
|
|
await clearAllQueuesForSeason(seasonId);
|
2025-10-24 21:41:30 -07:00
|
|
|
await deleteSeasonTimers(seasonId);
|
|
|
|
|
await updateSeason(seasonId, {
|
|
|
|
|
status: 'pre_draft',
|
|
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
|
|
|
|
|
// All operations used the same season ID
|
|
|
|
|
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
|
|
|
|
|
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
|
2025-10-24 21:41:30 -07:00
|
|
|
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
|
|
|
|
|
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
|
|
|
|
|
status: 'pre_draft',
|
|
|
|
|
currentPickNumber: null,
|
|
|
|
|
draftStartedAt: null,
|
|
|
|
|
draftPaused: false,
|
|
|
|
|
});
|
2025-10-21 22:34:26 -07:00
|
|
|
});
|
|
|
|
|
});
|