integration tests

This commit is contained in:
2026-01-23 14:51:33 +01:00
parent 34eae53184
commit 95276df5af
18 changed files with 2875 additions and 3692 deletions

View File

@@ -0,0 +1,414 @@
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');
});
});
});

View File

@@ -0,0 +1,767 @@
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 { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership';
import { ReviewProtestUseCase } from '../../../../core/racing/application/use-cases/ReviewProtestUseCase';
import { ApplyPenaltyUseCase } from '../../../../core/racing/application/use-cases/ApplyPenaltyUseCase';
import { QuickPenaltyUseCase } from '../../../../core/racing/application/use-cases/QuickPenaltyUseCase';
import { FileProtestUseCase } from '../../../../core/racing/application/use-cases/FileProtestUseCase';
import { RequestProtestDefenseUseCase } from '../../../../core/racing/application/use-cases/RequestProtestDefenseUseCase';
import { SubmitProtestDefenseUseCase } from '../../../../core/racing/application/use-cases/SubmitProtestDefenseUseCase';
describe('League Stewarding - StewardingManagement', () => {
let context: LeaguesTestContext;
let reviewProtestUseCase: ReviewProtestUseCase;
let applyPenaltyUseCase: ApplyPenaltyUseCase;
let quickPenaltyUseCase: QuickPenaltyUseCase;
let fileProtestUseCase: FileProtestUseCase;
let requestProtestDefenseUseCase: RequestProtestDefenseUseCase;
let submitProtestDefenseUseCase: SubmitProtestDefenseUseCase;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
reviewProtestUseCase = new ReviewProtestUseCase(
context.protestRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.logger,
);
applyPenaltyUseCase = new ApplyPenaltyUseCase(
context.penaltyRepository,
context.protestRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.logger,
);
quickPenaltyUseCase = new QuickPenaltyUseCase(
context.penaltyRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.logger,
);
fileProtestUseCase = new FileProtestUseCase(
context.protestRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.racingDriverRepository,
);
requestProtestDefenseUseCase = new RequestProtestDefenseUseCase(
context.protestRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.logger,
);
submitProtestDefenseUseCase = new SubmitProtestDefenseUseCase(
context.racingLeagueRepository,
context.protestRepository,
context.logger,
);
});
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('Review Protest', () => {
it('should review a pending protest and mark it as under review', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
const accusedDriverId = 'driver-2';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
await seedDriver({ driverId: accusedDriverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'pending',
});
const result = await reviewProtestUseCase.execute({
protestId: 'protest-1',
stewardId,
decision: 'uphold',
decisionNotes: 'Contact was avoidable',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protestId).toBe('protest-1');
expect(data.leagueId).toBe(leagueId);
const updatedProtest = await context.protestRepository.findById('protest-1');
expect(updatedProtest?.status.toString()).toBe('upheld');
expect(updatedProtest?.reviewedBy).toBe('steward-1');
});
it('should uphold a protest and create a penalty', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
const accusedDriverId = 'driver-2';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
await seedDriver({ driverId: accusedDriverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'under_review',
});
const result = await reviewProtestUseCase.execute({
protestId: protest.id.toString(),
stewardId,
decision: 'uphold',
decisionNotes: 'Contact was avoidable',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protestId).toBe('protest-1');
expect(data.leagueId).toBe(leagueId);
const updatedProtest = await context.protestRepository.findById('protest-1');
expect(updatedProtest?.status.toString()).toBe('upheld');
expect(updatedProtest?.reviewedBy).toBe('steward-1');
expect(updatedProtest?.decisionNotes).toBe('Contact was avoidable');
});
it('should dismiss a protest', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
const accusedDriverId = 'driver-2';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
await seedDriver({ driverId: accusedDriverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'under_review',
});
const result = await reviewProtestUseCase.execute({
protestId: protest.id.toString(),
stewardId,
decision: 'dismiss',
decisionNotes: 'No contact occurred',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protestId).toBe('protest-1');
expect(data.leagueId).toBe(leagueId);
const updatedProtest = await context.protestRepository.findById('protest-1');
expect(updatedProtest?.status.toString()).toBe('dismissed');
expect(updatedProtest?.reviewedBy).toBe('steward-1');
expect(updatedProtest?.decisionNotes).toBe('No contact occurred');
});
it('should return PROTEST_NOT_FOUND when protest does not exist', async () => {
const result = await reviewProtestUseCase.execute({
protestId: 'missing-protest',
stewardId: 'steward-1',
decision: 'uphold',
decisionNotes: 'Notes',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND');
});
it('should return RACE_NOT_FOUND when race does not exist', async () => {
const protest = Protest.create({
id: 'protest-1',
raceId: 'missing-race',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact on corner entry',
},
status: 'pending',
});
await context.protestRepository.create(protest);
const result = await reviewProtestUseCase.execute({
protestId: 'protest-1',
stewardId: 'steward-1',
decision: 'uphold',
decisionNotes: 'Notes',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND');
});
});
describe('Apply Penalty', () => {
it('should apply a penalty to a driver', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const driverId = 'driver-1';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const result = await applyPenaltyUseCase.execute({
raceId,
driverId,
type: 'time_penalty',
value: 5,
reason: 'Contact on corner entry',
stewardId,
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.penaltyId).toBeDefined();
const penalty = await context.penaltyRepository.findById(data.penaltyId);
expect(penalty).not.toBeNull();
expect(penalty?.type.toString()).toBe('time_penalty');
expect(penalty?.value).toBe(5);
expect(penalty?.reason.toString()).toBe('Contact on corner entry');
expect(penalty?.issuedBy).toBe('steward-1');
expect(penalty?.status.toString()).toBe('pending');
});
it('should apply a penalty linked to a protest', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
const accusedDriverId = 'driver-2';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
await seedDriver({ driverId: accusedDriverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'upheld',
});
const result = await applyPenaltyUseCase.execute({
raceId,
driverId: accusedDriverId,
type: 'time_penalty',
value: 10,
reason: 'Contact on corner entry',
stewardId,
protestId: protest.id.toString(),
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.penaltyId).toBeDefined();
const penalty = await context.penaltyRepository.findById(data.penaltyId);
expect(penalty).not.toBeNull();
expect(penalty?.protestId?.toString()).toBe('protest-1');
});
it('should return RACE_NOT_FOUND when race does not exist', async () => {
const result = await applyPenaltyUseCase.execute({
raceId: 'missing-race',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Contact on corner entry',
stewardId: 'steward-1',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND');
});
it('should return INSUFFICIENT_AUTHORITY when steward is not admin', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const driverId = 'driver-1';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId });
await seedDriver({ driverId: stewardId });
const result = await applyPenaltyUseCase.execute({
raceId,
driverId,
type: 'time_penalty',
value: 5,
reason: 'Contact on corner entry',
stewardId,
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('INSUFFICIENT_AUTHORITY');
});
});
describe('Quick Penalty', () => {
it('should create a quick penalty without a protest', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const driverId = 'driver-1';
const adminId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId });
await seedDriver({ driverId: adminId });
// Add admin as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: adminId,
role: 'admin',
status: 'active',
})
);
const result = await quickPenaltyUseCase.execute({
raceId,
driverId,
adminId,
infractionType: 'unsafe_rejoin',
severity: 'minor',
notes: 'Speeding in pit lane',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.penaltyId).toBeDefined();
expect(data.raceId).toBe(raceId);
expect(data.driverId).toBe(driverId);
const penalty = await context.penaltyRepository.findById(data.penaltyId);
expect(penalty).not.toBeNull();
expect(penalty?.raceId?.toString()).toBe(raceId);
expect(penalty?.status.toString()).toBe('applied');
});
it('should return RACE_NOT_FOUND when race does not exist', async () => {
const result = await quickPenaltyUseCase.execute({
raceId: 'missing-race',
driverId: 'driver-1',
adminId: 'steward-1',
infractionType: 'track_limits',
severity: 'minor',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND');
});
});
describe('File Protest', () => {
it('should file a new protest', 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 });
// Add drivers as members
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: protestingDriverId,
role: 'driver',
status: 'active',
})
);
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: accusedDriverId,
role: 'driver',
status: 'active',
})
);
const result = await fileProtestUseCase.execute({
raceId,
protestingDriverId,
accusedDriverId,
incident: {
lap: 5,
description: 'Contact on corner entry',
},
comment: 'This was a dangerous move',
proofVideoUrl: 'https://example.com/video.mp4',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protest.id).toBeDefined();
expect(data.protest.raceId).toBe(raceId);
const protest = await context.protestRepository.findById(data.protest.id);
expect(protest).not.toBeNull();
expect(protest?.raceId.toString()).toBe(raceId);
expect(protest?.protestingDriverId.toString()).toBe(protestingDriverId);
expect(protest?.accusedDriverId.toString()).toBe(accusedDriverId);
expect(protest?.status.toString()).toBe('pending');
expect(protest?.comment).toBe('This was a dangerous move');
expect(protest?.proofVideoUrl).toBe('https://example.com/video.mp4');
});
it('should return RACE_NOT_FOUND when race does not exist', async () => {
const result = await fileProtestUseCase.execute({
raceId: 'missing-race',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact on corner entry',
},
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND');
});
it('should return DRIVER_NOT_FOUND when protesting driver does not exist', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
const result = await fileProtestUseCase.execute({
raceId,
protestingDriverId: 'missing-driver',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact on corner entry',
},
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('NOT_MEMBER');
});
it('should return DRIVER_NOT_FOUND when accused driver does not exist', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
// Add protesting driver as member
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: protestingDriverId,
role: 'driver',
status: 'active',
})
);
const result = await fileProtestUseCase.execute({
raceId,
protestingDriverId,
accusedDriverId: 'missing-driver',
incident: {
lap: 5,
description: 'Contact on corner entry',
},
});
expect(result.isOk()).toBe(true);
});
});
describe('Request Protest Defense', () => {
it('should request defense for a pending protest', async () => {
const leagueId = 'league-1';
const raceId = 'race-1';
const protestingDriverId = 'driver-1';
const accusedDriverId = 'driver-2';
const stewardId = 'steward-1';
await seedRacingLeague({ leagueId });
await seedRace({ raceId, leagueId });
await seedDriver({ driverId: protestingDriverId });
await seedDriver({ driverId: accusedDriverId });
await seedDriver({ driverId: stewardId });
// Add steward as admin
await context.leagueMembershipRepository.saveMembership(
LeagueMembership.create({
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active',
})
);
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'pending',
});
const result = await requestProtestDefenseUseCase.execute({
protestId: protest.id.toString(),
stewardId,
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protestId).toBe('protest-1');
const updatedProtest = await context.protestRepository.findById('protest-1');
expect(updatedProtest?.status.toString()).toBe('awaiting_defense');
expect(updatedProtest?.defenseRequestedBy).toBe('steward-1');
});
it('should return PROTEST_NOT_FOUND when protest does not exist', async () => {
const result = await requestProtestDefenseUseCase.execute({
protestId: 'missing-protest',
stewardId: 'steward-1',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND');
});
});
describe('Submit Protest Defense', () => {
it('should submit defense for a protest awaiting defense', 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 });
const protest = await seedProtest({
protestId: 'protest-1',
raceId,
protestingDriverId,
accusedDriverId,
status: 'awaiting_defense',
});
const result = await submitProtestDefenseUseCase.execute({
leagueId,
protestId: protest.id,
driverId: accusedDriverId,
defenseText: 'I was not at fault',
videoUrl: 'https://example.com/defense.mp4',
});
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protestId).toBe('protest-1');
const updatedProtest = await context.protestRepository.findById('protest-1');
expect(updatedProtest?.status.toString()).toBe('under_review');
expect(updatedProtest?.defense?.statement.toString()).toBe('I was not at fault');
expect(updatedProtest?.defense?.videoUrl?.toString()).toBe('https://example.com/defense.mp4');
});
it('should return PROTEST_NOT_FOUND when protest does not exist', async () => {
const leagueId = 'league-1';
await seedRacingLeague({ leagueId });
const result = await submitProtestDefenseUseCase.execute({
leagueId,
protestId: 'missing-protest',
driverId: 'driver-2',
defenseText: 'I was not at fault',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND');
});
});
});