Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
767 lines
24 KiB
TypeScript
767 lines
24 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 { 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');
|
|
});
|
|
});
|
|
});
|