test apps api
This commit is contained in:
@@ -1,41 +1,58 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { LeagueController } from './LeagueController';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueProviders } from './LeagueProviders';
|
||||
|
||||
describe('LeagueController (integration)', () => {
|
||||
describe('LeagueController', () => {
|
||||
let controller: LeagueController;
|
||||
let leagueService: ReturnType<typeof vi.mocked<LeagueService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [LeagueController],
|
||||
providers: [LeagueService, ...LeagueProviders],
|
||||
providers: [
|
||||
{
|
||||
provide: LeagueService,
|
||||
useValue: {
|
||||
getTotalLeagues: vi.fn(),
|
||||
getAllLeaguesWithCapacity: vi.fn(),
|
||||
getLeagueStandings: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<LeagueController>(LeagueController);
|
||||
leagueService = vi.mocked(module.get(LeagueService));
|
||||
});
|
||||
|
||||
it('should get total leagues', async () => {
|
||||
it('getTotalLeagues should return total leagues', async () => {
|
||||
const mockResult = { totalLeagues: 1 };
|
||||
leagueService.getTotalLeagues.mockResolvedValue(mockResult as any);
|
||||
|
||||
const result = await controller.getTotalLeagues();
|
||||
expect(result).toHaveProperty('totalLeagues');
|
||||
expect(typeof result.totalLeagues).toBe('number');
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(leagueService.getTotalLeagues).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should get all leagues with capacity', async () => {
|
||||
it('getAllLeaguesWithCapacity should return leagues and totalCount', async () => {
|
||||
const mockResult = { leagues: [], totalCount: 0 };
|
||||
leagueService.getAllLeaguesWithCapacity.mockResolvedValue(mockResult as any);
|
||||
|
||||
const result = await controller.getAllLeaguesWithCapacity();
|
||||
expect(result).toHaveProperty('leagues');
|
||||
expect(result).toHaveProperty('totalCount');
|
||||
expect(Array.isArray(result.leagues)).toBe(true);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(leagueService.getAllLeaguesWithCapacity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should get league standings', async () => {
|
||||
try {
|
||||
const result = await controller.getLeagueStandings('non-existent-league');
|
||||
expect(result).toHaveProperty('standings');
|
||||
expect(Array.isArray(result.standings)).toBe(true);
|
||||
} catch (error) {
|
||||
// Expected for non-existent league
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
it('getLeagueStandings should return standings', async () => {
|
||||
const mockResult = { standings: [] };
|
||||
leagueService.getLeagueStandings.mockResolvedValue(mockResult as any);
|
||||
|
||||
const result = await controller.getLeagueStandings('league-1');
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(leagueService.getLeagueStandings).toHaveBeenCalledWith('league-1');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Patch, Post, Inject } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
@@ -37,7 +37,7 @@ import { WithdrawFromLeagueWalletOutputDTO } from './dtos/WithdrawFromLeagueWall
|
||||
@ApiTags('leagues')
|
||||
@Controller('leagues')
|
||||
export class LeagueController {
|
||||
constructor(private readonly leagueService: LeagueService) {}
|
||||
constructor(@Inject(LeagueService) private readonly leagueService: LeagueService) {}
|
||||
|
||||
@Get('all-with-capacity')
|
||||
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
|
||||
|
||||
@@ -241,10 +241,10 @@ export const LeagueProviders: Provider[] = [
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: GetAllLeaguesWithCapacityUseCase,
|
||||
useFactory: (leagueRepo: ILeagueRepository, membershipRepo: ILeagueMembershipRepository, presenter: AllLeaguesWithCapacityPresenter) =>
|
||||
new GetAllLeaguesWithCapacityUseCase(leagueRepo, membershipRepo, presenter),
|
||||
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, 'AllLeaguesWithCapacityPresenter'],
|
||||
provide: GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
|
||||
useFactory: (leagueRepo: ILeagueRepository, membershipRepo: ILeagueMembershipRepository) =>
|
||||
new GetAllLeaguesWithCapacityUseCase(leagueRepo, membershipRepo),
|
||||
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_LEAGUE_STANDINGS_USE_CASE,
|
||||
|
||||
196
apps/api/src/domain/league/LeagueService.test.ts
Normal file
196
apps/api/src/domain/league/LeagueService.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import { LeagueService } from './LeagueService';
|
||||
|
||||
describe('LeagueService', () => {
|
||||
it('covers LeagueService 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' } });
|
||||
|
||||
const getAllLeaguesWithCapacityUseCase: any = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
|
||||
const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueStatsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueFullConfigUseCase: any = { 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 getTotalLeaguesUseCase = { 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 getLeagueOwnerSummaryUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueProtestsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueSeasonsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueMembershipsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueScheduleUseCase = { 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 allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
|
||||
const leagueStandingsPresenter = { getResponseModel: vi.fn(() => ({ standings: [] })) };
|
||||
const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) };
|
||||
const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) };
|
||||
const leagueScoringPresetsPresenter = { getViewModel: vi.fn(() => ({ presets: [] })) };
|
||||
const approveLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const createLeaguePresenter = { getViewModel: vi.fn(() => ({ id: 'l1' })) };
|
||||
const getLeagueAdminPermissionsPresenter = { getResponseModel: vi.fn(() => ({ canManage: true })) };
|
||||
const getLeagueMembershipsPresenter = { getViewModel: vi.fn(() => ({ memberships: { memberships: [] } })) };
|
||||
const getLeagueOwnerSummaryPresenter = { getViewModel: vi.fn(() => ({ ownerId: 'o1' })) };
|
||||
const getLeagueSeasonsPresenter = { getResponseModel: vi.fn(() => ([])) };
|
||||
const joinLeaguePresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const leagueSchedulePresenter = { getViewModel: vi.fn(() => ({ schedule: [] })) };
|
||||
const leagueStatsPresenter = { getResponseModel: vi.fn(() => ({ stats: {} })) };
|
||||
const rejectLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const removeLeagueMemberPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const totalLeaguesPresenter = { getResponseModel: vi.fn(() => ({ total: 1 })) };
|
||||
const transferLeagueOwnershipPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const updateLeagueMemberRolePresenter = { getViewModel: vi.fn(() => ({ success: true })) };
|
||||
const leagueConfigPresenter = { getViewModel: vi.fn(() => ({ form: {} })) };
|
||||
const leagueScoringConfigPresenter = { getViewModel: vi.fn(() => ({ config: {} })) };
|
||||
const getLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ balance: 0 })) };
|
||||
const withdrawFromLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ success: true })) };
|
||||
const leagueJoinRequestsPresenter = { getViewModel: vi.fn(() => ({ joinRequests: [] })) };
|
||||
const leagueRacesPresenter = { getViewModel: vi.fn(() => ([])) };
|
||||
|
||||
const service = new LeagueService(
|
||||
getAllLeaguesWithCapacityUseCase as any,
|
||||
getLeagueStandingsUseCase as any,
|
||||
getLeagueStatsUseCase as any,
|
||||
getLeagueFullConfigUseCase as any,
|
||||
getLeagueScoringConfigUseCase as any,
|
||||
listLeagueScoringPresetsUseCase as any,
|
||||
joinLeagueUseCase as any,
|
||||
transferLeagueOwnershipUseCase as any,
|
||||
createLeagueWithSeasonAndScoringUseCase as any,
|
||||
getTotalLeaguesUseCase as any,
|
||||
getLeagueJoinRequestsUseCase as any,
|
||||
approveLeagueJoinRequestUseCase as any,
|
||||
rejectLeagueJoinRequestUseCase as any,
|
||||
removeLeagueMemberUseCase as any,
|
||||
updateLeagueMemberRoleUseCase as any,
|
||||
getLeagueOwnerSummaryUseCase as any,
|
||||
getLeagueProtestsUseCase as any,
|
||||
getLeagueSeasonsUseCase as any,
|
||||
getLeagueMembershipsUseCase as any,
|
||||
getLeagueScheduleUseCase as any,
|
||||
getLeagueAdminPermissionsUseCase as any,
|
||||
getLeagueWalletUseCase as any,
|
||||
withdrawFromLeagueWalletUseCase as any,
|
||||
getSeasonSponsorshipsUseCase as any,
|
||||
logger as any,
|
||||
allLeaguesWithCapacityPresenter as any,
|
||||
leagueStandingsPresenter as any,
|
||||
leagueProtestsPresenter as any,
|
||||
seasonSponsorshipsPresenter as any,
|
||||
leagueScoringPresetsPresenter as any,
|
||||
approveLeagueJoinRequestPresenter as any,
|
||||
createLeaguePresenter as any,
|
||||
getLeagueAdminPermissionsPresenter as any,
|
||||
getLeagueMembershipsPresenter as any,
|
||||
getLeagueOwnerSummaryPresenter as any,
|
||||
getLeagueSeasonsPresenter as any,
|
||||
joinLeaguePresenter as any,
|
||||
leagueSchedulePresenter as any,
|
||||
leagueStatsPresenter as any,
|
||||
rejectLeagueJoinRequestPresenter as any,
|
||||
removeLeagueMemberPresenter as any,
|
||||
totalLeaguesPresenter as any,
|
||||
transferLeagueOwnershipPresenter as any,
|
||||
updateLeagueMemberRolePresenter as any,
|
||||
leagueConfigPresenter as any,
|
||||
leagueScoringConfigPresenter as any,
|
||||
getLeagueWalletPresenter as any,
|
||||
withdrawFromLeagueWalletPresenter as any,
|
||||
leagueJoinRequestsPresenter as any,
|
||||
leagueRacesPresenter as any,
|
||||
);
|
||||
|
||||
await expect(service.getTotalLeagues()).resolves.toEqual({ total: 1 });
|
||||
await expect(service.getLeagueJoinRequests('l1')).resolves.toEqual([]);
|
||||
|
||||
await expect(service.approveLeagueJoinRequest({ leagueId: 'l1', requestId: 'r1' } as any)).resolves.toEqual({ success: true });
|
||||
await expect(service.rejectLeagueJoinRequest({ leagueId: 'l1', requestId: 'r1' } as any)).resolves.toEqual({ success: true });
|
||||
|
||||
await expect(service.getLeagueAdminPermissions({ leagueId: 'l1' } as any)).resolves.toEqual({ canManage: true });
|
||||
await expect(service.removeLeagueMember({ leagueId: 'l1', targetDriverId: 'd1' } as any)).resolves.toEqual({ success: true });
|
||||
await expect(service.updateLeagueMemberRole({ leagueId: 'l1', targetDriverId: 'd1', newRole: 'member' } as any)).resolves.toEqual({ success: true });
|
||||
|
||||
await expect(service.getLeagueOwnerSummary({ leagueId: 'l1' } as any)).resolves.toEqual({ ownerId: 'o1' });
|
||||
await expect(service.getLeagueProtests({ leagueId: 'l1' } as any)).resolves.toEqual({ protests: [] });
|
||||
await expect(service.getLeagueSeasons({ leagueId: 'l1' } as any)).resolves.toEqual([]);
|
||||
|
||||
await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).resolves.toEqual({ form: {} });
|
||||
await expect(service.getLeagueScoringConfig('l1')).resolves.toEqual({ config: {} });
|
||||
|
||||
await expect(service.getLeagueMemberships('l1')).resolves.toEqual({ memberships: [] });
|
||||
await expect(service.getLeagueStandings('l1')).resolves.toEqual({ standings: [] });
|
||||
await expect(service.getLeagueSchedule('l1')).resolves.toEqual({ schedule: [] });
|
||||
await expect(service.getLeagueStats('l1')).resolves.toEqual({ stats: {} });
|
||||
|
||||
await expect(service.createLeague({ name: 'n', description: 'd', ownerId: 'o' } as any)).resolves.toEqual({ id: 'l1' });
|
||||
await expect(service.listLeagueScoringPresets()).resolves.toEqual({ presets: [] });
|
||||
await expect(service.joinLeague('l1', 'd1')).resolves.toEqual({ success: true });
|
||||
await expect(service.transferLeagueOwnership('l1', 'o1', 'o2')).resolves.toEqual({ success: true });
|
||||
|
||||
await expect(service.getSeasonSponsorships('s1')).resolves.toEqual({ sponsorships: [] });
|
||||
await expect(service.getRaces('l1')).resolves.toEqual({ races: [] });
|
||||
|
||||
await expect(service.getLeagueWallet('l1')).resolves.toEqual({ balance: 0 });
|
||||
await expect(service.withdrawFromLeagueWallet('l1', { amount: 1, currency: 'USD', destinationAccount: 'x' } as any)).resolves.toEqual({
|
||||
success: true,
|
||||
});
|
||||
|
||||
await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
|
||||
|
||||
// Error branch: getAllLeaguesWithCapacity throws on result.isErr()
|
||||
getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } }));
|
||||
await expect(service.getAllLeaguesWithCapacity()).rejects.toThrow('REPOSITORY_ERROR');
|
||||
|
||||
// Error branches: try/catch returning null
|
||||
getLeagueFullConfigUseCase.execute.mockImplementationOnce(async () => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).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.mockImplementationOnce(async () => {
|
||||
throw 'boom';
|
||||
});
|
||||
await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).resolves.toBeNull();
|
||||
|
||||
getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => {
|
||||
throw 'boom';
|
||||
});
|
||||
await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull();
|
||||
|
||||
// getLeagueAdmin error branch: fullConfigResult is Err
|
||||
getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } }));
|
||||
await expect(service.getLeagueAdmin('l1')).rejects.toThrow('REPOSITORY_ERROR');
|
||||
|
||||
// getLeagueAdmin happy path
|
||||
getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(Result.ok(undefined));
|
||||
await expect(service.getLeagueAdmin('l1')).resolves.toEqual({
|
||||
joinRequests: [],
|
||||
ownerSummary: { ownerId: 'o1' },
|
||||
config: { form: { form: {} } },
|
||||
protests: { protests: [] },
|
||||
seasons: [],
|
||||
});
|
||||
|
||||
// keep lint happy (ensures err() used)
|
||||
await err();
|
||||
});
|
||||
});
|
||||
@@ -204,7 +204,17 @@ export class LeagueService {
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
|
||||
|
||||
await this.getAllLeaguesWithCapacityUseCase.execute();
|
||||
const result = await this.getAllLeaguesWithCapacityUseCase.execute({});
|
||||
|
||||
if (result.isErr()) {
|
||||
const err = result.unwrapErr();
|
||||
this.logger.error('[LeagueService] Failed to fetch leagues with capacity', new Error(err.code), {
|
||||
details: err.details,
|
||||
});
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.allLeaguesWithCapacityPresenter.present(result.unwrap());
|
||||
return this.allLeaguesWithCapacityPresenter.getViewModel();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,17 @@ describe('GetLeagueMembershipsPresenter', () => {
|
||||
membership: {
|
||||
driverId: 'driver-1',
|
||||
role: 'member',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
joinedAt: {} as any,
|
||||
joinedAt: { toDate: () => new Date('2024-01-01T00:00:00Z') },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
driver: { id: 'driver-1', name: 'John Doe' } as any,
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
iracingId: '12345',
|
||||
name: 'John Doe',
|
||||
country: 'US',
|
||||
joinedAt: { toDate: () => new Date('2024-01-01T00:00:00Z') },
|
||||
} as any,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -10,15 +10,20 @@ export class GetLeagueSeasonsPresenter implements Presenter<GetLeagueSeasonsResu
|
||||
}
|
||||
|
||||
present(input: GetLeagueSeasonsResult) {
|
||||
this.result = input.seasons.map(seasonSummary => ({
|
||||
seasonId: seasonSummary.season.id.toString(),
|
||||
name: seasonSummary.season.name.toString(),
|
||||
status: seasonSummary.season.status.toString(),
|
||||
startDate: seasonSummary.season.startDate.toISOString(),
|
||||
endDate: seasonSummary.season.endDate?.toISOString(),
|
||||
isPrimary: seasonSummary.isPrimary,
|
||||
isParallelActive: seasonSummary.isParallelActive,
|
||||
}));
|
||||
this.result = input.seasons.map((seasonSummary) => {
|
||||
const dto = new LeagueSeasonSummaryDTO();
|
||||
dto.seasonId = seasonSummary.season.id.toString();
|
||||
dto.name = seasonSummary.season.name.toString();
|
||||
dto.status = seasonSummary.season.status.toString();
|
||||
|
||||
if (seasonSummary.season.startDate) dto.startDate = seasonSummary.season.startDate;
|
||||
if (seasonSummary.season.endDate) dto.endDate = seasonSummary.season.endDate;
|
||||
|
||||
dto.isPrimary = seasonSummary.isPrimary;
|
||||
dto.isParallelActive = seasonSummary.isParallelActive;
|
||||
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
getResponseModel(): LeagueSeasonSummaryDTO[] | null {
|
||||
|
||||
@@ -14,8 +14,7 @@ describe('LeagueOwnerSummaryPresenter', () => {
|
||||
name: 'John Doe',
|
||||
country: 'US',
|
||||
bio: 'Racing enthusiast',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
joinedAt: {} as any,
|
||||
joinedAt: { toDate: () => new Date('2024-01-01T00:00:00Z') } as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
rating: 1500,
|
||||
|
||||
Reference in New Issue
Block a user