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, ); 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'); }); }); });