415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import { LeaguesTestContext } from '../LeaguesTestContext';
|
|
import { League as RacingLeague } from '../../../../core/racing/domain/entities/League';
|
|
import { Race } from '../../../../core/racing/domain/entities/Race';
|
|
import { Driver } from '../../../../core/racing/domain/entities/Driver';
|
|
import { Protest } from '../../../../core/racing/domain/entities/Protest';
|
|
import { Penalty } from '../../../../core/racing/domain/entities/penalty/Penalty';
|
|
import { GetLeagueProtestsUseCase } from '../../../../core/racing/application/use-cases/GetLeagueProtestsUseCase';
|
|
import { GetRacePenaltiesUseCase } from '../../../../core/racing/application/use-cases/GetRacePenaltiesUseCase';
|
|
|
|
describe('League Stewarding - GetLeagueStewarding', () => {
|
|
let context: LeaguesTestContext;
|
|
let getLeagueProtestsUseCase: GetLeagueProtestsUseCase;
|
|
let getRacePenaltiesUseCase: GetRacePenaltiesUseCase;
|
|
|
|
beforeEach(() => {
|
|
context = new LeaguesTestContext();
|
|
context.clear();
|
|
|
|
getLeagueProtestsUseCase = new GetLeagueProtestsUseCase(
|
|
context.raceRepository,
|
|
context.protestRepository,
|
|
context.racingDriverRepository,
|
|
context.racingLeagueRepository,
|
|
);
|
|
|
|
getRacePenaltiesUseCase = new GetRacePenaltiesUseCase(
|
|
context.penaltyRepository,
|
|
context.racingDriverRepository,
|
|
);
|
|
});
|
|
|
|
const seedRacingLeague = async (params: { leagueId: string }) => {
|
|
const league = RacingLeague.create({
|
|
id: params.leagueId,
|
|
name: 'Racing League',
|
|
description: 'League used for stewarding integration tests',
|
|
ownerId: 'driver-123',
|
|
});
|
|
|
|
await context.racingLeagueRepository.create(league);
|
|
return league;
|
|
};
|
|
|
|
const seedRace = async (params: { raceId: string; leagueId: string }) => {
|
|
const race = Race.create({
|
|
id: params.raceId,
|
|
leagueId: params.leagueId,
|
|
track: 'Track 1',
|
|
car: 'GT3',
|
|
scheduledAt: new Date('2025-01-10T20:00:00Z'),
|
|
status: 'completed',
|
|
});
|
|
|
|
await context.raceRepository.create(race);
|
|
return race;
|
|
};
|
|
|
|
const seedDriver = async (params: { driverId: string; iracingId?: string }) => {
|
|
const driver = Driver.create({
|
|
id: params.driverId,
|
|
name: 'Driver Name',
|
|
iracingId: params.iracingId || `ir-${params.driverId}`,
|
|
country: 'US',
|
|
});
|
|
|
|
await context.racingDriverRepository.create(driver);
|
|
return driver;
|
|
};
|
|
|
|
const seedProtest = async (params: {
|
|
protestId: string;
|
|
raceId: string;
|
|
protestingDriverId: string;
|
|
accusedDriverId: string;
|
|
status?: string;
|
|
}) => {
|
|
const protest = Protest.create({
|
|
id: params.protestId,
|
|
raceId: params.raceId,
|
|
protestingDriverId: params.protestingDriverId,
|
|
accusedDriverId: params.accusedDriverId,
|
|
incident: {
|
|
lap: 5,
|
|
description: 'Contact on corner entry',
|
|
},
|
|
status: params.status || 'pending',
|
|
});
|
|
|
|
await context.protestRepository.create(protest);
|
|
return protest;
|
|
};
|
|
|
|
const seedPenalty = async (params: {
|
|
penaltyId: string;
|
|
leagueId: string;
|
|
driverId: string;
|
|
raceId?: string;
|
|
status?: string;
|
|
}) => {
|
|
const penalty = Penalty.create({
|
|
id: params.penaltyId,
|
|
leagueId: params.leagueId,
|
|
driverId: params.driverId,
|
|
type: 'time_penalty',
|
|
value: 5,
|
|
reason: 'Contact on corner entry',
|
|
issuedBy: 'steward-1',
|
|
status: params.status || 'pending',
|
|
...(params.raceId && { raceId: params.raceId }),
|
|
});
|
|
|
|
await context.penaltyRepository.create(penalty);
|
|
return penalty;
|
|
};
|
|
|
|
describe('Success Path', () => {
|
|
it('should retrieve league protests with driver and race details', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const protestingDriverId = 'driver-1';
|
|
const accusedDriverId = 'driver-2';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId: protestingDriverId });
|
|
await seedDriver({ driverId: accusedDriverId });
|
|
await seedProtest({
|
|
protestId: 'protest-1',
|
|
raceId,
|
|
protestingDriverId,
|
|
accusedDriverId,
|
|
status: 'pending',
|
|
});
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.league.id.toString()).toBe(leagueId);
|
|
expect(data.protests).toHaveLength(1);
|
|
expect(data.protests[0].protest.id.toString()).toBe('protest-1');
|
|
expect(data.protests[0].protest.status.toString()).toBe('pending');
|
|
expect(data.protests[0].race?.id.toString()).toBe(raceId);
|
|
expect(data.protests[0].protestingDriver?.id.toString()).toBe(protestingDriverId);
|
|
expect(data.protests[0].accusedDriver?.id.toString()).toBe(accusedDriverId);
|
|
});
|
|
|
|
it('should retrieve penalties with driver details', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const driverId = 'driver-1';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId });
|
|
await seedPenalty({
|
|
penaltyId: 'penalty-1',
|
|
leagueId,
|
|
driverId,
|
|
raceId,
|
|
status: 'pending',
|
|
});
|
|
|
|
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.penalties).toHaveLength(1);
|
|
expect(data.penalties[0].id.toString()).toBe('penalty-1');
|
|
expect(data.penalties[0].status.toString()).toBe('pending');
|
|
expect(data.drivers).toHaveLength(1);
|
|
expect(data.drivers[0].id.toString()).toBe(driverId);
|
|
});
|
|
|
|
it('should retrieve multiple protests for a league', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const protestingDriverId = 'driver-1';
|
|
const accusedDriverId = 'driver-2';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId: protestingDriverId });
|
|
await seedDriver({ driverId: accusedDriverId });
|
|
await seedProtest({
|
|
protestId: 'protest-1',
|
|
raceId,
|
|
protestingDriverId,
|
|
accusedDriverId,
|
|
status: 'pending',
|
|
});
|
|
await seedProtest({
|
|
protestId: 'protest-2',
|
|
raceId,
|
|
protestingDriverId: accusedDriverId,
|
|
accusedDriverId: protestingDriverId,
|
|
status: 'under_review',
|
|
});
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.protests).toHaveLength(2);
|
|
expect(data.protests.map(p => p.protest.id.toString()).sort()).toEqual(['protest-1', 'protest-2']);
|
|
});
|
|
|
|
it('should retrieve multiple penalties for a race', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const driverId1 = 'driver-1';
|
|
const driverId2 = 'driver-2';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId: driverId1 });
|
|
await seedDriver({ driverId: driverId2 });
|
|
await seedPenalty({
|
|
penaltyId: 'penalty-1',
|
|
leagueId,
|
|
driverId: driverId1,
|
|
raceId,
|
|
status: 'pending',
|
|
});
|
|
await seedPenalty({
|
|
penaltyId: 'penalty-2',
|
|
leagueId,
|
|
driverId: driverId2,
|
|
raceId,
|
|
status: 'applied',
|
|
});
|
|
|
|
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.penalties).toHaveLength(2);
|
|
expect(data.penalties.map(p => p.id.toString()).sort()).toEqual(['penalty-1', 'penalty-2']);
|
|
expect(data.drivers).toHaveLength(2);
|
|
});
|
|
|
|
it('should retrieve resolved protests', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const protestingDriverId = 'driver-1';
|
|
const accusedDriverId = 'driver-2';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId: protestingDriverId });
|
|
await seedDriver({ driverId: accusedDriverId });
|
|
await seedProtest({
|
|
protestId: 'protest-1',
|
|
raceId,
|
|
protestingDriverId,
|
|
accusedDriverId,
|
|
status: 'upheld',
|
|
});
|
|
await seedProtest({
|
|
protestId: 'protest-2',
|
|
raceId,
|
|
protestingDriverId,
|
|
accusedDriverId,
|
|
status: 'dismissed',
|
|
});
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.protests).toHaveLength(2);
|
|
expect(data.protests.filter(p => p.protest.status.toString() === 'upheld')).toHaveLength(1);
|
|
expect(data.protests.filter(p => p.protest.status.toString() === 'dismissed')).toHaveLength(1);
|
|
});
|
|
|
|
it('should retrieve applied penalties', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const driverId = 'driver-1';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
await seedDriver({ driverId });
|
|
await seedPenalty({
|
|
penaltyId: 'penalty-1',
|
|
leagueId,
|
|
driverId,
|
|
raceId,
|
|
status: 'applied',
|
|
});
|
|
|
|
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.penalties).toHaveLength(1);
|
|
expect(data.penalties[0].status.toString()).toBe('applied');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle league with no protests', async () => {
|
|
const leagueId = 'league-empty';
|
|
await seedRacingLeague({ leagueId });
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.protests).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle race with no penalties', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
|
|
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.penalties).toHaveLength(0);
|
|
expect(data.drivers).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle league with no races', async () => {
|
|
const leagueId = 'league-empty';
|
|
await seedRacingLeague({ leagueId });
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.protests).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle protest with missing driver details', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const protestingDriverId = 'driver-1';
|
|
const accusedDriverId = 'driver-2';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
// Don't seed drivers
|
|
await seedProtest({
|
|
protestId: 'protest-1',
|
|
raceId,
|
|
protestingDriverId,
|
|
accusedDriverId,
|
|
status: 'pending',
|
|
});
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.protests).toHaveLength(1);
|
|
expect(data.protests[0].protestingDriver).toBeNull();
|
|
expect(data.protests[0].accusedDriver).toBeNull();
|
|
});
|
|
|
|
it('should handle penalty with missing driver details', async () => {
|
|
const leagueId = 'league-1';
|
|
const raceId = 'race-1';
|
|
const driverId = 'driver-1';
|
|
|
|
await seedRacingLeague({ leagueId });
|
|
await seedRace({ raceId, leagueId });
|
|
// Don't seed driver
|
|
await seedPenalty({
|
|
penaltyId: 'penalty-1',
|
|
leagueId,
|
|
driverId,
|
|
raceId,
|
|
status: 'pending',
|
|
});
|
|
|
|
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const data = result.unwrap();
|
|
expect(data.penalties).toHaveLength(1);
|
|
expect(data.drivers).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should return LEAGUE_NOT_FOUND when league does not exist', async () => {
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId: 'missing-league' });
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.unwrapErr().code).toBe('LEAGUE_NOT_FOUND');
|
|
});
|
|
|
|
it('should handle repository errors gracefully', async () => {
|
|
const leagueId = 'league-1';
|
|
await seedRacingLeague({ leagueId });
|
|
|
|
// Mock repository error
|
|
context.raceRepository.findByLeagueId = async () => {
|
|
throw new Error('Database error');
|
|
};
|
|
|
|
const result = await getLeagueProtestsUseCase.execute({ leagueId });
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
|
});
|
|
});
|
|
});
|