239 lines
6.5 KiB
TypeScript
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');
|
|
});
|
|
});
|