refactor use cases
This commit is contained in:
@@ -21,17 +21,8 @@ import {
|
||||
type ManageSeasonLifecycleErrorCode,
|
||||
type LeagueConfigFormModel,
|
||||
} from '@core/racing/application/use-cases/SeasonUseCases';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
type MockOutputPort<T> = UseCaseOutputPort<T> & { present: ReturnType<typeof vi.fn> };
|
||||
|
||||
function createOutputPort<T>(): MockOutputPort<T> {
|
||||
return {
|
||||
present: vi.fn<(data: T) => void>(),
|
||||
};
|
||||
}
|
||||
|
||||
function getUnknownString(value: unknown): string | null {
|
||||
if (typeof value === 'string') return value;
|
||||
if (
|
||||
@@ -137,15 +128,13 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<CreateSeasonForLeagueResult>();
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output);
|
||||
|
||||
return { leagueRepo, seasonRepo, output, useCase };
|
||||
return { leagueRepo, seasonRepo, useCase };
|
||||
}
|
||||
|
||||
it('creates a planned Season for an existing league with config-derived props', async () => {
|
||||
const { seasonRepo, output, useCase } = setup();
|
||||
const { seasonRepo, useCase } = setup();
|
||||
|
||||
const config = createLeagueConfigFormModel({
|
||||
basics: {
|
||||
@@ -161,8 +150,6 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
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,
|
||||
@@ -182,13 +169,9 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
||||
expect(payloadRaw).toBeDefined();
|
||||
const payload = payloadRaw as CreateSeasonForLeagueResult;
|
||||
|
||||
const payload = result.unwrap();
|
||||
expect(payload.season).toBeDefined();
|
||||
|
||||
const season = payload.season;
|
||||
expect(season.leagueId).toBe('league-1');
|
||||
expect(season.gameId).toBe('iracing');
|
||||
@@ -210,7 +193,7 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
});
|
||||
|
||||
it('clones configuration from a source season when sourceSeasonId is provided', async () => {
|
||||
const { seasonRepo, output, useCase } = setup();
|
||||
const { seasonRepo, useCase } = setup();
|
||||
|
||||
const sourceSeason = Season.create({
|
||||
id: 'source-season',
|
||||
@@ -233,12 +216,8 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
||||
expect(payloadRaw).toBeDefined();
|
||||
const payload = payloadRaw as CreateSeasonForLeagueResult;
|
||||
const payload = result.unwrap();
|
||||
expect(payload.season).toBeDefined();
|
||||
|
||||
const season = payload.season;
|
||||
expect(season.id).not.toBe(sourceSeason.id);
|
||||
@@ -262,9 +241,7 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<CreateSeasonForLeagueResult>();
|
||||
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output);
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const command: CreateSeasonForLeagueCommand = {
|
||||
leagueId: 'missing-league',
|
||||
@@ -278,11 +255,9 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<CreateSeasonForLeagueErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('League not found');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('ListSeasonsForLeagueUseCase', () => {
|
||||
function setup() {
|
||||
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
||||
@@ -294,15 +269,13 @@ describe('ListSeasonsForLeagueUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<ListSeasonsForLeagueResult>();
|
||||
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output);
|
||||
|
||||
return { leagueRepo, seasonRepo, output, useCase };
|
||||
return { leagueRepo, seasonRepo, useCase };
|
||||
}
|
||||
|
||||
it('lists seasons for a league with summaries', async () => {
|
||||
const { seasonRepo, output, useCase } = setup();
|
||||
const { seasonRepo, useCase } = setup();
|
||||
|
||||
const s1 = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -335,12 +308,8 @@ describe('ListSeasonsForLeagueUseCase', () => {
|
||||
const result = await useCase.execute({ leagueId: 'league-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
||||
expect(payloadRaw).toBeDefined();
|
||||
const payload = payloadRaw as ListSeasonsForLeagueResult;
|
||||
const payload = result.unwrap();
|
||||
expect(payload.seasons).toBeDefined();
|
||||
|
||||
const league1Seasons = payload.seasons.filter((s) => s.leagueId === 'league-1');
|
||||
expect(league1Seasons.map((s) => s.id).sort()).toEqual(['season-1', 'season-2']);
|
||||
@@ -356,9 +325,7 @@ describe('ListSeasonsForLeagueUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<ListSeasonsForLeagueResult>();
|
||||
|
||||
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output);
|
||||
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const result = await useCase.execute({ leagueId: 'missing-league' });
|
||||
|
||||
@@ -366,11 +333,9 @@ describe('ListSeasonsForLeagueUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<ListSeasonsForLeagueErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('League not found');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('GetSeasonDetailsUseCase', () => {
|
||||
function setup(leagueSeed: Array<{ id: string }>) {
|
||||
const leagueRepo = createFakeLeagueRepository(leagueSeed);
|
||||
@@ -382,15 +347,13 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<GetSeasonDetailsResult>();
|
||||
const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo, output);
|
||||
|
||||
return { leagueRepo, seasonRepo, output, useCase };
|
||||
return { leagueRepo, seasonRepo, useCase };
|
||||
}
|
||||
|
||||
it('returns full details for a season belonging to the league', async () => {
|
||||
const { seasonRepo, output, useCase } = setup([{ id: 'league-1' }]);
|
||||
const { seasonRepo, useCase } = setup([{ id: 'league-1' }]);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -408,12 +371,8 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
||||
expect(payloadRaw).toBeDefined();
|
||||
const payload = payloadRaw as GetSeasonDetailsResult;
|
||||
const payload = result.unwrap();
|
||||
expect(payload.season).toBeDefined();
|
||||
|
||||
expect(payload.season.id).toBe('season-1');
|
||||
expect(payload.season.leagueId).toBe('league-1');
|
||||
@@ -424,7 +383,7 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
});
|
||||
|
||||
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
|
||||
const { output, useCase } = setup([]);
|
||||
const { useCase } = setup([]);
|
||||
|
||||
const result = await useCase.execute({
|
||||
leagueId: 'missing-league',
|
||||
@@ -435,11 +394,10 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<GetSeasonDetailsErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('League not found');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns SEASON_NOT_FOUND when season does not belong to league', async () => {
|
||||
const { seasonRepo, output, useCase } = setup([{ id: 'league-1' }]);
|
||||
const { seasonRepo, useCase } = setup([{ id: 'league-1' }]);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -460,11 +418,9 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<GetSeasonDetailsErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('SEASON_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('does not belong to league');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('ManageSeasonLifecycleUseCase', () => {
|
||||
function setup() {
|
||||
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
||||
@@ -476,15 +432,13 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<ManageSeasonLifecycleResult>();
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);
|
||||
|
||||
return { leagueRepo, seasonRepo, output, useCase };
|
||||
return { leagueRepo, seasonRepo, useCase };
|
||||
}
|
||||
|
||||
it('applies activate → complete → archive transitions and persists state', async () => {
|
||||
const { seasonRepo, output, useCase } = setup();
|
||||
const { seasonRepo, useCase } = setup();
|
||||
|
||||
let currentSeason = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -508,10 +462,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
|
||||
const activated = await useCase.execute(activateCommand);
|
||||
expect(activated.isOk()).toBe(true);
|
||||
|
||||
const activatePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
||||
expect(activatePayloadRaw).toBeDefined();
|
||||
const activatePayload = activatePayloadRaw as ManageSeasonLifecycleResult;
|
||||
const activatePayload = activated.unwrap();
|
||||
expect(activatePayload.season.status.toString()).toBe('active');
|
||||
|
||||
const completeCommand: ManageSeasonLifecycleCommand = {
|
||||
@@ -522,10 +473,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
|
||||
const completed = await useCase.execute(completeCommand);
|
||||
expect(completed.isOk()).toBe(true);
|
||||
|
||||
const completePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[1]?.[0];
|
||||
expect(completePayloadRaw).toBeDefined();
|
||||
const completePayload = completePayloadRaw as ManageSeasonLifecycleResult;
|
||||
const completePayload = completed.unwrap();
|
||||
expect(completePayload.season.status.toString()).toBe('completed');
|
||||
|
||||
const archiveCommand: ManageSeasonLifecycleCommand = {
|
||||
@@ -536,17 +484,14 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
|
||||
const archived = await useCase.execute(archiveCommand);
|
||||
expect(archived.isOk()).toBe(true);
|
||||
|
||||
const archivePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[2]?.[0];
|
||||
expect(archivePayloadRaw).toBeDefined();
|
||||
const archivePayload = archivePayloadRaw as ManageSeasonLifecycleResult;
|
||||
const archivePayload = archived.unwrap();
|
||||
expect(archivePayload.season.status.toString()).toBe('archived');
|
||||
|
||||
expect(currentSeason.status.toString()).toBe('archived');
|
||||
});
|
||||
|
||||
it('returns INVALID_LIFECYCLE_TRANSITION for invalid transitions and does not call output', async () => {
|
||||
const { seasonRepo, output, useCase } = setup();
|
||||
const { seasonRepo, useCase } = setup();
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -570,7 +515,6 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('INVALID_LIFECYCLE_TRANSITION');
|
||||
expect(error.details?.message).toBeDefined();
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
|
||||
@@ -583,9 +527,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<ManageSeasonLifecycleResult>();
|
||||
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const command: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'missing-league',
|
||||
@@ -599,7 +541,6 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('League not found');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns SEASON_NOT_FOUND when season does not belong to league', async () => {
|
||||
@@ -612,9 +553,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
listActiveByLeague: vi.fn(),
|
||||
} as unknown as ISeasonRepository;
|
||||
|
||||
const output = createOutputPort<ManageSeasonLifecycleResult>();
|
||||
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
@@ -638,6 +577,5 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
|
||||
expect(error.code).toBe('SEASON_NOT_FOUND');
|
||||
expect(error.details?.message).toContain('does not belong to league');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user