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

239 lines
6.5 KiB
TypeScript

import {
CreateSeasonForLeagueUseCase,
type CreateSeasonForLeagueInput,
type LeagueConfigFormModel,
} from '@core/racing/application/use-cases/CreateSeasonForLeagueUseCase';
import { Season } from '@core/racing/domain/entities/season/Season';
import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository';
import type { SeasonRepository } from '@core/racing/domain/repositories/SeasonRepository';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
function createLeagueConfigFormModel(overrides?: Partial<LeagueConfigFormModel>): LeagueConfigFormModel {
return {
basics: {
name: 'Test League',
visibility: 'ranked',
gameId: 'iracing',
...overrides?.basics,
},
structure: {
mode: 'solo',
maxDrivers: 30,
...overrides?.structure,
},
championships: {
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
...overrides?.championships,
},
scoring: {
patternId: 'sprint-main-driver',
customScoringEnabled: false,
...overrides?.scoring,
},
dropPolicy: {
strategy: 'bestNResults',
n: 3,
...overrides?.dropPolicy,
},
timings: {
qualifyingMinutes: 10,
mainRaceMinutes: 30,
sessionCount: 8,
seasonStartDate: '2025-01-01',
raceStartTime: '20:00',
timezoneId: 'UTC',
recurrenceStrategy: 'weekly',
weekdays: ['Mon'],
...overrides?.timings,
},
stewarding: {
decisionMode: 'steward_vote',
requiredVotes: 3,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
...overrides?.stewarding,
},
...overrides,
};
}
describe('CreateSeasonForLeagueUseCase', () => {
const mockLeagueFindById = vi.fn();
const mockLeagueRepo: {
findById: Mock;
findAll: Mock;
findByOwnerId: Mock;
create: Mock;
update: Mock;
delete: Mock;
exists: Mock;
searchByName: Mock;
} = {
findById: mockLeagueFindById,
findAll: vi.fn(),
findByOwnerId: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
exists: vi.fn(),
searchByName: vi.fn(),
};
const mockSeasonFindById = vi.fn();
const mockSeasonAdd = vi.fn();
const mockSeasonRepo: {
findById: Mock;
findByLeagueId: Mock;
create: Mock;
add: Mock;
update: Mock;
listByLeague: Mock;
listActiveByLeague: Mock;
} = {
findById: mockSeasonFindById,
findByLeagueId: vi.fn(),
create: vi.fn(),
add: mockSeasonAdd,
update: vi.fn(),
listByLeague: vi.fn(),
listActiveByLeague: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it('creates a planned Season for an existing league with config-derived props', async () => {
mockLeagueFindById.mockResolvedValue({ id: 'league-1' });
mockSeasonAdd.mockResolvedValue(undefined);
const useCase = new CreateSeasonForLeagueUseCase(
mockLeagueRepo as unknown as LeagueRepository,
mockSeasonRepo as unknown as SeasonRepository
);
const config = createLeagueConfigFormModel({
basics: {
name: 'League With Config',
visibility: 'ranked',
gameId: 'iracing',
},
scoring: {
patternId: 'club-default',
customScoringEnabled: true,
},
dropPolicy: {
strategy: 'dropWorstN',
n: 2,
},
timings: {
qualifyingMinutes: 10,
mainRaceMinutes: 30,
sessionCount: 8,
},
});
const command: CreateSeasonForLeagueInput = {
leagueId: 'league-1',
name: 'Season from Config',
gameId: 'iracing',
config,
};
const result = await useCase.execute(command);
expect(result.isOk()).toBe(true);
const presented = result.unwrap();
expect(presented.season).toBeInstanceOf(Season);
expect(presented.league.id).toBe('league-1');
});
it('clones configuration from a source season when sourceSeasonId is provided', async () => {
const sourceSeason = Season.create({
id: 'source-season',
leagueId: 'league-1',
gameId: 'iracing',
name: 'Source Season',
status: 'planned',
}).withMaxDrivers(40);
mockLeagueFindById.mockResolvedValue({ id: 'league-1' });
mockSeasonFindById.mockResolvedValue(sourceSeason);
mockSeasonAdd.mockResolvedValue(undefined);
const useCase = new CreateSeasonForLeagueUseCase(
mockLeagueRepo as unknown as LeagueRepository,
mockSeasonRepo as unknown as SeasonRepository
);
const command: CreateSeasonForLeagueInput = {
leagueId: 'league-1',
name: 'Cloned Season',
gameId: 'iracing',
sourceSeasonId: 'source-season',
};
const result = await useCase.execute(command);
expect(result.isOk()).toBe(true);
const presented = result.unwrap();
expect(presented.season.maxDrivers).toBe(40);
});
it('returns error when league not found', async () => {
mockLeagueFindById.mockResolvedValue(null);
const useCase = new CreateSeasonForLeagueUseCase(
mockLeagueRepo as unknown as LeagueRepository,
mockSeasonRepo as unknown as SeasonRepository
);
const command: CreateSeasonForLeagueInput = {
leagueId: 'missing-league',
name: 'Any',
gameId: 'iracing',
};
const result = await useCase.execute(command);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('LEAGUE_NOT_FOUND');
expect(error.details?.message).toBe('League not found: missing-league');
});
it('returns validation error when source season is missing', async () => {
mockLeagueFindById.mockResolvedValue({ id: 'league-1' });
mockSeasonFindById.mockResolvedValue(undefined);
const useCase = new CreateSeasonForLeagueUseCase(
mockLeagueRepo as unknown as LeagueRepository,
mockSeasonRepo as unknown as SeasonRepository
);
const command: CreateSeasonForLeagueInput = {
leagueId: 'league-1',
name: 'Cloned Season',
gameId: 'iracing',
sourceSeasonId: 'missing-source',
};
const result = await useCase.execute(command);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.details?.message).toBe('Source Season not found: missing-source');
});
});