website service tests

This commit is contained in:
2025-12-23 23:36:55 +01:00
parent efcdbd17f2
commit 43a8afe7a9
13 changed files with 1107 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
import { ApiClient } from './api/index';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
export const apiClient = new ApiClient(API_BASE_URL);

View File

@@ -8,7 +8,9 @@ import { Blocker } from './Blocker';
export class ThrottleBlocker extends Blocker {
private lastExecutionTime = 0;
constructor(private readonly delayMs: number) {}
constructor(private readonly delayMs: number) {
super();
}
canExecute(): boolean {
return Date.now() - this.lastExecutionTime >= this.delayMs;

View File

@@ -0,0 +1,60 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { DashboardService } from './DashboardService';
import { DashboardApiClient } from '../../api/dashboard/DashboardApiClient';
import { DashboardOverviewViewModel } from '../../view-models/DashboardOverviewViewModel';
describe('DashboardService', () => {
let mockApiClient: Mocked<DashboardApiClient>;
let service: DashboardService;
beforeEach(() => {
mockApiClient = {
getDashboardOverview: vi.fn(),
} as Mocked<DashboardApiClient>;
service = new DashboardService(mockApiClient);
});
describe('getDashboardOverview', () => {
it('should call apiClient.getDashboardOverview and return DashboardOverviewViewModel', async () => {
const mockDto = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
country: 'US',
totalRaces: 42,
wins: 10,
podiums: 20,
rating: 1500,
globalRank: 5,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { feedItems: [] },
friends: [],
};
mockApiClient.getDashboardOverview.mockResolvedValue(mockDto);
const result = await service.getDashboardOverview();
expect(mockApiClient.getDashboardOverview).toHaveBeenCalled();
expect(result).toBeInstanceOf(DashboardOverviewViewModel);
expect(result.activeLeaguesCount).toBe(3);
});
it('should throw error when apiClient.getDashboardOverview fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDashboardOverview.mockRejectedValue(error);
await expect(service.getDashboardOverview()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,113 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LandingService } from './LandingService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { HomeDiscoveryViewModel } from '@/lib/view-models/HomeDiscoveryViewModel';
describe('LandingService', () => {
let mockRacesApi: Mocked<RacesApiClient>;
let mockLeaguesApi: Mocked<LeaguesApiClient>;
let mockTeamsApi: Mocked<TeamsApiClient>;
let service: LandingService;
beforeEach(() => {
mockRacesApi = {
getPageData: vi.fn(),
} as unknown as Mocked<RacesApiClient>;
mockLeaguesApi = {
getAllWithCapacity: vi.fn(),
} as unknown as Mocked<LeaguesApiClient>;
mockTeamsApi = {
getAll: vi.fn(),
} as unknown as Mocked<TeamsApiClient>;
service = new LandingService(mockRacesApi, mockLeaguesApi, mockTeamsApi);
});
describe('getHomeDiscovery', () => {
it('should return home discovery data with top leagues, teams, and upcoming races', async () => {
const racesDto = {
races: [
{
id: 'race-1',
track: 'Monza',
car: 'Ferrari',
scheduledAt: '2023-10-01T10:00:00Z',
status: 'upcoming',
leagueId: 'league-1',
leagueName: 'Test League',
strengthOfField: 1500,
isUpcoming: true,
isLive: false,
isPast: false,
},
{
id: 'race-2',
track: 'Silverstone',
car: 'Mercedes',
scheduledAt: '2023-10-02T10:00:00Z',
status: 'upcoming',
leagueId: 'league-1',
leagueName: 'Test League',
strengthOfField: 1600,
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const leaguesDto = {
leagues: [
{ id: 'league-1', name: 'League One' },
{ id: 'league-2', name: 'League Two' },
{ id: 'league-3', name: 'League Three' },
{ id: 'league-4', name: 'League Four' },
{ id: 'league-5', name: 'League Five' },
],
};
const teamsDto = {
teams: [
{ id: 'team-1', name: 'Team One', tag: 'T1', description: 'Best team' },
{ id: 'team-2', name: 'Team Two', tag: 'T2', description: 'Great team' },
{ id: 'team-3', name: 'Team Three', tag: 'T3', description: 'Awesome team' },
{ id: 'team-4', name: 'Team Four', tag: 'T4', description: 'Cool team' },
{ id: 'team-5', name: 'Team Five', tag: 'T5', description: 'Pro team' },
],
};
mockRacesApi.getPageData.mockResolvedValue(racesDto as any);
mockLeaguesApi.getAllWithCapacity.mockResolvedValue(leaguesDto as any);
mockTeamsApi.getAll.mockResolvedValue(teamsDto as any);
const result = await service.getHomeDiscovery();
expect(result).toBeInstanceOf(HomeDiscoveryViewModel);
expect(result.topLeagues).toHaveLength(4); // First 4 leagues
expect(result.teams).toHaveLength(4); // First 4 teams
expect(result.upcomingRaces).toHaveLength(2); // All upcoming races (first 4)
});
it('should handle empty data', async () => {
mockRacesApi.getPageData.mockResolvedValue({ races: [] } as any);
mockLeaguesApi.getAllWithCapacity.mockResolvedValue({ leagues: [] } as any);
mockTeamsApi.getAll.mockResolvedValue({ teams: [] } as any);
const result = await service.getHomeDiscovery();
expect(result.topLeagues).toHaveLength(0);
expect(result.teams).toHaveLength(0);
expect(result.upcomingRaces).toHaveLength(0);
});
it('should throw error when any API call fails', async () => {
mockRacesApi.getPageData.mockRejectedValue(new Error('Races API failed'));
await expect(service.getHomeDiscovery()).rejects.toThrow('Races API failed');
});
});
});

View File

@@ -0,0 +1,132 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueSettingsService } from './LeagueSettingsService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { DriversApiClient } from '../../api/drivers/DriversApiClient';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
describe('LeagueSettingsService', () => {
let mockLeaguesApiClient: Mocked<LeaguesApiClient>;
let mockDriversApiClient: Mocked<DriversApiClient>;
let service: LeagueSettingsService;
beforeEach(() => {
mockLeaguesApiClient = {
getAllWithCapacity: vi.fn(),
getLeagueConfig: vi.fn(),
getScoringPresets: vi.fn(),
getMemberships: vi.fn(),
transferOwnership: vi.fn(),
} as unknown as Mocked<LeaguesApiClient>;
mockDriversApiClient = {
getLeaderboard: vi.fn(),
getDriver: vi.fn(),
} as unknown as Mocked<DriversApiClient>;
service = new LeagueSettingsService(mockLeaguesApiClient, mockDriversApiClient);
});
describe('getLeagueSettings', () => {
it('should return league settings with all data', async () => {
const leagueId = 'league-123';
// Mock getAllWithCapacity
mockLeaguesApiClient.getAllWithCapacity.mockResolvedValue({
leagues: [
{ id: leagueId, name: 'Test League', ownerId: 'owner-123', capacity: 20, currentMembers: 10 },
],
});
// Mock getLeagueConfig
mockLeaguesApiClient.getLeagueConfig.mockResolvedValue({
config: { someConfig: 'value' },
});
// Mock getScoringPresets
mockLeaguesApiClient.getScoringPresets.mockResolvedValue({
presets: [{ id: 'preset-1', name: 'Preset 1' }],
});
// Mock getLeaderboard
mockDriversApiClient.getLeaderboard.mockResolvedValue({
drivers: [
{ id: 'owner-123', name: 'Owner Driver', rating: 1500, rank: 10 },
{ id: 'member-1', name: 'Member 1', rating: 1400, rank: 20 },
],
});
// Mock getDriver for owner
mockDriversApiClient.getDriver.mockResolvedValue({
id: 'owner-123',
name: 'Owner Driver',
avatarUrl: 'https://example.com/avatar.jpg',
country: 'US',
});
// Mock getMemberships
mockLeaguesApiClient.getMemberships.mockResolvedValue({
members: [
{ driverId: 'member-1', role: 'member' },
],
});
const result = await service.getLeagueSettings(leagueId);
expect(result).toBeInstanceOf(LeagueSettingsViewModel);
expect(result.league.id).toBe(leagueId);
expect(result.league.name).toBe('Test League');
expect(result.league.ownerId).toBe('owner-123');
expect(result.presets).toHaveLength(1);
expect(result.owner).toBeDefined();
expect(result.members).toHaveLength(1);
});
it('should return null when league not found', async () => {
const leagueId = 'non-existent-league';
mockLeaguesApiClient.getAllWithCapacity.mockResolvedValue({
leagues: [],
});
const result = await service.getLeagueSettings(leagueId);
expect(result).toBeNull();
});
it('should handle errors gracefully', async () => {
const leagueId = 'league-123';
mockLeaguesApiClient.getAllWithCapacity.mockRejectedValue(new Error('API error'));
const result = await service.getLeagueSettings(leagueId);
expect(result).toBeNull();
});
});
describe('transferOwnership', () => {
it('should call apiClient.transferOwnership and return success', async () => {
const leagueId = 'league-123';
const currentOwnerId = 'owner-123';
const newOwnerId = 'new-owner-456';
mockLeaguesApiClient.transferOwnership.mockResolvedValue({ success: true });
const result = await service.transferOwnership(leagueId, currentOwnerId, newOwnerId);
expect(mockLeaguesApiClient.transferOwnership).toHaveBeenCalledWith(leagueId, currentOwnerId, newOwnerId);
expect(result).toBe(true);
});
it('should throw error when apiClient.transferOwnership fails', async () => {
const leagueId = 'league-123';
const currentOwnerId = 'owner-123';
const newOwnerId = 'new-owner-456';
const error = new Error('API call failed');
mockLeaguesApiClient.transferOwnership.mockRejectedValue(error);
await expect(service.transferOwnership(leagueId, currentOwnerId, newOwnerId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,147 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueStewardingService } from './LeagueStewardingService';
import { RaceService } from '../races/RaceService';
import { ProtestService } from '../protests/ProtestService';
import { PenaltyService } from '../penalties/PenaltyService';
import { DriverService } from '../drivers/DriverService';
import { LeagueMembershipService } from './LeagueMembershipService';
import { LeagueStewardingViewModel } from '../../view-models/LeagueStewardingViewModel';
describe('LeagueStewardingService', () => {
let mockRaceService: Mocked<RaceService>;
let mockProtestService: Mocked<ProtestService>;
let mockPenaltyService: Mocked<PenaltyService>;
let mockDriverService: Mocked<DriverService>;
let mockLeagueMembershipService: Mocked<LeagueMembershipService>;
let service: LeagueStewardingService;
beforeEach(() => {
mockRaceService = {
findByLeagueId: vi.fn(),
} as Mocked<RaceService>;
mockProtestService = {
findByRaceId: vi.fn(),
} as Mocked<ProtestService>;
mockPenaltyService = {
findByRaceId: vi.fn(),
} as Mocked<PenaltyService>;
mockDriverService = {
findByIds: vi.fn(),
} as Mocked<DriverService>;
mockLeagueMembershipService = {} as Mocked<LeagueMembershipService>;
service = new LeagueStewardingService(
mockRaceService,
mockProtestService,
mockPenaltyService,
mockDriverService,
mockLeagueMembershipService
);
});
describe('getLeagueStewardingData', () => {
it('should return stewarding data with all races and their protests/penalties', async () => {
const leagueId = 'league-123';
// Mock races
mockRaceService.findByLeagueId.mockResolvedValue([
{ id: 'race-1', track: 'Monza', scheduledAt: '2023-10-01T10:00:00Z' },
{ id: 'race-2', track: 'Silverstone', scheduledAt: '2023-09-15T10:00:00Z' },
]);
// Mock protests for race-1
mockProtestService.findByRaceId.mockImplementation(async (raceId) => {
if (raceId === 'race-1') {
return [
{ id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', status: 'pending' },
{ id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', status: 'upheld' },
];
}
return [];
});
// Mock penalties for race-1
mockPenaltyService.findByRaceId.mockImplementation(async (raceId) => {
if (raceId === 'race-1') {
return [
{ id: 'penalty-1', driverId: 'driver-2', type: 'time', value: 5, reason: 'Incident' },
];
}
return [];
});
// Mock driver service
mockDriverService.findByIds.mockResolvedValue([
{ id: 'driver-1', name: 'Driver 1' },
{ id: 'driver-2', name: 'Driver 2' },
{ id: 'driver-3', name: 'Driver 3' },
{ id: 'driver-4', name: 'Driver 4' },
]);
const result = await service.getLeagueStewardingData(leagueId);
expect(result).toBeInstanceOf(LeagueStewardingViewModel);
expect(result.racesWithData).toHaveLength(2);
expect(result.totalPending).toBe(1);
expect(result.totalResolved).toBe(1);
expect(result.totalPenalties).toBe(1);
expect(result.pendingRaces).toHaveLength(1);
expect(result.historyRaces).toHaveLength(1);
});
it('should handle empty races array', async () => {
const leagueId = 'league-123';
mockRaceService.findByLeagueId.mockResolvedValue([]);
mockProtestService.findByRaceId.mockResolvedValue([]);
mockPenaltyService.findByRaceId.mockResolvedValue([]);
mockDriverService.findByIds.mockResolvedValue([]);
const result = await service.getLeagueStewardingData(leagueId);
expect(result.racesWithData).toHaveLength(0);
expect(result.totalPending).toBe(0);
expect(result.totalResolved).toBe(0);
expect(result.totalPenalties).toBe(0);
});
});
describe('reviewProtest', () => {
it('should call protestService.reviewProtest', async () => {
const input = {
protestId: 'protest-123',
stewardId: 'steward-456',
decision: 'upheld',
decisionNotes: 'Test notes',
};
mockProtestService.reviewProtest = vi.fn().mockResolvedValue(undefined);
await service.reviewProtest(input);
expect(mockProtestService.reviewProtest).toHaveBeenCalledWith(input);
});
});
describe('applyPenalty', () => {
it('should call penaltyService.applyPenalty', async () => {
const input = {
driverId: 'driver-123',
raceId: 'race-456',
type: 'time',
value: 10,
reason: 'Test reason',
};
mockPenaltyService.applyPenalty = vi.fn().mockResolvedValue(undefined);
await service.applyPenalty(input);
expect(mockPenaltyService.applyPenalty).toHaveBeenCalledWith(input);
});
});
});

View File

@@ -0,0 +1,123 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueWalletService } from './LeagueWalletService';
import { WalletsApiClient } from '../../api/wallets/WalletsApiClient';
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
describe('LeagueWalletService', () => {
let mockApiClient: Mocked<WalletsApiClient>;
let service: LeagueWalletService;
beforeEach(() => {
mockApiClient = {
getLeagueWallet: vi.fn(),
withdrawFromLeagueWallet: vi.fn(),
} as unknown as Mocked<WalletsApiClient>;
service = new LeagueWalletService(mockApiClient);
});
describe('getWalletForLeague', () => {
it('should call apiClient.getLeagueWallet and return LeagueWalletViewModel', async () => {
const leagueId = 'league-123';
const mockDto = {
balance: 1000,
currency: 'USD',
totalRevenue: 5000,
totalFees: 1000,
totalWithdrawals: 2000,
pendingPayouts: 500,
canWithdraw: true,
transactions: [
{
id: 'txn-1',
type: 'sponsorship' as const,
description: 'Sponsorship payment',
amount: 100,
fee: 10,
netAmount: 90,
date: '2023-10-01T10:00:00Z',
status: 'completed' as const,
reference: 'ref-1',
},
],
};
mockApiClient.getLeagueWallet.mockResolvedValue(mockDto);
const result = await service.getWalletForLeague(leagueId);
expect(mockApiClient.getLeagueWallet).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueWalletViewModel);
expect(result.balance).toBe(1000);
expect(result.currency).toBe('USD');
expect(result.transactions).toHaveLength(1);
expect(result.formattedBalance).toBe('$1000.00');
});
it('should throw error when apiClient.getLeagueWallet fails', async () => {
const leagueId = 'league-123';
const error = new Error('API call failed');
mockApiClient.getLeagueWallet.mockRejectedValue(error);
await expect(service.getWalletForLeague(leagueId)).rejects.toThrow('API call failed');
});
});
describe('withdraw', () => {
it('should call apiClient.withdrawFromLeagueWallet with correct parameters', async () => {
const leagueId = 'league-123';
const amount = 500;
const currency = 'USD';
const seasonId = 'season-456';
const destinationAccount = 'account-789';
const mockResponse = { success: true, message: 'Withdrawal successful' };
mockApiClient.withdrawFromLeagueWallet.mockResolvedValue(mockResponse);
const result = await service.withdraw(leagueId, amount, currency, seasonId, destinationAccount);
expect(mockApiClient.withdrawFromLeagueWallet).toHaveBeenCalledWith(leagueId, {
amount,
currency,
seasonId,
destinationAccount,
});
expect(result).toEqual(mockResponse);
});
it('should throw error when apiClient.withdrawFromLeagueWallet fails', async () => {
const leagueId = 'league-123';
const amount = 500;
const currency = 'USD';
const seasonId = 'season-456';
const destinationAccount = 'account-789';
const error = new Error('Withdrawal failed');
mockApiClient.withdrawFromLeagueWallet.mockRejectedValue(error);
await expect(service.withdraw(leagueId, amount, currency, seasonId, destinationAccount)).rejects.toThrow('Withdrawal failed');
});
it('should block multiple rapid calls due to throttle', async () => {
const leagueId = 'league-123';
const amount = 500;
const currency = 'USD';
const seasonId = 'season-456';
const destinationAccount = 'account-789';
const mockResponse = { success: true };
mockApiClient.withdrawFromLeagueWallet.mockResolvedValue(mockResponse);
// First call should succeed
await service.withdraw(leagueId, amount, currency, seasonId, destinationAccount);
// Reset mock
mockApiClient.withdrawFromLeagueWallet.mockClear();
// Immediate second call should be blocked by throttle and throw error
await expect(service.withdraw(leagueId, amount, currency, seasonId, destinationAccount)).rejects.toThrow('Request blocked due to rate limiting');
expect(mockApiClient.withdrawFromLeagueWallet).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueWizardService } from './LeagueWizardService';
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
import { apiClient } from '@/lib/apiClient';
// Mock the apiClient
vi.mock('@/lib/apiClient', () => ({
apiClient: {
leagues: {
create: vi.fn(),
},
},
}));
describe('LeagueWizardService', () => {
describe('createLeague', () => {
it('should call apiClient.leagues.create with correct command', async () => {
const form = {
name: 'Test League',
description: 'A test league',
toCreateLeagueCommand: vi.fn().mockReturnValue({
name: 'Test League',
description: 'A test league',
ownerId: 'owner-123',
}),
} as unknown as LeagueWizardCommandModel;
const ownerId = 'owner-123';
const mockOutput = { leagueId: 'new-league-id', success: true };
(apiClient.leagues.create as any).mockResolvedValue(mockOutput);
const result = await LeagueWizardService.createLeague(form, ownerId);
expect(form.toCreateLeagueCommand).toHaveBeenCalledWith(ownerId);
expect(apiClient.leagues.create).toHaveBeenCalledWith({
name: 'Test League',
description: 'A test league',
ownerId: 'owner-123',
});
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.leagues.create fails', async () => {
const form = {
name: 'Test League',
description: 'A test league',
toCreateLeagueCommand: vi.fn().mockReturnValue({
name: 'Test League',
description: 'A test league',
ownerId: 'owner-123',
}),
} as unknown as LeagueWizardCommandModel;
const ownerId = 'owner-123';
const error = new Error('API call failed');
(apiClient.leagues.create as Mocked<typeof apiClient.leagues.create>).mockRejectedValue(error);
await expect(LeagueWizardService.createLeague(form, ownerId)).rejects.toThrow('API call failed');
});
});
describe('createLeagueFromConfig', () => {
it('should call createLeague with same parameters', async () => {
const form = {
name: 'Test League',
description: 'A test league',
toCreateLeagueCommand: vi.fn().mockReturnValue({
name: 'Test League',
description: 'A test league',
ownerId: 'owner-123',
}),
} as unknown as LeagueWizardCommandModel;
const ownerId = 'owner-123';
const mockOutput = { leagueId: 'new-league-id', success: true };
(apiClient.leagues.create as Mocked<typeof apiClient.leagues.create>).mockResolvedValue(mockOutput);
const result = await LeagueWizardService.createLeagueFromConfig(form, ownerId);
expect(apiClient.leagues.create).toHaveBeenCalled();
expect(result).toEqual(mockOutput);
});
});
});

View File

@@ -0,0 +1,90 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { PenaltyService } from './PenaltyService';
import { PenaltiesApiClient } from '../../api/penalties/PenaltiesApiClient';
describe('PenaltyService', () => {
let mockApiClient: Mocked<PenaltiesApiClient>;
let service: PenaltyService;
beforeEach(() => {
mockApiClient = {
getRacePenalties: vi.fn(),
applyPenalty: vi.fn(),
} as Mocked<PenaltiesApiClient>;
service = new PenaltyService(mockApiClient);
});
describe('findByRaceId', () => {
it('should call apiClient.getRacePenalties and return penalties array', async () => {
const raceId = 'race-123';
const mockDto = {
penalties: [
{ id: 'penalty-1', driverId: 'driver-1', type: 'time', value: 5, reason: 'Incident' },
{ id: 'penalty-2', driverId: 'driver-2', type: 'grid', value: 3, reason: 'Qualifying incident' },
],
};
mockApiClient.getRacePenalties.mockResolvedValue(mockDto);
const result = await service.findByRaceId(raceId);
expect(mockApiClient.getRacePenalties).toHaveBeenCalledWith(raceId);
expect(result).toEqual(mockDto.penalties);
expect(result).toHaveLength(2);
});
it('should handle empty penalties array', async () => {
const raceId = 'race-123';
const mockDto = { penalties: [] };
mockApiClient.getRacePenalties.mockResolvedValue(mockDto);
const result = await service.findByRaceId(raceId);
expect(result).toEqual([]);
expect(result).toHaveLength(0);
});
it('should throw error when apiClient.getRacePenalties fails', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.getRacePenalties.mockRejectedValue(error);
await expect(service.findByRaceId(raceId)).rejects.toThrow('API call failed');
});
});
describe('applyPenalty', () => {
it('should call apiClient.applyPenalty with input', async () => {
const input = {
driverId: 'driver-123',
raceId: 'race-456',
type: 'time',
value: 10,
reason: 'Test reason',
};
mockApiClient.applyPenalty.mockResolvedValue(undefined);
await service.applyPenalty(input);
expect(mockApiClient.applyPenalty).toHaveBeenCalledWith(input);
});
it('should throw error when apiClient.applyPenalty fails', async () => {
const input = {
driverId: 'driver-123',
raceId: 'race-456',
type: 'time',
value: 10,
reason: 'Test reason',
};
const error = new Error('API call failed');
mockApiClient.applyPenalty.mockRejectedValue(error);
await expect(service.applyPenalty(input)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,205 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { ProtestService } from './ProtestService';
import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
import { ProtestViewModel } from '../../view-models/ProtestViewModel';
import { RaceViewModel } from '../../view-models/RaceViewModel';
import { ProtestDriverViewModel } from '../../view-models/ProtestDriverViewModel';
describe('ProtestService', () => {
let mockApiClient: Mocked<ProtestsApiClient>;
let service: ProtestService;
beforeEach(() => {
mockApiClient = {
getLeagueProtests: vi.fn(),
getLeagueProtest: vi.fn(),
applyPenalty: vi.fn(),
requestDefense: vi.fn(),
reviewProtest: vi.fn(),
getRaceProtests: vi.fn(),
} as Mocked<ProtestsApiClient>;
service = new ProtestService(mockApiClient);
});
describe('getLeagueProtests', () => {
it('should call apiClient.getLeagueProtests and return transformed data', async () => {
const leagueId = 'league-123';
const mockDto = {
protests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: { lap: 5, description: 'Contact at turn 1' },
filedAt: '2023-10-01T10:00:00Z',
status: 'pending',
},
],
racesById: { 'race-1': { id: 'race-1', track: 'Monza' } },
driversById: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
};
mockApiClient.getLeagueProtests.mockResolvedValue(mockDto);
const result = await service.getLeagueProtests(leagueId);
expect(mockApiClient.getLeagueProtests).toHaveBeenCalledWith(leagueId);
expect(result.protests).toHaveLength(1);
expect(result.protests[0]).toBeInstanceOf(ProtestViewModel);
expect(result.racesById).toEqual(mockDto.racesById);
expect(result.driversById).toEqual(mockDto.driversById);
});
it('should throw error when apiClient.getLeagueProtests fails', async () => {
const leagueId = 'league-123';
const error = new Error('API call failed');
mockApiClient.getLeagueProtests.mockRejectedValue(error);
await expect(service.getLeagueProtests(leagueId)).rejects.toThrow('API call failed');
});
});
describe('getProtestById', () => {
it('should call apiClient.getLeagueProtest and return transformed data', async () => {
const leagueId = 'league-123';
const protestId = 'protest-1';
const mockDto = {
protests: [
{
id: protestId,
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: { lap: 5, description: 'Contact at turn 1' },
filedAt: '2023-10-01T10:00:00Z',
status: 'pending',
},
],
racesById: { 'race-1': { id: 'race-1', track: 'Monza', scheduledAt: '2023-10-01T10:00:00Z' } },
driversById: {
'driver-1': { id: 'driver-1', name: 'Driver 1', avatarUrl: 'https://example.com/avatar1.jpg' },
'driver-2': { id: 'driver-2', name: 'Driver 2', avatarUrl: 'https://example.com/avatar2.jpg' },
},
};
mockApiClient.getLeagueProtest.mockResolvedValue(mockDto);
const result = await service.getProtestById(leagueId, protestId);
expect(mockApiClient.getLeagueProtest).toHaveBeenCalledWith(leagueId, protestId);
expect(result).not.toBeNull();
expect(result?.protest).toBeInstanceOf(ProtestViewModel);
expect(result?.race).toBeInstanceOf(RaceViewModel);
expect(result?.protestingDriver).toBeInstanceOf(ProtestDriverViewModel);
expect(result?.accusedDriver).toBeInstanceOf(ProtestDriverViewModel);
});
it('should return null when protest not found', async () => {
const leagueId = 'league-123';
const protestId = 'protest-999';
const mockDto = {
protests: [],
racesById: {},
driversById: {},
};
mockApiClient.getLeagueProtest.mockResolvedValue(mockDto);
const result = await service.getProtestById(leagueId, protestId);
expect(result).toBeNull();
});
it('should throw error when apiClient.getLeagueProtest fails', async () => {
const leagueId = 'league-123';
const protestId = 'protest-1';
const error = new Error('API call failed');
mockApiClient.getLeagueProtest.mockRejectedValue(error);
await expect(service.getProtestById(leagueId, protestId)).rejects.toThrow('API call failed');
});
});
describe('applyPenalty', () => {
it('should call apiClient.applyPenalty', async () => {
const input = {
protestId: 'protest-123',
driverId: 'driver-456',
type: 'time',
value: 10,
reason: 'Test reason',
};
mockApiClient.applyPenalty.mockResolvedValue(undefined);
await service.applyPenalty(input);
expect(mockApiClient.applyPenalty).toHaveBeenCalledWith(input);
});
});
describe('requestDefense', () => {
it('should call apiClient.requestDefense', async () => {
const input = {
protestId: 'protest-123',
driverId: 'driver-456',
message: 'Please provide defense',
};
mockApiClient.requestDefense.mockResolvedValue(undefined);
await service.requestDefense(input);
expect(mockApiClient.requestDefense).toHaveBeenCalledWith(input);
});
});
describe('reviewProtest', () => {
it('should call apiClient.reviewProtest', async () => {
const input = {
protestId: 'protest-123',
stewardId: 'steward-456',
decision: 'upheld',
decisionNotes: 'Test notes',
};
mockApiClient.reviewProtest.mockResolvedValue(undefined);
await service.reviewProtest(input);
expect(mockApiClient.reviewProtest).toHaveBeenCalledWith(input);
});
});
describe('findByRaceId', () => {
it('should call apiClient.getRaceProtests and return protests array', async () => {
const raceId = 'race-123';
const mockDto = {
protests: [
{ id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', status: 'pending' },
{ id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', status: 'resolved' },
],
driverMap: {},
};
mockApiClient.getRaceProtests.mockResolvedValue(mockDto);
const result = await service.findByRaceId(raceId);
expect(mockApiClient.getRaceProtests).toHaveBeenCalledWith(raceId);
expect(result).toEqual(mockDto.protests);
expect(result).toHaveLength(2);
});
it('should throw error when apiClient.getRaceProtests fails', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.getRaceProtests.mockRejectedValue(error);
await expect(service.findByRaceId(raceId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,140 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceStewardingService } from './RaceStewardingService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
import { PenaltiesApiClient } from '../../api/penalties/PenaltiesApiClient';
import { RaceStewardingViewModel } from '../../view-models/RaceStewardingViewModel';
describe('RaceStewardingService', () => {
let mockRacesApiClient: Mocked<RacesApiClient>;
let mockProtestsApiClient: Mocked<ProtestsApiClient>;
let mockPenaltiesApiClient: Mocked<PenaltiesApiClient>;
let service: RaceStewardingService;
beforeEach(() => {
mockRacesApiClient = {
getDetail: vi.fn(),
} as Mocked<RacesApiClient>;
mockProtestsApiClient = {
getRaceProtests: vi.fn(),
} as Mocked<ProtestsApiClient>;
mockPenaltiesApiClient = {
getRacePenalties: vi.fn(),
} as Mocked<PenaltiesApiClient>;
service = new RaceStewardingService(mockRacesApiClient, mockProtestsApiClient, mockPenaltiesApiClient);
});
describe('getRaceStewardingData', () => {
it('should return race stewarding data with all protests and penalties', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const raceDetailDto = {
race: {
id: raceId,
track: 'Monza',
scheduledAt: '2023-10-01T10:00:00Z',
status: 'completed',
},
league: {
id: 'league-1',
name: 'Test League',
},
};
const protestsDto = {
protests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: { lap: 5, description: 'Contact at turn 1' },
filedAt: '2023-10-01T10:00:00Z',
status: 'pending',
},
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: { lap: 10, description: 'Off-track' },
filedAt: '2023-10-01T10:05:00Z',
status: 'upheld',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
},
};
const penaltiesDto = {
penalties: [
{ id: 'penalty-1', driverId: 'driver-2', type: 'time', value: 5, reason: 'Incident' },
{ id: 'penalty-2', driverId: 'driver-4', type: 'grid', value: 3, reason: 'Qualifying incident' },
],
driverMap: {
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
},
};
mockRacesApiClient.getDetail.mockResolvedValue(raceDetailDto as any);
mockProtestsApiClient.getRaceProtests.mockResolvedValue(protestsDto as any);
mockPenaltiesApiClient.getRacePenalties.mockResolvedValue(penaltiesDto as any);
const result = await service.getRaceStewardingData(raceId, driverId);
expect(mockRacesApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
expect(mockProtestsApiClient.getRaceProtests).toHaveBeenCalledWith(raceId);
expect(mockPenaltiesApiClient.getRacePenalties).toHaveBeenCalledWith(raceId);
expect(result).toBeInstanceOf(RaceStewardingViewModel);
expect(result.race).toEqual(raceDetailDto.race);
expect(result.league).toEqual(raceDetailDto.league);
expect(result.protests).toHaveLength(2);
expect(result.penalties).toHaveLength(2);
expect(result.pendingProtests).toHaveLength(1);
expect(result.resolvedProtests).toHaveLength(1);
expect(result.pendingCount).toBe(1);
expect(result.resolvedCount).toBe(1);
expect(result.penaltiesCount).toBe(2);
});
it('should handle empty protests and penalties', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const raceDetailDto = {
race: { id: raceId, track: 'Monza', scheduledAt: '2023-10-01T10:00:00Z', status: 'completed' },
league: { id: 'league-1', name: 'Test League' },
};
mockRacesApiClient.getDetail.mockResolvedValue(raceDetailDto as any);
mockProtestsApiClient.getRaceProtests.mockResolvedValue({ protests: [], driverMap: {} } as any);
mockPenaltiesApiClient.getRacePenalties.mockResolvedValue({ penalties: [], driverMap: {} } as any);
const result = await service.getRaceStewardingData(raceId, driverId);
expect(result.protests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should throw error when any API call fails', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const error = new Error('API call failed');
mockRacesApiClient.getDetail.mockRejectedValue(error);
await expect(service.getRaceStewardingData(raceId, driverId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -70,6 +70,7 @@
"deploy:website:preview": "npx vercel deploy --cwd apps/website",
"deploy:website:prod": "npx vercel deploy --prod",
"dev": "echo 'Development server placeholder - to be configured'",
"lint": "npx eslint apps/api/src --ext .ts,.tsx --max-warnings 0",
"docker:dev": "docker-compose -f docker-compose.dev.yml up",
"docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build",
"docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v",

View File

@@ -20,6 +20,7 @@ export default defineConfig({
},
resolve: {
alias: {
'@': path.resolve(__dirname, './apps/website'),
'@core': path.resolve(__dirname, './core'),
'@adapters': path.resolve(__dirname, './adapters'),
'@testing': path.resolve(__dirname, './testing'),