Files
gridpilot.gg/core/racing/application/use-cases/SendPerformanceSummaryUseCase.test.ts
2026-01-16 19:46:49 +01:00

223 lines
8.6 KiB
TypeScript

import type { Logger } from '@core/shared/application/Logger';
import { Result } from '@core/shared/domain/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import type { NotificationService } from '../../../notifications/application/ports/NotificationService';
import type { DriverRepository } from '../../domain/repositories/DriverRepository';
import type { LeagueMembershipRepository } from '../../domain/repositories/LeagueMembershipRepository';
import type { LeagueRepository } from '../../domain/repositories/LeagueRepository';
import type { RaceEventRepository } from '../../domain/repositories/RaceEventRepository';
import type { ResultRepository } from '../../domain/repositories/ResultRepository';
import {
SendPerformanceSummaryUseCase,
type SendPerformanceSummaryErrorCode,
type SendPerformanceSummaryInput,
} from './SendPerformanceSummaryUseCase';
const unwrapError = (
result: Result<void, ApplicationErrorCode<SendPerformanceSummaryErrorCode, { message: string }>>,
): ApplicationErrorCode<SendPerformanceSummaryErrorCode, { message: string }> => {
expect(result.isErr()).toBe(true);
return result.unwrapErr();
};
describe('SendPerformanceSummaryUseCase', () => {
let notificationService: { sendNotification: Mock };
let raceEventRepository: { findById: Mock };
let resultRepository: { findByRaceId: Mock };
let leagueRepository: { findById: Mock };
let membershipRepository: { getMembership: Mock };
let driverRepository: { findById: Mock };
let logger: Logger;
let useCase: SendPerformanceSummaryUseCase;
beforeEach(() => {
notificationService = { sendNotification: vi.fn() };
raceEventRepository = { findById: vi.fn() };
resultRepository = { findByRaceId: vi.fn() };
leagueRepository = { findById: vi.fn() };
membershipRepository = { getMembership: vi.fn() };
driverRepository = { findById: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
useCase = new SendPerformanceSummaryUseCase(notificationService as unknown as NotificationService,
raceEventRepository as unknown as RaceEventRepository,
resultRepository as unknown as ResultRepository,
leagueRepository as unknown as LeagueRepository,
membershipRepository as unknown as LeagueMembershipRepository,
driverRepository as unknown as DriverRepository,
logger);
});
const createInput = (overrides: Partial<SendPerformanceSummaryInput> = {}): SendPerformanceSummaryInput => ({
leagueId: 'league-1',
raceId: 'race-1',
driverId: 'driver-1',
triggeredById: 'driver-1',
...overrides,
});
it('sends performance summary notification and presents result on success', async () => {
const mockRaceEvent = {
id: 'race-1',
leagueId: 'league-1',
name: 'Test Race',
getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1', status: 'completed' }),
};
const mockLeague = { id: 'league-1' };
const mockDriver = { id: 'driver-1' };
leagueRepository.findById.mockResolvedValue(mockLeague);
raceEventRepository.findById.mockResolvedValue(mockRaceEvent);
driverRepository.findById.mockResolvedValue(mockDriver);
const mockResults = [
{
driverId: { toString: () => 'driver-1' },
position: { toNumber: () => 1 },
incidents: { toNumber: () => 0 },
getPositionChange: vi.fn().mockReturnValue(2),
},
];
resultRepository.findByRaceId.mockResolvedValue(mockResults);
const input = createInput();
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const presented = result.unwrap();
expect(leagueRepository.findById).toHaveBeenCalledWith('league-1');
expect(raceEventRepository.findById).toHaveBeenCalledWith('race-1');
expect(driverRepository.findById).toHaveBeenCalledWith('driver-1');
expect(resultRepository.findByRaceId).toHaveBeenCalledWith('session-1');
expect(notificationService.sendNotification).toHaveBeenCalledTimes(1);
expect(notificationService.sendNotification).toHaveBeenCalledWith(
expect.objectContaining({
recipientId: 'driver-1',
type: 'race_performance_summary',
title: 'Race Complete: Test Race',
}),
);
expect(presented).toEqual({
leagueId: 'league-1',
raceId: 'race-1',
driverId: 'driver-1',
notificationsSent: 1,
});
});
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
leagueRepository.findById.mockResolvedValue(null);
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('LEAGUE_NOT_FOUND');
expect(error.details?.message).toBe('League not found');
});
it('returns RACE_NOT_FOUND when race event does not exist', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
raceEventRepository.findById.mockResolvedValue(null);
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('RACE_NOT_FOUND');
expect(error.details?.message).toBe('Race event not found');
});
it('returns DRIVER_NOT_FOUND when driver does not exist', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
raceEventRepository.findById.mockResolvedValue({
id: 'race-1',
leagueId: 'league-1',
name: 'Test Race',
getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1', status: 'completed' }),
});
driverRepository.findById.mockResolvedValue(null);
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('DRIVER_NOT_FOUND');
expect(error.details?.message).toBe('Driver not found');
});
it('returns INSUFFICIENT_PERMISSIONS when triggeredBy is not driver and not steward or higher', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
raceEventRepository.findById.mockResolvedValue({
id: 'race-1',
leagueId: 'league-1',
name: 'Test Race',
getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1', status: 'completed' }),
});
driverRepository.findById.mockResolvedValue({ id: 'driver-1' });
membershipRepository.getMembership.mockResolvedValue(null);
const result = await useCase.execute(createInput({ triggeredById: 'user-1' }));
const error = unwrapError(result);
expect(error.code).toBe('INSUFFICIENT_PERMISSIONS');
expect(error.details?.message).toBe('Insufficient permissions to send performance summary');
});
it('returns SUMMARY_NOT_AVAILABLE when main race session is missing or not completed', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
raceEventRepository.findById.mockResolvedValue({
id: 'race-1',
leagueId: 'league-1',
name: 'Test Race',
getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1', status: 'in_progress' }),
});
driverRepository.findById.mockResolvedValue({ id: 'driver-1' });
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('SUMMARY_NOT_AVAILABLE');
expect(error.details?.message).toBe('Performance summary is not available for this race');
});
it('returns SUMMARY_NOT_AVAILABLE when no result exists for driver', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
raceEventRepository.findById.mockResolvedValue({
id: 'race-1',
leagueId: 'league-1',
name: 'Test Race',
getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1', status: 'completed' }),
});
driverRepository.findById.mockResolvedValue({ id: 'driver-1' });
resultRepository.findByRaceId.mockResolvedValue([]);
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('SUMMARY_NOT_AVAILABLE');
expect(error.details?.message).toBe('Performance summary is not available for this driver');
});
it('wraps repository errors into REPOSITORY_ERROR and does not present output', async () => {
const mockError = new Error('Repository failure');
leagueRepository.findById.mockRejectedValue(mockError);
const result = await useCase.execute(createInput());
const error = unwrapError(result);
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details?.message).toBe('Repository failure');
expect(logger.error).toHaveBeenCalled();
});
});