view models
This commit is contained in:
149
apps/website/lib/services/races/RaceResultsService.test.ts
Normal file
149
apps/website/lib/services/races/RaceResultsService.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { RaceResultsService } from './RaceResultsService';
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
|
||||
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
|
||||
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
|
||||
import type { RaceResultsDetailDTO, RaceWithSOFDTO } from '../../types/generated';
|
||||
|
||||
describe('RaceResultsService', () => {
|
||||
let mockApiClient: Mocked<RacesApiClient>;
|
||||
let service: RaceResultsService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getResultsDetail: vi.fn(),
|
||||
getWithSOF: vi.fn(),
|
||||
importResults: vi.fn(),
|
||||
} as Mocked<RacesApiClient>;
|
||||
|
||||
service = new RaceResultsService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getResultsDetail', () => {
|
||||
it('should call apiClient.getResultsDetail and return RaceResultsDetailViewModel', async () => {
|
||||
const raceId = 'race-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const mockDto: RaceResultsDetailDTO = {
|
||||
raceId,
|
||||
track: 'Test Track',
|
||||
};
|
||||
|
||||
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getResultsDetail(raceId, currentUserId);
|
||||
|
||||
expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
|
||||
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
|
||||
expect(result.raceId).toBe(raceId);
|
||||
expect(result.track).toBe('Test Track');
|
||||
});
|
||||
|
||||
it('should handle undefined currentUserId', async () => {
|
||||
const raceId = 'race-123';
|
||||
|
||||
const mockDto: RaceResultsDetailDTO = {
|
||||
raceId,
|
||||
track: 'Test Track',
|
||||
};
|
||||
|
||||
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getResultsDetail(raceId);
|
||||
|
||||
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
|
||||
expect(result.currentUserId).toBe('');
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getResultsDetail fails', async () => {
|
||||
const raceId = 'race-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getResultsDetail.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getResultsDetail(raceId, currentUserId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWithSOF', () => {
|
||||
it('should call apiClient.getWithSOF and return RaceWithSOFViewModel', async () => {
|
||||
const raceId = 'race-123';
|
||||
|
||||
const mockDto: RaceWithSOFDTO = {
|
||||
id: raceId,
|
||||
track: 'Test Track',
|
||||
};
|
||||
|
||||
mockApiClient.getWithSOF.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getWithSOF(raceId);
|
||||
|
||||
expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
|
||||
expect(result).toBeInstanceOf(RaceWithSOFViewModel);
|
||||
expect(result.id).toBe(raceId);
|
||||
expect(result.track).toBe('Test Track');
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getWithSOF fails', async () => {
|
||||
const raceId = 'race-123';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getWithSOF.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getWithSOF(raceId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('importResults', () => {
|
||||
it('should call apiClient.importResults and return ImportRaceResultsSummaryViewModel', async () => {
|
||||
const raceId = 'race-123';
|
||||
const input = { raceId, results: [{ position: 1 }] };
|
||||
|
||||
const mockDto = {
|
||||
raceId,
|
||||
importedCount: 10,
|
||||
errors: ['Error 1'],
|
||||
};
|
||||
|
||||
mockApiClient.importResults.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.importResults(raceId, input);
|
||||
|
||||
expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
|
||||
expect(result).toBeInstanceOf(ImportRaceResultsSummaryViewModel);
|
||||
expect(result.raceId).toBe(raceId);
|
||||
expect(result.importedCount).toBe(10);
|
||||
expect(result.errors).toEqual(['Error 1']);
|
||||
});
|
||||
|
||||
it('should handle successful import with no errors', async () => {
|
||||
const raceId = 'race-123';
|
||||
const input = { raceId, results: [] };
|
||||
|
||||
const mockDto = {
|
||||
raceId,
|
||||
importedCount: 5,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
mockApiClient.importResults.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.importResults(raceId, input);
|
||||
|
||||
expect(result.importedCount).toBe(5);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.importResults fails', async () => {
|
||||
const raceId = 'race-123';
|
||||
const input = { raceId, results: [] };
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.importResults.mockRejectedValue(error);
|
||||
|
||||
await expect(service.importResults(raceId, input)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,17 @@
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
|
||||
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
|
||||
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
type ImportRaceResultsInputDto = { raceId: string; results: Array<any> };
|
||||
// TODO: Move this type to apps/website/lib/types/generated when available
|
||||
type ImportRaceResultsInputDto = { raceId: string; results: Array<unknown> };
|
||||
|
||||
// Note: RaceWithSOFViewModel and ImportRaceResultsSummaryViewModel are defined in presenters
|
||||
// These will need to be converted to proper view models
|
||||
type RaceWithSOFViewModel = any; // TODO: Create proper view model
|
||||
type ImportRaceResultsSummaryViewModel = any; // TODO: Create proper view model
|
||||
// TODO: Move this type to apps/website/lib/types/generated when available
|
||||
type ImportRaceResultsSummaryDto = {
|
||||
raceId: string;
|
||||
importedCount: number;
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Race Results Service
|
||||
@@ -29,20 +33,18 @@ export class RaceResultsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get race with strength of field calculation
|
||||
* TODO: Create RaceWithSOFViewModel and use it here
|
||||
*/
|
||||
async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
|
||||
const dto = await this.apiClient.getWithSOF(raceId);
|
||||
return dto; // TODO: return new RaceWithSOFViewModel(dto);
|
||||
}
|
||||
* Get race with strength of field calculation
|
||||
*/
|
||||
async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
|
||||
const dto = await this.apiClient.getWithSOF(raceId);
|
||||
return new RaceWithSOFViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import race results and get summary
|
||||
* TODO: Create ImportRaceResultsSummaryViewModel and use it here
|
||||
*/
|
||||
async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
const dto = await this.apiClient.importResults(raceId, input);
|
||||
return dto; // TODO: return new ImportRaceResultsSummaryViewModel(dto);
|
||||
}
|
||||
* Import race results and get summary
|
||||
*/
|
||||
async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
const dto = await this.apiClient.importResults(raceId, input) as ImportRaceResultsSummaryDto;
|
||||
return new ImportRaceResultsSummaryViewModel(dto);
|
||||
}
|
||||
}
|
||||
134
apps/website/lib/services/races/RaceService.test.ts
Normal file
134
apps/website/lib/services/races/RaceService.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
|
||||
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
|
||||
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
|
||||
|
||||
describe('RaceService', () => {
|
||||
let mockApiClient: Mocked<RacesApiClient>;
|
||||
let service: RaceService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getDetail: vi.fn(),
|
||||
getPageData: vi.fn(),
|
||||
getTotal: vi.fn(),
|
||||
} as Mocked<RacesApiClient>;
|
||||
|
||||
service = new RaceService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getRaceDetail', () => {
|
||||
it('should call apiClient.getDetail and return RaceDetailViewModel', async () => {
|
||||
const raceId = 'race-123';
|
||||
const driverId = 'driver-456';
|
||||
|
||||
const mockDto = {
|
||||
race: { id: raceId, track: 'Test Track' },
|
||||
league: { id: 'league-1', name: 'Test League' },
|
||||
entryList: [],
|
||||
registration: { isRegistered: true, canRegister: false },
|
||||
userResult: null,
|
||||
};
|
||||
|
||||
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRaceDetail(raceId, driverId);
|
||||
|
||||
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
|
||||
expect(result).toBeInstanceOf(RaceDetailViewModel);
|
||||
expect(result.race?.id).toBe(raceId);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getDetail fails', async () => {
|
||||
const raceId = 'race-123';
|
||||
const driverId = 'driver-456';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getDetail.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRaceDetail(raceId, driverId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRacesPageData', () => {
|
||||
it('should call apiClient.getPageData and return RacesPageViewModel with transformed data', async () => {
|
||||
const mockDto = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari',
|
||||
scheduledAt: '2023-10-01T10:00:00Z',
|
||||
status: 'upcoming',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Silverstone',
|
||||
car: 'Mercedes',
|
||||
scheduledAt: '2023-09-15T10:00:00Z',
|
||||
status: 'completed',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesPageData();
|
||||
|
||||
expect(mockApiClient.getPageData).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(RacesPageViewModel);
|
||||
expect(result.upcomingRaces).toHaveLength(1);
|
||||
expect(result.completedRaces).toHaveLength(1);
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(result.upcomingRaces[0].title).toBe('Monza - Ferrari');
|
||||
expect(result.completedRaces[0].title).toBe('Silverstone - Mercedes');
|
||||
});
|
||||
|
||||
it('should handle empty races array', async () => {
|
||||
const mockDto = { races: [] };
|
||||
|
||||
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesPageData();
|
||||
|
||||
expect(result.upcomingRaces).toHaveLength(0);
|
||||
expect(result.completedRaces).toHaveLength(0);
|
||||
expect(result.totalCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getPageData fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getPageData.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRacesPageData()).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRacesTotal', () => {
|
||||
it('should call apiClient.getTotal and return RaceStatsViewModel', async () => {
|
||||
const mockDto = { totalRaces: 42 };
|
||||
|
||||
mockApiClient.getTotal.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.getRacesTotal();
|
||||
|
||||
expect(mockApiClient.getTotal).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(RaceStatsViewModel);
|
||||
expect(result.totalRaces).toBe(42);
|
||||
expect(result.formattedTotalRaces).toBe('42');
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getTotal fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getTotal.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,20 @@
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
|
||||
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
|
||||
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
type RacesPageDataDto = { races: Array<any> };
|
||||
type RaceStatsDto = { totalRaces: number };
|
||||
type RacesPageDataRaceDTO = {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
};
|
||||
type RacesPageDataDto = { races: RacesPageDataRaceDTO[] };
|
||||
type RaceStatsDTO = { totalRaces: number };
|
||||
|
||||
/**
|
||||
* Race Service
|
||||
@@ -28,18 +39,51 @@ export class RaceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get races page data
|
||||
* TODO: Add view model transformation when view model is available
|
||||
* Get races page data with view model transformation
|
||||
*/
|
||||
async getRacesPageData(): Promise<RacesPageDataDto> {
|
||||
return this.apiClient.getPageData();
|
||||
async getRacesPageData(): Promise<RacesPageViewModel> {
|
||||
const dto = await this.apiClient.getPageData();
|
||||
return new RacesPageViewModel(this.transformRacesPageData(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total races statistics
|
||||
* TODO: Add view model transformation when view model is available
|
||||
* Get total races statistics with view model transformation
|
||||
*/
|
||||
async getRacesTotal(): Promise<RaceStatsDto> {
|
||||
return this.apiClient.getTotal();
|
||||
async getRacesTotal(): Promise<RaceStatsViewModel> {
|
||||
const dto: RaceStatsDTO = await this.apiClient.getTotal();
|
||||
return new RaceStatsViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API races page data to view model format
|
||||
*/
|
||||
private transformRacesPageData(dto: RacesPageDataDto): {
|
||||
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
totalCount: number;
|
||||
} {
|
||||
const upcomingRaces = dto.races
|
||||
.filter(race => race.status !== 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
const completedRaces = dto.races
|
||||
.filter(race => race.status === 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
return {
|
||||
upcomingRaces,
|
||||
completedRaces,
|
||||
totalCount: dto.races.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user