test apps api

This commit is contained in:
2025-12-23 23:14:51 +01:00
parent 16cd572c63
commit efcdbd17f2
71 changed files with 3924 additions and 913 deletions

View File

@@ -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');
});
});

View File

@@ -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' })

View File

@@ -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,

View 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();
});
});

View File

@@ -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();
}

View File

@@ -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,
},
],
};

View File

@@ -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 {

View File

@@ -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,