import { requestContextMiddleware } from '@adapters/http/RequestContext'; import { Result } from '@core/shared/domain/Result'; import { describe, expect, it, vi } from 'vitest'; import { LeagueService } from './LeagueService'; async function withUserId(userId: string, fn: () => Promise): Promise { const req = { user: { userId } }; const res = {}; return await new Promise((resolve, reject) => { requestContextMiddleware(req as never, res as never, () => { fn().then(resolve, reject); }); }); } describe('LeagueService - All Endpoints', () => { it('covers all league endpoint happy paths and error branches', async () => { const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const ok = async () => Result.ok(undefined); const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never); // Discovery use cases const getAllLeaguesWithCapacityUseCase = { execute: vi.fn(async () => Result.ok({ leagues: [] })) }; const getAllLeaguesWithCapacityAndScoringUseCase = { execute: vi.fn(ok) }; const getTotalLeaguesUseCase = { execute: vi.fn(ok) }; // Detail use cases const getLeagueOwnerSummaryUseCase = { execute: vi.fn(ok) }; const getLeagueSeasonsUseCase = { execute: vi.fn(ok) }; const getLeagueStatsUseCase = { execute: vi.fn(ok) }; const getLeagueMembershipsUseCase = { execute: vi.fn(ok) }; // Schedule use case const getLeagueScheduleUseCase = { execute: vi.fn(ok) }; // Standings use case const getLeagueStandingsUseCase = { execute: vi.fn(ok) }; // Other use cases (for completeness) const getLeagueFullConfigUseCase = { execute: vi.fn(ok) }; const getLeagueScoringConfigUseCase = { execute: vi.fn(ok) }; const listLeagueScoringPresetsUseCase = { execute: vi.fn(ok) }; const joinLeagueUseCase = { execute: vi.fn(ok) }; const transferLeagueOwnershipUseCase = { execute: vi.fn(ok) }; const createLeagueWithSeasonAndScoringUseCase = { execute: vi.fn(ok) }; const getLeagueJoinRequestsUseCase = { execute: vi.fn(ok) }; const approveLeagueJoinRequestUseCase = { execute: vi.fn(ok) }; const rejectLeagueJoinRequestUseCase = { execute: vi.fn(ok) }; const removeLeagueMemberUseCase = { execute: vi.fn(ok) }; const updateLeagueMemberRoleUseCase = { execute: vi.fn(ok) }; const getLeagueProtestsUseCase = { execute: vi.fn(ok) }; const getLeagueAdminPermissionsUseCase = { execute: vi.fn(ok) }; const getLeagueWalletUseCase = { execute: vi.fn(ok) }; const withdrawFromLeagueWalletUseCase = { execute: vi.fn(ok) }; const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) }; const getLeagueRosterMembersUseCase = { execute: vi.fn(ok) }; const getLeagueRosterJoinRequestsUseCase = { execute: vi.fn(ok) }; // Schedule mutation use cases const createLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) }; const updateLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) }; const deleteLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) }; const publishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) }; const unpublishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) }; // Presenters const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) }; const allLeaguesWithCapacityAndScoringPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })), }; const leagueStandingsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ standings: [] })) }; const leagueProtestsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ protests: [] })) }; const seasonSponsorshipsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ sponsorships: [] })) }; const leagueScoringPresetsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ presets: [] })) }; const approveLeagueJoinRequestPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const createLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ id: 'l1' })) }; const getLeagueAdminPermissionsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ canManage: true })) }; const getLeagueMembershipsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ memberships: { members: [] } })), }; const getLeagueRosterMembersPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ([])), }; const getLeagueRosterJoinRequestsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ([])), }; const getLeagueOwnerSummaryPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ driver: { id: 'd1', iracingId: '12345', name: 'Driver', country: 'US', joinedAt: '2024-01-01T00:00:00Z' }, rating: 1500, rank: 10 })) }; const getLeagueSeasonsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ([])) }; const joinLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const leagueSchedulePresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) }; const leagueStatsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ totalMembers: 0, totalRaces: 0, averageRating: 0 })) }; const rejectLeagueJoinRequestPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const removeLeagueMemberPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const totalLeaguesPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ totalLeagues: 0 })) }; const transferLeagueOwnershipPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const updateLeagueMemberRolePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) }; const leagueConfigPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ form: {} })) }; const leagueScoringConfigPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ config: {} })) }; const getLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ balance: 0 })) }; const withdrawFromLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) }; const leagueJoinRequestsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ joinRequests: [] })) }; const createLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ raceId: 'race-1' })) }; const updateLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) }; const deleteLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) }; const publishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: true })) }; const unpublishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: false })) }; const service = new (LeagueService as unknown as { new (...args: never[]): LeagueService })( getAllLeaguesWithCapacityUseCase as never, getAllLeaguesWithCapacityAndScoringUseCase as never, getLeagueStandingsUseCase as never, getLeagueStatsUseCase as never, getLeagueFullConfigUseCase as never, getLeagueScoringConfigUseCase as never, listLeagueScoringPresetsUseCase as never, joinLeagueUseCase as never, transferLeagueOwnershipUseCase as never, createLeagueWithSeasonAndScoringUseCase as never, getTotalLeaguesUseCase as never, getLeagueJoinRequestsUseCase as never, approveLeagueJoinRequestUseCase as never, rejectLeagueJoinRequestUseCase as never, removeLeagueMemberUseCase as never, updateLeagueMemberRoleUseCase as never, getLeagueOwnerSummaryUseCase as never, getLeagueProtestsUseCase as never, getLeagueSeasonsUseCase as never, getLeagueMembershipsUseCase as never, getLeagueScheduleUseCase as never, getLeagueAdminPermissionsUseCase as never, getLeagueWalletUseCase as never, withdrawFromLeagueWalletUseCase as never, getSeasonSponsorshipsUseCase as never, createLeagueSeasonScheduleRaceUseCase as never, updateLeagueSeasonScheduleRaceUseCase as never, deleteLeagueSeasonScheduleRaceUseCase as never, publishLeagueSeasonScheduleUseCase as never, unpublishLeagueSeasonScheduleUseCase as never, logger as never, allLeaguesWithCapacityPresenter as never, allLeaguesWithCapacityAndScoringPresenter as never, leagueStandingsPresenter as never, leagueProtestsPresenter as never, seasonSponsorshipsPresenter as never, leagueScoringPresetsPresenter as never, approveLeagueJoinRequestPresenter as never, createLeaguePresenter as never, getLeagueAdminPermissionsPresenter as never, getLeagueMembershipsPresenter as never, getLeagueOwnerSummaryPresenter as never, getLeagueSeasonsPresenter as never, joinLeaguePresenter as never, leagueSchedulePresenter as never, leagueStatsPresenter as never, rejectLeagueJoinRequestPresenter as never, removeLeagueMemberPresenter as never, totalLeaguesPresenter as never, transferLeagueOwnershipPresenter as never, updateLeagueMemberRolePresenter as never, leagueConfigPresenter as never, leagueScoringConfigPresenter as never, getLeagueWalletPresenter as never, withdrawFromLeagueWalletPresenter as never, leagueJoinRequestsPresenter as never, createLeagueSeasonScheduleRacePresenter as never, updateLeagueSeasonScheduleRacePresenter as never, deleteLeagueSeasonScheduleRacePresenter as never, publishLeagueSeasonSchedulePresenter as never, unpublishLeagueSeasonSchedulePresenter as never, // Roster admin read delegation (added for strict TDD) getLeagueRosterMembersUseCase as never, getLeagueRosterJoinRequestsUseCase as never, getLeagueRosterMembersPresenter as never, getLeagueRosterJoinRequestsPresenter as never, ); // Discovery endpoints await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] }); await expect(service.getAllLeaguesWithCapacityAndScoring()).resolves.toEqual({ leagues: [], totalCount: 0 }); await expect(service.getTotalLeagues()).resolves.toEqual({ totalLeagues: 0 }); // Detail endpoints await expect(service.getLeagueOwnerSummary({ leagueId: 'l1' } as never)).resolves.toEqual({ driver: { id: 'd1', iracingId: '12345', name: 'Driver', country: 'US', joinedAt: '2024-01-01T00:00:00Z' }, rating: 1500, rank: 10 }); await expect(service.getLeagueSeasons({ leagueId: 'l1' } as never)).resolves.toEqual([]); await expect(service.getLeagueStats('l1')).resolves.toEqual({ totalMembers: 0, totalRaces: 0, averageRating: 0 }); await expect(service.getLeagueMemberships('l1')).resolves.toEqual({ members: [] }); // Schedule endpoint await expect(service.getLeagueSchedule('l1')).resolves.toEqual({ seasonId: 'season-1', published: false, races: [] }); expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); getLeagueScheduleUseCase.execute.mockClear(); await expect(service.getLeagueSchedule('l1', { seasonId: 'season-x' } as never)).resolves.toEqual({ seasonId: 'season-1', published: false, races: [], }); expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', seasonId: 'season-x' }); // Standings endpoint await expect(service.getLeagueStandings('l1')).resolves.toEqual({ standings: [] }); // Error branches: use case returns error result getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never)); await expect(service.getAllLeaguesWithCapacity()).rejects.toThrow('REPOSITORY_ERROR'); getLeagueFullConfigUseCase.execute.mockResolvedValueOnce( Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never) ); await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull(); getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => { throw new Error('boom'); }); await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull(); // Cover non-Error throw branches for logger.error wrapping getLeagueFullConfigUseCase.execute.mockResolvedValueOnce( Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never) ); await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull(); getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => { throw 'boom'; }); await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull(); // keep lint happy (ensures err() used) await err(); }); });