website service tests
This commit is contained in:
5
apps/website/lib/apiClient.ts
Normal file
5
apps/website/lib/apiClient.ts
Normal 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);
|
||||||
@@ -8,7 +8,9 @@ import { Blocker } from './Blocker';
|
|||||||
export class ThrottleBlocker extends Blocker {
|
export class ThrottleBlocker extends Blocker {
|
||||||
private lastExecutionTime = 0;
|
private lastExecutionTime = 0;
|
||||||
|
|
||||||
constructor(private readonly delayMs: number) {}
|
constructor(private readonly delayMs: number) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
canExecute(): boolean {
|
canExecute(): boolean {
|
||||||
return Date.now() - this.lastExecutionTime >= this.delayMs;
|
return Date.now() - this.lastExecutionTime >= this.delayMs;
|
||||||
|
|||||||
60
apps/website/lib/services/dashboard/DashboardService.test.ts
Normal file
60
apps/website/lib/services/dashboard/DashboardService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
113
apps/website/lib/services/landing/LandingService.test.ts
Normal file
113
apps/website/lib/services/landing/LandingService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
132
apps/website/lib/services/leagues/LeagueSettingsService.test.ts
Normal file
132
apps/website/lib/services/leagues/LeagueSettingsService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
123
apps/website/lib/services/leagues/LeagueWalletService.test.ts
Normal file
123
apps/website/lib/services/leagues/LeagueWalletService.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
90
apps/website/lib/services/penalties/PenaltyService.test.ts
Normal file
90
apps/website/lib/services/penalties/PenaltyService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
205
apps/website/lib/services/protests/ProtestService.test.ts
Normal file
205
apps/website/lib/services/protests/ProtestService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
140
apps/website/lib/services/races/RaceStewardingService.test.ts
Normal file
140
apps/website/lib/services/races/RaceStewardingService.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"deploy:website:preview": "npx vercel deploy --cwd apps/website",
|
"deploy:website:preview": "npx vercel deploy --cwd apps/website",
|
||||||
"deploy:website:prod": "npx vercel deploy --prod",
|
"deploy:website:prod": "npx vercel deploy --prod",
|
||||||
"dev": "echo 'Development server placeholder - to be configured'",
|
"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": "docker-compose -f docker-compose.dev.yml up",
|
||||||
"docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build",
|
"docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build",
|
||||||
"docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v",
|
"docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './apps/website'),
|
||||||
'@core': path.resolve(__dirname, './core'),
|
'@core': path.resolve(__dirname, './core'),
|
||||||
'@adapters': path.resolve(__dirname, './adapters'),
|
'@adapters': path.resolve(__dirname, './adapters'),
|
||||||
'@testing': path.resolve(__dirname, './testing'),
|
'@testing': path.resolve(__dirname, './testing'),
|
||||||
|
|||||||
Reference in New Issue
Block a user