Files
gridpilot.gg/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts
2025-12-23 15:38:50 +01:00

214 lines
8.3 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import {
RecalculateChampionshipStandingsUseCase,
type RecalculateChampionshipStandingsInput,
type RecalculateChampionshipStandingsResult,
type RecalculateChampionshipStandingsErrorCode,
} from './RecalculateChampionshipStandingsUseCase';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IChampionshipStandingRepository } from '../../domain/repositories/IChampionshipStandingRepository';
import type { Penalty } from '../../domain/entities/Penalty';
import { EventScoringService } from '../../domain/services/EventScoringService';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import { ChampionshipAggregator } from '../../domain/services/ChampionshipAggregator';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('RecalculateChampionshipStandingsUseCase', () => {
let useCase: RecalculateChampionshipStandingsUseCase;
let leagueRepository: { findById: Mock };
let seasonRepository: { findById: Mock };
let leagueScoringConfigRepository: { findBySeasonId: Mock };
let raceRepository: { findByLeagueId: Mock };
let resultRepository: { findByRaceId: Mock };
let penaltyRepository: { findByRaceId: Mock };
let championshipStandingRepository: { saveAll: Mock };
let eventScoringService: { scoreSession: Mock };
let championshipAggregator: { aggregate: Mock };
let logger: Logger;
let output: UseCaseOutputPort<RecalculateChampionshipStandingsResult> & {
present: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
leagueRepository = { findById: vi.fn() };
seasonRepository = { findById: vi.fn() };
leagueScoringConfigRepository = { findBySeasonId: vi.fn() };
raceRepository = { findByLeagueId: vi.fn() };
resultRepository = { findByRaceId: vi.fn() };
penaltyRepository = { findByRaceId: vi.fn() };
championshipStandingRepository = { saveAll: vi.fn() };
eventScoringService = { scoreSession: vi.fn() };
championshipAggregator = { aggregate: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
output = { present: vi.fn() } as unknown as typeof output;
useCase = new RecalculateChampionshipStandingsUseCase(
leagueRepository as unknown as ILeagueRepository,
seasonRepository as unknown as ISeasonRepository,
leagueScoringConfigRepository as unknown as ILeagueScoringConfigRepository,
raceRepository as unknown as IRaceRepository,
resultRepository as unknown as IResultRepository,
penaltyRepository as unknown as IPenaltyRepository,
championshipStandingRepository as unknown as IChampionshipStandingRepository,
eventScoringService as unknown as EventScoringService,
championshipAggregator as unknown as ChampionshipAggregator,
logger,
output,
);
});
it('returns league not found error', async () => {
leagueRepository.findById.mockResolvedValue(null);
const input: RecalculateChampionshipStandingsInput = {
leagueId: 'league-1',
seasonId: 'season-1',
};
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
RecalculateChampionshipStandingsErrorCode,
{ message: string }
>;
expect(error.code).toBe('LEAGUE_NOT_FOUND');
expect(error.details.message).toContain('league-1');
expect(output.present).not.toHaveBeenCalled();
});
it('returns season not found error when season does not exist', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
seasonRepository.findById.mockResolvedValue(null);
const input: RecalculateChampionshipStandingsInput = {
leagueId: 'league-1',
seasonId: 'season-1',
};
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
RecalculateChampionshipStandingsErrorCode,
{ message: string }
>;
expect(error.code).toBe('SEASON_NOT_FOUND');
expect(output.present).not.toHaveBeenCalled();
});
it('returns season not found error when season belongs to different league', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
seasonRepository.findById.mockResolvedValue({ id: 'season-1', leagueId: 'other-league' });
const input: RecalculateChampionshipStandingsInput = {
leagueId: 'league-1',
seasonId: 'season-1',
};
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
RecalculateChampionshipStandingsErrorCode,
{ message: string }
>;
expect(error.code).toBe('SEASON_NOT_FOUND');
expect(output.present).not.toHaveBeenCalled();
});
it('recalculates standings successfully and presents result', async () => {
const league = { id: 'league-1' };
const season = { id: 'season-1', leagueId: 'league-1' };
const championship = {
id: 'champ-1',
name: 'Champ 1',
sessionTypes: ['main'],
pointsTableBySessionType: {},
dropScorePolicy: {},
};
const leagueScoringConfig = { championships: [championship] };
const races = [{ id: 'race-1', sessionType: 'race' as const }];
const results: unknown[] = [];
const penalties: Penalty[] = [];
const standings = [
{
participant: { id: 'driver-1' },
position: { toNumber: () => 1 },
totalPoints: { toNumber: () => 25 },
resultsCounted: { toNumber: () => 1 },
resultsDropped: { toNumber: () => 0 },
},
];
leagueRepository.findById.mockResolvedValue(league);
seasonRepository.findById.mockResolvedValue(season);
leagueScoringConfigRepository.findBySeasonId.mockResolvedValue(leagueScoringConfig);
raceRepository.findByLeagueId.mockResolvedValue(races);
resultRepository.findByRaceId.mockResolvedValue(results);
penaltyRepository.findByRaceId.mockResolvedValue(penalties);
eventScoringService.scoreSession.mockReturnValue({});
championshipAggregator.aggregate.mockReturnValue(standings);
championshipStandingRepository.saveAll.mockResolvedValue(undefined);
const input: RecalculateChampionshipStandingsInput = {
leagueId: 'league-1',
seasonId: 'season-1',
};
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as RecalculateChampionshipStandingsResult;
expect(presented.leagueId).toBe('league-1');
expect(presented.seasonId).toBe('season-1');
expect(presented.entries).toHaveLength(1);
expect(presented.entries[0]).toEqual({
driverId: 'driver-1',
teamId: null,
position: 1,
points: 25,
});
});
it('wraps repository failures in REPOSITORY_ERROR and does not call output', async () => {
leagueRepository.findById.mockResolvedValue({ id: 'league-1' });
seasonRepository.findById.mockResolvedValue({ id: 'season-1', leagueId: 'league-1' });
leagueScoringConfigRepository.findBySeasonId.mockImplementation(() => {
throw new Error('boom');
});
const input: RecalculateChampionshipStandingsInput = {
leagueId: 'league-1',
seasonId: 'season-1',
};
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
RecalculateChampionshipStandingsErrorCode,
{ message: string }
>;
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toContain('boom');
expect(output.present).not.toHaveBeenCalled();
});
});