311 lines
9.1 KiB
TypeScript
311 lines
9.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import {
|
|
InMemorySeasonRepository,
|
|
} from '@core/racing/infrastructure/repositories/InMemoryScoringRepositories';
|
|
import { Season } from '@core/racing/domain/entities/Season';
|
|
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
|
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
|
import {
|
|
CreateSeasonForLeagueUseCase,
|
|
type CreateSeasonForLeagueCommand,
|
|
} from '@core/racing/application/use-cases/CreateSeasonForLeagueUseCase';
|
|
import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO';
|
|
import type { Logger } from '@core/shared/application';
|
|
|
|
const logger: Logger = {
|
|
debug: () => {},
|
|
info: () => {},
|
|
warn: () => {},
|
|
error: () => {},
|
|
};
|
|
|
|
function createFakeLeagueRepository(seed: Array<{ id: string }>): ILeagueRepository {
|
|
return {
|
|
findById: async (id: string) => seed.find((l) => l.id === id) ?? null,
|
|
findAll: async () => seed,
|
|
create: async (league: any) => league,
|
|
update: async (league: any) => league,
|
|
} as unknown as ILeagueRepository;
|
|
}
|
|
|
|
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('InMemorySeasonRepository', () => {
|
|
it('add and findById provide a roundtrip for Season', async () => {
|
|
const repo = new InMemorySeasonRepository(logger);
|
|
const season = Season.create({
|
|
id: 'season-1',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Test Season',
|
|
status: 'planned',
|
|
});
|
|
|
|
await repo.add(season);
|
|
const loaded = await repo.findById(season.id);
|
|
|
|
expect(loaded).not.toBeNull();
|
|
expect(loaded!.id).toBe(season.id);
|
|
expect(loaded!.leagueId).toBe(season.leagueId);
|
|
expect(loaded!.status).toBe('planned');
|
|
});
|
|
|
|
it('update persists changed Season state', async () => {
|
|
const repo = new InMemorySeasonRepository(logger);
|
|
const season = Season.create({
|
|
id: 'season-1',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Initial Season',
|
|
status: 'planned',
|
|
});
|
|
|
|
await repo.add(season);
|
|
const activated = season.activate();
|
|
|
|
await repo.update(activated);
|
|
|
|
const loaded = await repo.findById(season.id);
|
|
expect(loaded).not.toBeNull();
|
|
expect(loaded!.status).toBe('active');
|
|
});
|
|
|
|
it('listByLeague returns only seasons for that league', async () => {
|
|
const repo = new InMemorySeasonRepository(logger);
|
|
const s1 = Season.create({
|
|
id: 's1',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'L1 S1',
|
|
status: 'planned',
|
|
});
|
|
const s2 = Season.create({
|
|
id: 's2',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'L1 S2',
|
|
status: 'active',
|
|
});
|
|
const s3 = Season.create({
|
|
id: 's3',
|
|
leagueId: 'league-2',
|
|
gameId: 'iracing',
|
|
name: 'L2 S1',
|
|
status: 'planned',
|
|
});
|
|
|
|
await repo.add(s1);
|
|
await repo.add(s2);
|
|
await repo.add(s3);
|
|
|
|
const league1Seasons = await repo.listByLeague('league-1');
|
|
const league2Seasons = await repo.listByLeague('league-2');
|
|
|
|
expect(league1Seasons.map((s: Season) => s.id).sort()).toEqual(['s1', 's2']);
|
|
expect(league2Seasons.map((s: Season) => s.id)).toEqual(['s3']);
|
|
});
|
|
|
|
it('listActiveByLeague returns only active seasons for a league', async () => {
|
|
const repo = new InMemorySeasonRepository(logger);
|
|
const s1 = Season.create({
|
|
id: 's1',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Planned',
|
|
status: 'planned',
|
|
});
|
|
const s2 = Season.create({
|
|
id: 's2',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Active',
|
|
status: 'active',
|
|
});
|
|
const s3 = Season.create({
|
|
id: 's3',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Completed',
|
|
status: 'completed',
|
|
});
|
|
const s4 = Season.create({
|
|
id: 's4',
|
|
leagueId: 'league-2',
|
|
gameId: 'iracing',
|
|
name: 'Other League Active',
|
|
status: 'active',
|
|
});
|
|
|
|
await repo.add(s1);
|
|
await repo.add(s2);
|
|
await repo.add(s3);
|
|
await repo.add(s4);
|
|
|
|
const activeInLeague1 = await repo.listActiveByLeague('league-1');
|
|
|
|
expect(activeInLeague1.map((s: Season) => s.id)).toEqual(['s2']);
|
|
});
|
|
});
|
|
|
|
describe('CreateSeasonForLeagueUseCase', () => {
|
|
it('creates a planned Season for an existing league with config-derived props', async () => {
|
|
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
|
const seasonRepo = new InMemorySeasonRepository(logger);
|
|
|
|
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
|
|
|
const config = createLeagueConfigFormModel({
|
|
basics: {
|
|
name: 'League With Config',
|
|
visibility: 'ranked',
|
|
gameId: 'iracing',
|
|
},
|
|
scoring: {
|
|
patternId: 'club-default',
|
|
customScoringEnabled: true,
|
|
},
|
|
dropPolicy: {
|
|
strategy: 'dropWorstN',
|
|
n: 2,
|
|
},
|
|
// Intentionally omit seasonStartDate / raceStartTime to avoid schedule derivation,
|
|
// focusing this test on scoring/drop/stewarding/maxDrivers mapping.
|
|
timings: {
|
|
qualifyingMinutes: 10,
|
|
mainRaceMinutes: 30,
|
|
sessionCount: 8,
|
|
},
|
|
});
|
|
|
|
const command: CreateSeasonForLeagueCommand = {
|
|
leagueId: 'league-1',
|
|
name: 'Season from Config',
|
|
gameId: 'iracing',
|
|
config,
|
|
};
|
|
|
|
const result = await useCase.execute(command);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(result.value.seasonId).toBeDefined();
|
|
|
|
const created = await seasonRepo.findById(result.value.seasonId);
|
|
expect(created).not.toBeNull();
|
|
const season = created!;
|
|
|
|
expect(season.leagueId).toBe('league-1');
|
|
expect(season.gameId).toBe('iracing');
|
|
expect(season.name).toBe('Season from Config');
|
|
expect(season.status).toBe('planned');
|
|
|
|
// Schedule is optional when timings lack seasonStartDate / raceStartTime.
|
|
expect(season.schedule).toBeUndefined();
|
|
expect(season.scoringConfig).toBeDefined();
|
|
expect(season.scoringConfig!.scoringPresetId).toBe('club-default');
|
|
expect(season.scoringConfig!.customScoringEnabled).toBe(true);
|
|
|
|
expect(season.dropPolicy).toBeDefined();
|
|
expect(season.dropPolicy!.strategy).toBe('dropWorstN');
|
|
expect(season.dropPolicy!.n).toBe(2);
|
|
|
|
expect(season.stewardingConfig).toBeDefined();
|
|
expect(season.maxDrivers).toBe(30);
|
|
});
|
|
|
|
it('clones configuration from a source season when sourceSeasonId is provided', async () => {
|
|
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
|
const seasonRepo = new InMemorySeasonRepository(logger);
|
|
|
|
const sourceSeason = Season.create({
|
|
id: 'source-season',
|
|
leagueId: 'league-1',
|
|
gameId: 'iracing',
|
|
name: 'Source Season',
|
|
status: 'planned',
|
|
}).withMaxDrivers(40);
|
|
|
|
await seasonRepo.add(sourceSeason);
|
|
|
|
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
|
|
|
const command: CreateSeasonForLeagueCommand = {
|
|
leagueId: 'league-1',
|
|
name: 'Cloned Season',
|
|
gameId: 'iracing',
|
|
sourceSeasonId: 'source-season',
|
|
};
|
|
|
|
const result = await useCase.execute(command);
|
|
const created = await seasonRepo.findById(result.value.seasonId);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(created).not.toBeNull();
|
|
const season = created!;
|
|
|
|
expect(season.id).not.toBe(sourceSeason.id);
|
|
expect(season.leagueId).toBe(sourceSeason.leagueId);
|
|
expect(season.gameId).toBe(sourceSeason.gameId);
|
|
expect(season.status).toBe('planned');
|
|
expect(season.maxDrivers).toBe(sourceSeason.maxDrivers);
|
|
expect(season.schedule).toBe(sourceSeason.schedule);
|
|
expect(season.scoringConfig).toBe(sourceSeason.scoringConfig);
|
|
expect(season.dropPolicy).toBe(sourceSeason.dropPolicy);
|
|
expect(season.stewardingConfig).toBe(sourceSeason.stewardingConfig);
|
|
});
|
|
}); |