258 lines
13 KiB
TypeScript
258 lines
13 KiB
TypeScript
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<T>(userId: string, fn: () => Promise<T>): Promise<T> {
|
|
const req = { user: { userId } };
|
|
const res = {};
|
|
|
|
return await new Promise<T>((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();
|
|
});
|
|
});
|