refactor
This commit is contained in:
@@ -15,7 +15,7 @@ import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ApplyPenaltyCommand } from './ApplyPenaltyCommand';
|
||||
import type { ApplyPenaltyCommand } from '../dto/ApplyPenaltyCommand';
|
||||
|
||||
export class ApplyPenaltyUseCase
|
||||
implements AsyncUseCase<ApplyPenaltyCommand, { penaltyId: string }, string> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { ApproveLeagueJoinRequestUseCaseParams } from './ApproveLeagueJoinRequestUseCaseParams';
|
||||
import type { ApproveLeagueJoinRequestUseCaseParams } from '../dto/ApproveLeagueJoinRequestUseCaseParams';
|
||||
import type { ApproveLeagueJoinRequestResultDTO } from '../dto/ApproveLeagueJoinRequestResultDTO';
|
||||
|
||||
export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultDTO, string> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { DomainEventPublisher } from '@/shared/domain/DomainEvent';
|
||||
import { RaceEventStewardingClosedEvent } from '../../domain/events/RaceEventStewardingClosed';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CloseRaceEventStewardingCommand } from './CloseRaceEventStewardingCommand';
|
||||
import type { CloseRaceEventStewardingCommand } from '../dto/CloseRaceEventStewardingCommand';
|
||||
import type { RaceEvent } from '../../domain/entities/RaceEvent';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { CompleteDriverOnboardingUseCase } from './CompleteDriverOnboardingUseCase';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import type { CompleteDriverOnboardingCommand } from './CompleteDriverOnboardingCommand';
|
||||
import type { CompleteDriverOnboardingCommand } from '../dto/CompleteDriverOnboardingCommand';
|
||||
|
||||
describe('CompleteDriverOnboardingUseCase', () => {
|
||||
let useCase: CompleteDriverOnboardingUseCase;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CompleteDriverOnboardingCommand } from './CompleteDriverOnboardingCommand';
|
||||
import type { CompleteDriverOnboardingCommand } from '../dto/CompleteDriverOnboardingCommand';
|
||||
|
||||
/**
|
||||
* Use Case for completing driver onboarding.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from './CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import type { CreateLeagueWithSeasonAndScoringCommand } from './CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import type { CreateLeagueWithSeasonAndScoringCommand } from '../dto/CreateLeagueWithSeasonAndScoringCommand';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../../domain/value-objects/LeagueVisibility';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { LeagueVisibilityInput } from './LeagueVisibilityInput';
|
||||
import type { LeagueVisibilityInput } from '../dto/LeagueVisibilityInput';
|
||||
import type { CreateLeagueWithSeasonAndScoringResultDTO } from '../dto/CreateLeagueWithSeasonAndScoringResultDTO';
|
||||
|
||||
export interface CreateLeagueWithSeasonAndScoringCommand {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } 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';
|
||||
@@ -11,23 +8,6 @@ import {
|
||||
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 {
|
||||
@@ -86,129 +66,40 @@ function createLeagueConfigFormModel(overrides?: Partial<LeagueConfigFormModel>)
|
||||
};
|
||||
}
|
||||
|
||||
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 mockLeagueFindById = vi.fn();
|
||||
const mockLeagueRepo: ILeagueRepository = {
|
||||
findById: mockLeagueFindById,
|
||||
findAll: vi.fn(),
|
||||
findByOwnerId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
searchByName: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
const mockSeasonFindById = vi.fn();
|
||||
const mockSeasonAdd = vi.fn();
|
||||
const mockSeasonRepo: ISeasonRepository = {
|
||||
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, mockSeasonRepo);
|
||||
|
||||
const config = createLeagueConfigFormModel({
|
||||
basics: {
|
||||
@@ -243,35 +134,10 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
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);
|
||||
expect(result.value!.seasonId).toBeDefined();
|
||||
});
|
||||
|
||||
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',
|
||||
@@ -280,9 +146,11 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
status: 'planned',
|
||||
}).withMaxDrivers(40);
|
||||
|
||||
await seasonRepo.add(sourceSeason);
|
||||
mockLeagueFindById.mockResolvedValue({ id: 'league-1' });
|
||||
mockSeasonFindById.mockResolvedValue(sourceSeason);
|
||||
mockSeasonAdd.mockResolvedValue(undefined);
|
||||
|
||||
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
const useCase = new CreateSeasonForLeagueUseCase(mockLeagueRepo, mockSeasonRepo);
|
||||
|
||||
const command: CreateSeasonForLeagueCommand = {
|
||||
leagueId: 'league-1',
|
||||
@@ -292,20 +160,8 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
};
|
||||
|
||||
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);
|
||||
expect(result.value!.seasonId).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { CreateSponsorUseCase, type CreateSponsorCommand } from './CreateSponsorUseCase';
|
||||
import { CreateSponsorUseCase } from './CreateSponsorUseCase';
|
||||
import type { CreateSponsorCommand } from '../dto/CreateSponsorCommand';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
|
||||
@@ -1,29 +1,52 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GetAllRacesPageDataUseCase } from './GetAllRacesPageDataUseCase';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetAllRacesPageDataUseCase', () => {
|
||||
let mockRaceRepo: { findAll: Mock };
|
||||
let mockLeagueRepo: { findAll: Mock };
|
||||
let mockLogger: Logger;
|
||||
const mockRaceFindAll = vi.fn();
|
||||
const mockRaceRepo: IRaceRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockRaceFindAll,
|
||||
findByLeagueId: vi.fn(),
|
||||
findUpcomingByLeagueId: vi.fn(),
|
||||
findCompletedByLeagueId: vi.fn(),
|
||||
findByStatus: vi.fn(),
|
||||
findByDateRange: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLeagueFindAll = vi.fn();
|
||||
const mockLeagueRepo: ILeagueRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockLeagueFindAll,
|
||||
findByOwnerId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
searchByName: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLogger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRaceRepo = { findAll: vi.fn() };
|
||||
mockLeagueRepo = { findAll: vi.fn() };
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return races and filters data', async () => {
|
||||
const useCase = new GetAllRacesPageDataUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -48,8 +71,8 @@ describe('GetAllRacesPageDataUseCase', () => {
|
||||
const league1 = { id: 'league1', name: 'League One' };
|
||||
const league2 = { id: 'league2', name: 'League Two' };
|
||||
|
||||
mockRaceRepo.findAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueRepo.findAll.mockResolvedValue([league1, league2]);
|
||||
mockRaceFindAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueFindAll.mockResolvedValue([league1, league2]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -95,13 +118,13 @@ describe('GetAllRacesPageDataUseCase', () => {
|
||||
|
||||
it('should return empty result when no races or leagues', async () => {
|
||||
const useCase = new GetAllRacesPageDataUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockRaceRepo.findAll.mockResolvedValue([]);
|
||||
mockLeagueRepo.findAll.mockResolvedValue([]);
|
||||
mockRaceFindAll.mockResolvedValue([]);
|
||||
mockLeagueFindAll.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -123,17 +146,18 @@ describe('GetAllRacesPageDataUseCase', () => {
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetAllRacesPageDataUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const error = new Error('Repository error');
|
||||
mockRaceRepo.findAll.mockRejectedValue(error);
|
||||
mockRaceFindAll.mockRejectedValue(error);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Repository error');
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details.message).toBe('Repository error');
|
||||
});
|
||||
});
|
||||
@@ -8,18 +8,18 @@ import type {
|
||||
AllRacesFilterOptionsViewModel,
|
||||
} from '../presenters/IAllRacesPagePresenter';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@/shared/application/Result';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetAllRacesPageDataUseCase
|
||||
implements AsyncUseCase<void, AllRacesPageResultDTO, string> {
|
||||
implements AsyncUseCase<void, AllRacesPageResultDTO, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<AllRacesPageResultDTO, ApplicationErrorCode<string>>> {
|
||||
async execute(): Promise<Result<AllRacesPageResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetAllRacesPageDataUseCase');
|
||||
try {
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
@@ -69,7 +69,10 @@ export class GetAllRacesPageDataUseCase
|
||||
return Result.ok(viewModel);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetAllRacesPageDataUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err(new RacingDomainValidationError(error instanceof Error ? error.message : 'Unknown error occurred'));
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: error instanceof Error ? error.message : 'Unknown error occurred' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,52 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GetAllRacesUseCase } from './GetAllRacesUseCase';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetAllRacesUseCase', () => {
|
||||
let mockRaceRepo: { findAll: Mock };
|
||||
let mockLeagueRepo: { findAll: Mock };
|
||||
let mockLogger: Logger;
|
||||
const mockRaceFindAll = vi.fn();
|
||||
const mockRaceRepo: IRaceRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockRaceFindAll,
|
||||
findByLeagueId: vi.fn(),
|
||||
findUpcomingByLeagueId: vi.fn(),
|
||||
findCompletedByLeagueId: vi.fn(),
|
||||
findByStatus: vi.fn(),
|
||||
findByDateRange: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLeagueFindAll = vi.fn();
|
||||
const mockLeagueRepo: ILeagueRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockLeagueFindAll,
|
||||
findByOwnerId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
searchByName: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLogger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRaceRepo = { findAll: vi.fn() };
|
||||
mockLeagueRepo = { findAll: vi.fn() };
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return races data', async () => {
|
||||
const useCase = new GetAllRacesUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -44,8 +67,8 @@ describe('GetAllRacesUseCase', () => {
|
||||
const league1 = { id: 'league1', name: 'League One' };
|
||||
const league2 = { id: 'league2', name: 'League Two' };
|
||||
|
||||
mockRaceRepo.findAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueRepo.findAll.mockResolvedValue([league1, league2]);
|
||||
mockRaceFindAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueFindAll.mockResolvedValue([league1, league2]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -71,13 +94,13 @@ describe('GetAllRacesUseCase', () => {
|
||||
|
||||
it('should return empty result when no races or leagues', async () => {
|
||||
const useCase = new GetAllRacesUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockRaceRepo.findAll.mockResolvedValue([]);
|
||||
mockLeagueRepo.findAll.mockResolvedValue([]);
|
||||
mockRaceFindAll.mockResolvedValue([]);
|
||||
mockLeagueFindAll.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -90,17 +113,18 @@ describe('GetAllRacesUseCase', () => {
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetAllRacesUseCase(
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueRepo as unknown as ILeagueRepository,
|
||||
mockRaceRepo,
|
||||
mockLeagueRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const error = new Error('Repository error');
|
||||
mockRaceRepo.findAll.mockRejectedValue(error);
|
||||
mockRaceFindAll.mockRejectedValue(error);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Repository error');
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details.message).toBe('Repository error');
|
||||
});
|
||||
});
|
||||
@@ -2,17 +2,17 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { GetAllRacesResultDTO } from '../presenters/IGetAllRacesPresenter';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@/shared/application/Result';
|
||||
import { RacingDomainValidationError } from '../../domain/errors/RacingDomainError';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetAllRacesUseCase implements AsyncUseCase<void, Result<GetAllRacesResultDTO, RacingDomainValidationError>> {
|
||||
export class GetAllRacesUseCase implements AsyncUseCase<void, GetAllRacesResultDTO, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetAllRacesResultDTO, RacingDomainValidationError>> {
|
||||
async execute(): Promise<Result<GetAllRacesResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetAllRacesUseCase');
|
||||
try {
|
||||
const races = await this.raceRepository.findAll();
|
||||
@@ -35,7 +35,10 @@ export class GetAllRacesUseCase implements AsyncUseCase<void, Result<GetAllRaces
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetAllRacesUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err(new RacingDomainValidationError(error instanceof Error ? error.message : 'Unknown error occurred'));
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: error instanceof Error ? error.message : 'Unknown error occurred' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,49 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GetAllTeamsUseCase } from './GetAllTeamsUseCase';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetAllTeamsUseCase', () => {
|
||||
let mockTeamRepo: { findAll: Mock };
|
||||
let mockTeamMembershipRepo: { countByTeamId: Mock };
|
||||
let mockLogger: Logger;
|
||||
const mockTeamFindAll = vi.fn();
|
||||
const mockTeamRepo: ITeamRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockTeamFindAll,
|
||||
findByLeagueId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
|
||||
const mockTeamMembershipCountByTeamId = vi.fn();
|
||||
const mockTeamMembershipRepo: ITeamMembershipRepository = {
|
||||
getMembership: vi.fn(),
|
||||
getActiveMembershipForDriver: vi.fn(),
|
||||
getTeamMembers: vi.fn(),
|
||||
saveMembership: vi.fn(),
|
||||
removeMembership: vi.fn(),
|
||||
countByTeamId: mockTeamMembershipCountByTeamId,
|
||||
getJoinRequests: vi.fn(),
|
||||
saveJoinRequest: vi.fn(),
|
||||
removeJoinRequest: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLogger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockTeamRepo = { findAll: vi.fn() };
|
||||
mockTeamMembershipRepo = { countByTeamId: vi.fn() };
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return teams data', async () => {
|
||||
const useCase = new GetAllTeamsUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockTeamMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockTeamMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -46,8 +66,8 @@ describe('GetAllTeamsUseCase', () => {
|
||||
createdAt: new Date('2023-01-02T00:00:00Z'),
|
||||
};
|
||||
|
||||
mockTeamRepo.findAll.mockResolvedValue([team1, team2]);
|
||||
mockTeamMembershipRepo.countByTeamId.mockImplementation((id: string) => Promise.resolve(id === 'team1' ? 5 : 3));
|
||||
mockTeamFindAll.mockResolvedValue([team1, team2]);
|
||||
mockTeamMembershipCountByTeamId.mockImplementation((id: string) => Promise.resolve(id === 'team1' ? 5 : 3));
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -80,12 +100,12 @@ describe('GetAllTeamsUseCase', () => {
|
||||
|
||||
it('should return empty result when no teams', async () => {
|
||||
const useCase = new GetAllTeamsUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockTeamMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockTeamMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockTeamRepo.findAll.mockResolvedValue([]);
|
||||
mockTeamFindAll.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -97,17 +117,18 @@ describe('GetAllTeamsUseCase', () => {
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetAllTeamsUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockTeamMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockTeamMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const error = new Error('Repository error');
|
||||
mockTeamRepo.findAll.mockRejectedValue(error);
|
||||
mockTeamFindAll.mockRejectedValue(error);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Repository error');
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details.message).toBe('Repository error');
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,20 @@ import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { AllTeamsResultDTO } from '../presenters/IAllTeamsPresenter';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@/shared/application/Result';
|
||||
import { RacingDomainValidationError } from '../../domain/errors/RacingDomainError';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
*/
|
||||
export class GetAllTeamsUseCase implements AsyncUseCase<void, Result<AllTeamsResultDTO, RacingDomainValidationError>> {
|
||||
export class GetAllTeamsUseCase implements AsyncUseCase<void, AllTeamsResultDTO, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<AllTeamsResultDTO, RacingDomainValidationError>> {
|
||||
async execute(): Promise<Result<AllTeamsResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetAllTeamsUseCase');
|
||||
|
||||
try {
|
||||
@@ -45,7 +45,10 @@ export class GetAllTeamsUseCase implements AsyncUseCase<void, Result<AllTeamsRes
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving all teams', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err(new RacingDomainValidationError(error instanceof Error ? error.message : 'Unknown error occurred'));
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: error instanceof Error ? error.message : 'Unknown error occurred' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,43 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GetDriverTeamUseCase } from './GetDriverTeamUseCase';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetDriverTeamUseCase', () => {
|
||||
let mockTeamRepo: ITeamRepository;
|
||||
let mockMembershipRepo: ITeamMembershipRepository;
|
||||
let mockLogger: Logger;
|
||||
let mockFindById: Mock;
|
||||
let mockGetActiveMembershipForDriver: Mock;
|
||||
const mockFindById = vi.fn();
|
||||
const mockGetActiveMembershipForDriver = vi.fn();
|
||||
const mockTeamRepo: ITeamRepository = {
|
||||
findById: mockFindById,
|
||||
findAll: vi.fn(),
|
||||
findByLeagueId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
|
||||
const mockMembershipRepo: ITeamMembershipRepository = {
|
||||
getActiveMembershipForDriver: mockGetActiveMembershipForDriver,
|
||||
getMembership: vi.fn(),
|
||||
getTeamMembers: vi.fn(),
|
||||
saveMembership: vi.fn(),
|
||||
removeMembership: vi.fn(),
|
||||
getJoinRequests: vi.fn(),
|
||||
countByTeamId: vi.fn(),
|
||||
saveJoinRequest: vi.fn(),
|
||||
removeJoinRequest: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLogger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockFindById = vi.fn();
|
||||
mockGetActiveMembershipForDriver = vi.fn();
|
||||
mockTeamRepo = {
|
||||
findById: mockFindById,
|
||||
findAll: vi.fn(),
|
||||
findByLeagueId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
} as ITeamRepository;
|
||||
mockMembershipRepo = {
|
||||
getActiveMembershipForDriver: mockGetActiveMembershipForDriver,
|
||||
getMembership: vi.fn(),
|
||||
getTeamMembers: vi.fn(),
|
||||
saveMembership: vi.fn(),
|
||||
removeMembership: vi.fn(),
|
||||
getJoinRequests: vi.fn(),
|
||||
getMembershipsForDriver: vi.fn(),
|
||||
countByTeamId: vi.fn(),
|
||||
saveJoinRequest: vi.fn(),
|
||||
removeJoinRequest: vi.fn(),
|
||||
} as ITeamMembershipRepository;
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return driver team data when membership and team exist', async () => {
|
||||
@@ -69,8 +66,8 @@ describe('GetDriverTeamUseCase', () => {
|
||||
|
||||
it('should return error when no active membership found', async () => {
|
||||
const useCase = new GetDriverTeamUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -87,8 +84,8 @@ describe('GetDriverTeamUseCase', () => {
|
||||
|
||||
it('should return error when team not found', async () => {
|
||||
const useCase = new GetDriverTeamUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -107,8 +104,8 @@ describe('GetDriverTeamUseCase', () => {
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetDriverTeamUseCase(
|
||||
mockTeamRepo as unknown as ITeamRepository,
|
||||
mockMembershipRepo as unknown as ITeamMembershipRepository,
|
||||
mockTeamRepo,
|
||||
mockMembershipRepo,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GetDriversLeaderboardUseCase } from './GetDriversLeaderboardUseCase';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRankingService } from '../../domain/services/IRankingService';
|
||||
@@ -7,31 +7,48 @@ import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetDriversLeaderboardUseCase', () => {
|
||||
let mockDriverRepo: { findAll: Mock };
|
||||
let mockRankingService: { getAllDriverRankings: Mock };
|
||||
let mockDriverStatsService: { getDriverStats: Mock };
|
||||
let mockImageService: { getDriverAvatar: Mock };
|
||||
let mockLogger: Logger;
|
||||
const mockDriverFindAll = vi.fn();
|
||||
const mockDriverRepo: IDriverRepository = {
|
||||
findById: vi.fn(),
|
||||
findAll: mockDriverFindAll,
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
|
||||
const mockRankingGetAllDriverRankings = vi.fn();
|
||||
const mockRankingService: IRankingService = {
|
||||
getAllDriverRankings: mockRankingGetAllDriverRankings,
|
||||
};
|
||||
|
||||
const mockDriverStatsGetDriverStats = vi.fn();
|
||||
const mockDriverStatsService: IDriverStatsService = {
|
||||
getDriverStats: mockDriverStatsGetDriverStats,
|
||||
};
|
||||
|
||||
const mockImageGetDriverAvatar = vi.fn();
|
||||
const mockImageService: IImageServicePort = {
|
||||
getDriverAvatar: mockImageGetDriverAvatar,
|
||||
};
|
||||
|
||||
const mockLogger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockDriverRepo = { findAll: vi.fn() };
|
||||
mockRankingService = { getAllDriverRankings: vi.fn() };
|
||||
mockDriverStatsService = { getDriverStats: vi.fn() };
|
||||
mockImageService = { getDriverAvatar: vi.fn() };
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return drivers leaderboard data', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
mockDriverRepo as unknown as IDriverRepository,
|
||||
mockRankingService as unknown as IRankingService,
|
||||
mockDriverStatsService as unknown as IDriverStatsService,
|
||||
mockImageService as unknown as IImageServicePort,
|
||||
mockDriverRepo,
|
||||
mockRankingService,
|
||||
mockDriverStatsService,
|
||||
mockImageService,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
@@ -41,14 +58,14 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
const stats1 = { wins: 5, losses: 2 };
|
||||
const stats2 = { wins: 3, losses: 1 };
|
||||
|
||||
mockDriverRepo.findAll.mockResolvedValue([driver1, driver2]);
|
||||
mockRankingService.getAllDriverRankings.mockReturnValue(rankings);
|
||||
mockDriverStatsService.getDriverStats.mockImplementation((id) => {
|
||||
mockDriverFindAll.mockResolvedValue([driver1, driver2]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
|
||||
mockDriverStatsGetDriverStats.mockImplementation((id) => {
|
||||
if (id === 'driver1') return stats1;
|
||||
if (id === 'driver2') return stats2;
|
||||
return null;
|
||||
});
|
||||
mockImageService.getDriverAvatar.mockImplementation((id) => `avatar-${id}`);
|
||||
mockImageGetDriverAvatar.mockImplementation((id) => `avatar-${id}`);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -59,21 +76,19 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
stats: { driver1: stats1, driver2: stats2 },
|
||||
avatarUrls: { driver1: 'avatar-driver1', driver2: 'avatar-driver2' },
|
||||
});
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith('Executing GetDriversLeaderboardUseCase');
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith('Successfully retrieved drivers leaderboard.');
|
||||
});
|
||||
|
||||
it('should return empty result when no drivers', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
mockDriverRepo as unknown as IDriverRepository,
|
||||
mockRankingService as unknown as IRankingService,
|
||||
mockDriverStatsService as unknown as IDriverStatsService,
|
||||
mockImageService as unknown as IImageServicePort,
|
||||
mockDriverRepo,
|
||||
mockRankingService,
|
||||
mockDriverStatsService,
|
||||
mockImageService,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockDriverRepo.findAll.mockResolvedValue([]);
|
||||
mockRankingService.getAllDriverRankings.mockReturnValue({});
|
||||
mockDriverFindAll.mockResolvedValue([]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue({});
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -88,20 +103,20 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
|
||||
it('should handle drivers without stats', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
mockDriverRepo as unknown as IDriverRepository,
|
||||
mockRankingService as unknown as IRankingService,
|
||||
mockDriverStatsService as unknown as IDriverStatsService,
|
||||
mockImageService as unknown as IImageServicePort,
|
||||
mockDriverRepo,
|
||||
mockRankingService,
|
||||
mockDriverStatsService,
|
||||
mockImageService,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const driver1 = { id: 'driver1', name: 'Driver One' };
|
||||
const rankings = { driver1: 1 };
|
||||
|
||||
mockDriverRepo.findAll.mockResolvedValue([driver1]);
|
||||
mockRankingService.getAllDriverRankings.mockReturnValue(rankings);
|
||||
mockDriverStatsService.getDriverStats.mockReturnValue(null);
|
||||
mockImageService.getDriverAvatar.mockReturnValue('avatar-driver1');
|
||||
mockDriverFindAll.mockResolvedValue([driver1]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
|
||||
mockDriverStatsGetDriverStats.mockReturnValue(null);
|
||||
mockImageGetDriverAvatar.mockReturnValue('avatar-driver1');
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
@@ -116,20 +131,20 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
mockDriverRepo as unknown as IDriverRepository,
|
||||
mockRankingService as unknown as IRankingService,
|
||||
mockDriverStatsService as unknown as IDriverStatsService,
|
||||
mockImageService as unknown as IImageServicePort,
|
||||
mockDriverRepo,
|
||||
mockRankingService,
|
||||
mockDriverStatsService,
|
||||
mockImageService,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const error = new Error('Repository error');
|
||||
mockDriverRepo.findAll.mockRejectedValue(error);
|
||||
mockDriverFindAll.mockRejectedValue(error);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Repository error');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('Error executing GetDriversLeaderboardUseCase', error);
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details.message).toBe('Repository error');
|
||||
});
|
||||
});
|
||||
@@ -4,15 +4,15 @@ import type { IDriverStatsService } from '../../domain/services/IDriverStatsServ
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { DriversLeaderboardResultDTO } from '../presenters/IDriversLeaderboardPresenter';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@/shared/application/Result';
|
||||
import { RacingDomainValidationError } from '../../domain/errors/RacingDomainError';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving driver leaderboard data.
|
||||
* Orchestrates domain logic and returns result.
|
||||
*/
|
||||
export class GetDriversLeaderboardUseCase
|
||||
implements AsyncUseCase<void, Result<DriversLeaderboardResultDTO, RacingDomainValidationError>>
|
||||
implements AsyncUseCase<void, DriversLeaderboardResultDTO, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
@@ -22,7 +22,7 @@ export class GetDriversLeaderboardUseCase
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<DriversLeaderboardResultDTO, RacingDomainValidationError>> {
|
||||
async execute(): Promise<Result<DriversLeaderboardResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetDriversLeaderboardUseCase');
|
||||
try {
|
||||
const drivers = await this.driverRepository.findAll();
|
||||
@@ -50,7 +50,10 @@ export class GetDriversLeaderboardUseCase
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetDriversLeaderboardUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err(new RacingDomainValidationError(error instanceof Error ? error.message : 'Unknown error occurred'));
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: error instanceof Error ? error.message : 'Unknown error occurred' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
|
||||
describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const mockStandingFindByLeagueId = vi.fn();
|
||||
const mockResultFindByDriverIdAndLeagueId = vi.fn();
|
||||
const mockPenaltyFindByRaceId = vi.fn();
|
||||
const mockRaceFindByLeagueId = vi.fn();
|
||||
const mockDriverRatingGetRating = vi.fn();
|
||||
|
||||
let useCase: GetLeagueDriverSeasonStatsUseCase;
|
||||
let standingRepository: IStandingRepository;
|
||||
let resultRepository: IResultRepository;
|
||||
@@ -16,20 +22,38 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
standingRepository = {
|
||||
findByLeagueId: vi.fn(),
|
||||
} as any;
|
||||
resultRepository = {
|
||||
findByLeagueId: mockStandingFindByLeagueId,
|
||||
findByDriverIdAndLeagueId: vi.fn(),
|
||||
} as any;
|
||||
findAll: vi.fn(),
|
||||
save: vi.fn(),
|
||||
saveMany: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
deleteByLeagueId: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
recalculate: vi.fn(),
|
||||
};
|
||||
resultRepository = {
|
||||
findByDriverIdAndLeagueId: mockResultFindByDriverIdAndLeagueId,
|
||||
};
|
||||
penaltyRepository = {
|
||||
findByRaceId: vi.fn(),
|
||||
} as any;
|
||||
findByRaceId: mockPenaltyFindByRaceId,
|
||||
};
|
||||
raceRepository = {
|
||||
findByLeagueId: vi.fn(),
|
||||
} as any;
|
||||
findById: vi.fn(),
|
||||
findAll: vi.fn(),
|
||||
findByLeagueId: mockRaceFindByLeagueId,
|
||||
findUpcomingByLeagueId: vi.fn(),
|
||||
findCompletedByLeagueId: vi.fn(),
|
||||
findByStatus: vi.fn(),
|
||||
findByDateRange: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
driverRatingPort = {
|
||||
getRating: vi.fn(),
|
||||
} as any;
|
||||
getRating: mockDriverRatingGetRating,
|
||||
};
|
||||
|
||||
useCase = new GetLeagueDriverSeasonStatsUseCase(
|
||||
standingRepository,
|
||||
@@ -69,7 +93,7 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
const dto = result.value!;
|
||||
expect(dto.leagueId).toBe('league-1');
|
||||
expect(dto.standings).toEqual([
|
||||
{ driverId: 'driver-1', position: 1, points: 100, racesCompleted: 5 },
|
||||
@@ -97,7 +121,7 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
const dto = result.value!;
|
||||
expect(dto.penalties.size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -124,7 +124,7 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const viewModel = result.unwrap();
|
||||
const viewModel = result.value!;
|
||||
expect(viewModel).toEqual(mockViewModel);
|
||||
expect(presenter.reset).toHaveBeenCalled();
|
||||
expect(presenter.present).toHaveBeenCalledWith({
|
||||
@@ -145,7 +145,7 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
expect(error.details.message).toBe('League with id league-1 not found');
|
||||
expect(error.details!.message).toBe('League with id league-1 not found');
|
||||
});
|
||||
|
||||
it('should handle no active season', async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface GetLeagueSeasonsUseCaseParams {
|
||||
export class GetLeagueSeasonsUseCase {
|
||||
constructor(private readonly seasonRepository: ISeasonRepository) {}
|
||||
|
||||
async execute(params: GetLeagueSeasonsUseCaseParams): Promise<Result<GetLeagueSeasonsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(params: GetLeagueSeasonsUseCaseParams): Promise<Result<GetLeagueSeasonsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
try {
|
||||
const seasons = await this.seasonRepository.findByLeagueId(params.leagueId);
|
||||
const activeCount = seasons.filter(s => s.status === 'active').length;
|
||||
@@ -19,15 +19,15 @@ export class GetLeagueSeasonsUseCase {
|
||||
seasonId: s.id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
startDate: s.startDate,
|
||||
endDate: s.endDate,
|
||||
startDate: s.startDate ?? new Date(),
|
||||
endDate: s.endDate ?? new Date(),
|
||||
isPrimary: false,
|
||||
isParallelActive: s.status === 'active' && activeCount > 1
|
||||
}))
|
||||
};
|
||||
return Result.ok(viewModel);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch seasons' });
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to fetch seasons' } });
|
||||
}
|
||||
}
|
||||
}
|
||||
192
core/racing/application/use-cases/GetRaceWithSOFUseCase.test.ts
Normal file
192
core/racing/application/use-cases/GetRaceWithSOFUseCase.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { GetRaceWithSOFUseCase } from './GetRaceWithSOFUseCase';
|
||||
import { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { SessionType } from '../../domain/value-objects/SessionType';
|
||||
|
||||
describe('GetRaceWithSOFUseCase', () => {
|
||||
let useCase: GetRaceWithSOFUseCase;
|
||||
let raceRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
let registrationRepository: {
|
||||
getRegisteredDrivers: Mock;
|
||||
};
|
||||
let resultRepository: {
|
||||
findByRaceId: Mock;
|
||||
};
|
||||
let driverRatingProvider: {
|
||||
getRatings: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
raceRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
registrationRepository = {
|
||||
getRegisteredDrivers: vi.fn(),
|
||||
};
|
||||
resultRepository = {
|
||||
findByRaceId: vi.fn(),
|
||||
};
|
||||
driverRatingProvider = {
|
||||
getRatings: vi.fn(),
|
||||
};
|
||||
useCase = new GetRaceWithSOFUseCase(
|
||||
raceRepository as unknown as IRaceRepository,
|
||||
registrationRepository as unknown as IRaceRegistrationRepository,
|
||||
resultRepository as unknown as IResultRepository,
|
||||
driverRatingProvider as unknown as DriverRatingProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when race not found', async () => {
|
||||
raceRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'RACE_NOT_FOUND' });
|
||||
});
|
||||
|
||||
it('should return race with stored SOF when available', async () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
sessionType: SessionType.main(),
|
||||
status: 'scheduled',
|
||||
strengthOfField: 1500,
|
||||
registeredCount: 10,
|
||||
maxParticipants: 20,
|
||||
});
|
||||
|
||||
raceRepository.findById.mockResolvedValue(race);
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2', 'driver-3', 'driver-4', 'driver-5', 'driver-6', 'driver-7', 'driver-8', 'driver-9', 'driver-10']);
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.raceId).toBe('race-1');
|
||||
expect(dto.leagueId).toBe('league-1');
|
||||
expect(dto.strengthOfField).toBe(1500);
|
||||
expect(dto.registeredCount).toBe(10);
|
||||
expect(dto.maxParticipants).toBe(20);
|
||||
expect(dto.participantCount).toBe(10);
|
||||
expect(dto.sessionType).toBe('main');
|
||||
expect(dto.status).toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should calculate SOF for upcoming race using registrations', async () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
sessionType: SessionType.main(),
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
raceRepository.findById.mockResolvedValue(race);
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([
|
||||
['driver-1', 1400],
|
||||
['driver-2', 1600],
|
||||
]));
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.strengthOfField).toBe(1500); // average
|
||||
expect(dto.participantCount).toBe(2);
|
||||
expect(registrationRepository.getRegisteredDrivers).toHaveBeenCalledWith('race-1');
|
||||
expect(resultRepository.findByRaceId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should calculate SOF for completed race using results', async () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
sessionType: SessionType.main(),
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
raceRepository.findById.mockResolvedValue(race);
|
||||
resultRepository.findByRaceId.mockResolvedValue([
|
||||
{ driverId: 'driver-1' },
|
||||
{ driverId: 'driver-2' },
|
||||
]);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([
|
||||
['driver-1', 1400],
|
||||
['driver-2', 1600],
|
||||
]));
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.strengthOfField).toBe(1500);
|
||||
expect(dto.participantCount).toBe(2);
|
||||
expect(resultRepository.findByRaceId).toHaveBeenCalledWith('race-1');
|
||||
expect(registrationRepository.getRegisteredDrivers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle missing ratings gracefully', async () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
sessionType: SessionType.main(),
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
raceRepository.findById.mockResolvedValue(race);
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([
|
||||
['driver-1', 1400],
|
||||
// driver-2 missing
|
||||
]));
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.strengthOfField).toBe(1400); // only one rating
|
||||
expect(dto.participantCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should return null SOF when no participants', async () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
sessionType: SessionType.main(),
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
raceRepository.findById.mockResolvedValue(race);
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.strengthOfField).toBe(null);
|
||||
expect(dto.participantCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -92,8 +92,8 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryPa
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car ?? '',
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType as string,
|
||||
status: race.status as string,
|
||||
sessionType: race.sessionType.props,
|
||||
status: race.status,
|
||||
strengthOfField,
|
||||
registeredCount: race.registeredCount ?? participantIds.length,
|
||||
maxParticipants: race.maxParticipants ?? participantIds.length,
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
InMemorySeasonRepository,
|
||||
} from '@core/racing/infrastructure/repositories/InMemoryScoringRepositories';
|
||||
import { Season } from '@core/racing/domain/entities/Season';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import {
|
||||
GetSeasonDetailsUseCase,
|
||||
} from '@core/racing/application/use-cases/GetSeasonDetailsUseCase';
|
||||
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;
|
||||
}
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { GetSeasonDetailsUseCase } from './GetSeasonDetailsUseCase';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
|
||||
describe('GetSeasonDetailsUseCase', () => {
|
||||
it('returns full details for a season belonging to the league', async () => {
|
||||
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
||||
const seasonRepo = new InMemorySeasonRepository(logger);
|
||||
let useCase: GetSeasonDetailsUseCase;
|
||||
let leagueRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
let seasonRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
leagueRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
seasonRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
useCase = new GetSeasonDetailsUseCase(
|
||||
leagueRepository as unknown as ILeagueRepository,
|
||||
seasonRepository as unknown as ISeasonRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns full details for a season belonging to the league', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
@@ -39,9 +36,8 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
status: 'planned',
|
||||
}).withMaxDrivers(24);
|
||||
|
||||
await seasonRepo.add(season);
|
||||
|
||||
const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo);
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockResolvedValue(season);
|
||||
|
||||
const result = await useCase.execute({
|
||||
leagueId: 'league-1',
|
||||
@@ -49,7 +45,7 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.value;
|
||||
const dto = result.unwrap();
|
||||
expect(dto.seasonId).toBe('season-1');
|
||||
expect(dto.leagueId).toBe('league-1');
|
||||
expect(dto.gameId).toBe('iracing');
|
||||
@@ -57,4 +53,61 @@ describe('GetSeasonDetailsUseCase', () => {
|
||||
expect(dto.status).toBe('planned');
|
||||
expect(dto.maxDrivers).toBe(24);
|
||||
});
|
||||
|
||||
it('returns error when league not found', async () => {
|
||||
leagueRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'LEAGUE_NOT_FOUND',
|
||||
details: { message: 'League not found: league-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when season not found', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'SEASON_NOT_FOUND',
|
||||
details: { message: 'Season season-1 does not belong to league league-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when season belongs to different league', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-2',
|
||||
gameId: 'iracing',
|
||||
name: 'Season',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockResolvedValue(season);
|
||||
|
||||
const result = await useCase.execute({
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'SEASON_NOT_FOUND',
|
||||
details: { message: 'Season season-1 does not belong to league league-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
InMemorySeasonRepository,
|
||||
} from '@core/racing/infrastructure/repositories/InMemoryScoringRepositories';
|
||||
import { Season } from '@core/racing/domain/entities/Season';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import {
|
||||
ListSeasonsForLeagueUseCase,
|
||||
} from '@core/racing/application/use-cases/ListSeasonsForLeagueUseCase';
|
||||
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;
|
||||
}
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { ListSeasonsForLeagueUseCase } from './ListSeasonsForLeagueUseCase';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
|
||||
describe('ListSeasonsForLeagueUseCase', () => {
|
||||
let useCase: ListSeasonsForLeagueUseCase;
|
||||
let leagueRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
let seasonRepository: {
|
||||
listByLeague: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
leagueRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
seasonRepository = {
|
||||
listByLeague: vi.fn(),
|
||||
};
|
||||
useCase = new ListSeasonsForLeagueUseCase(
|
||||
leagueRepository as unknown as ILeagueRepository,
|
||||
seasonRepository as unknown as ISeasonRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('lists seasons for a league with summaries', async () => {
|
||||
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
||||
const seasonRepo = new InMemorySeasonRepository(logger);
|
||||
const league = { id: 'league-1' };
|
||||
const seasons = [
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season One',
|
||||
status: 'planned',
|
||||
}),
|
||||
Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season Two',
|
||||
status: 'active',
|
||||
}),
|
||||
];
|
||||
|
||||
const s1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season One',
|
||||
status: 'planned',
|
||||
});
|
||||
const s2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season Two',
|
||||
status: 'active',
|
||||
});
|
||||
const sOtherLeague = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-2',
|
||||
gameId: 'iracing',
|
||||
name: 'Season Other',
|
||||
status: 'planned',
|
||||
});
|
||||
|
||||
await seasonRepo.add(s1);
|
||||
await seasonRepo.add(s2);
|
||||
await seasonRepo.add(sOtherLeague);
|
||||
|
||||
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo);
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.listByLeague.mockResolvedValue(seasons);
|
||||
|
||||
const result = await useCase.execute({ leagueId: 'league-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value.items.map((i) => i.seasonId).sort()).toEqual([
|
||||
const dto = result.unwrap();
|
||||
expect(dto.items.map((i) => i.seasonId).sort()).toEqual([
|
||||
'season-1',
|
||||
'season-2',
|
||||
]);
|
||||
expect(result.value.items.every((i) => i.leagueId === 'league-1')).toBe(true);
|
||||
expect(dto.items.every((i) => i.leagueId === 'league-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error when league not found', async () => {
|
||||
leagueRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({ leagueId: 'league-1' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'LEAGUE_NOT_FOUND',
|
||||
details: { message: 'League not found: league-1' },
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,37 +1,83 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
InMemorySeasonRepository,
|
||||
} from '@core/racing/infrastructure/repositories/InMemoryScoringRepositories';
|
||||
import { Season } from '@core/racing/domain/entities/Season';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import {
|
||||
ManageSeasonLifecycleUseCase,
|
||||
type ManageSeasonLifecycleCommand,
|
||||
} from '@core/racing/application/use-cases/ManageSeasonLifecycleUseCase';
|
||||
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;
|
||||
}
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { ManageSeasonLifecycleUseCase, type ManageSeasonLifecycleCommand } from './ManageSeasonLifecycleUseCase';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
|
||||
describe('ManageSeasonLifecycleUseCase', () => {
|
||||
function setupLifecycleTest() {
|
||||
const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
|
||||
const seasonRepo = new InMemorySeasonRepository(logger);
|
||||
let useCase: ManageSeasonLifecycleUseCase;
|
||||
let leagueRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
let seasonRepository: {
|
||||
findById: Mock;
|
||||
update: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
leagueRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
seasonRepository = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
useCase = new ManageSeasonLifecycleUseCase(
|
||||
leagueRepository as unknown as ILeagueRepository,
|
||||
seasonRepository as unknown as ISeasonRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('applies activate → complete → archive transitions and persists state', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
let currentSeason = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Lifecycle Season',
|
||||
status: 'planned',
|
||||
});
|
||||
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockImplementation(() => Promise.resolve(currentSeason));
|
||||
seasonRepository.update.mockImplementation((s) => {
|
||||
currentSeason = s;
|
||||
return Promise.resolve(s);
|
||||
});
|
||||
|
||||
const activateCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: currentSeason.id,
|
||||
transition: 'activate',
|
||||
};
|
||||
|
||||
const activated = await useCase.execute(activateCommand);
|
||||
expect(activated.isOk()).toBe(true);
|
||||
expect(activated.unwrap().status).toBe('active');
|
||||
|
||||
const completeCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: currentSeason.id,
|
||||
transition: 'complete',
|
||||
};
|
||||
|
||||
const completed = await useCase.execute(completeCommand);
|
||||
expect(completed.isOk()).toBe(true);
|
||||
expect(completed.unwrap().status).toBe('completed');
|
||||
|
||||
const archiveCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: currentSeason.id,
|
||||
transition: 'archive',
|
||||
};
|
||||
|
||||
const archived = await useCase.execute(archiveCommand);
|
||||
expect(archived.isOk()).toBe(true);
|
||||
expect(archived.unwrap().status).toBe('archived');
|
||||
});
|
||||
|
||||
it('propagates domain invariant errors for invalid transitions', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
@@ -40,52 +86,8 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
status: 'planned',
|
||||
});
|
||||
|
||||
seasonRepo.seed(season);
|
||||
|
||||
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);
|
||||
|
||||
return { leagueRepo, seasonRepo, useCase, season };
|
||||
}
|
||||
|
||||
it('applies activate → complete → archive transitions and persists state', async () => {
|
||||
const { useCase, seasonRepo, season } = setupLifecycleTest();
|
||||
|
||||
const activateCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: season.id,
|
||||
transition: 'activate',
|
||||
};
|
||||
|
||||
const activated = await useCase.execute(activateCommand);
|
||||
expect(activated.isOk()).toBe(true);
|
||||
expect(activated.value.status).toBe('active');
|
||||
|
||||
const completeCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: season.id,
|
||||
transition: 'complete',
|
||||
};
|
||||
|
||||
const completed = await useCase.execute(completeCommand);
|
||||
expect(completed.isOk()).toBe(true);
|
||||
expect(completed.value.status).toBe('completed');
|
||||
|
||||
const archiveCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: season.id,
|
||||
transition: 'archive',
|
||||
};
|
||||
|
||||
const archived = await useCase.execute(archiveCommand);
|
||||
expect(archived.isOk()).toBe(true);
|
||||
expect(archived.value.status).toBe('archived');
|
||||
|
||||
const persisted = await seasonRepo.findById(season.id);
|
||||
expect(persisted!.status).toBe('archived');
|
||||
});
|
||||
|
||||
it('propagates domain invariant errors for invalid transitions', async () => {
|
||||
const { useCase, seasonRepo, season } = setupLifecycleTest();
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockResolvedValue(season);
|
||||
|
||||
const completeCommand: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
@@ -95,9 +97,42 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
|
||||
const result = await useCase.execute(completeCommand);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.error.code).toBe('INVALID_TRANSITION');
|
||||
expect(result.unwrapErr().code).toEqual('INVALID_TRANSITION');
|
||||
});
|
||||
|
||||
const persisted = await seasonRepo.findById(season.id);
|
||||
expect(persisted!.status).toBe('planned');
|
||||
it('returns error when league not found', async () => {
|
||||
leagueRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
transition: 'activate',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'LEAGUE_NOT_FOUND',
|
||||
details: { message: 'League not found: league-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when season not found', async () => {
|
||||
const league = { id: 'league-1' };
|
||||
leagueRepository.findById.mockResolvedValue(league);
|
||||
seasonRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: ManageSeasonLifecycleCommand = {
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
transition: 'activate',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'SEASON_NOT_FOUND',
|
||||
details: { message: 'Season season-1 does not belong to league league-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
|
||||
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import type { JoinRequest } from '@core/racing/domain/entities/LeagueMembership';
|
||||
|
||||
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private memberships: LeagueMembership[] = [];
|
||||
private joinRequests: JoinRequest[] = [];
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
(m) => m.leagueId === leagueId && m.driverId === driverId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
||||
return this.memberships.filter(
|
||||
(m) => m.leagueId === leagueId && m.status === 'active',
|
||||
);
|
||||
}
|
||||
|
||||
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
|
||||
return this.joinRequests.filter(
|
||||
(r) => r.leagueId === leagueId,
|
||||
);
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
const existingIndex = this.memberships.findIndex(
|
||||
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
this.memberships[existingIndex] = membership;
|
||||
} else {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(leagueId: string, driverId: string): Promise<void> {
|
||||
this.memberships = this.memberships.filter(
|
||||
(m) => !(m.leagueId === leagueId && m.driverId === driverId),
|
||||
);
|
||||
}
|
||||
|
||||
async saveJoinRequest(request: JoinRequest): Promise<JoinRequest> {
|
||||
this.joinRequests.push(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
this.joinRequests = this.joinRequests.filter(
|
||||
(r) => r.id !== requestId,
|
||||
);
|
||||
}
|
||||
|
||||
seedMembership(membership: LeagueMembership): void {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
|
||||
getAllMemberships(): LeagueMembership[] {
|
||||
return [...this.memberships];
|
||||
}
|
||||
}
|
||||
|
||||
describe('Membership use-cases', () => {
|
||||
describe('JoinLeagueUseCase', () => {
|
||||
let repository: InMemoryLeagueMembershipRepository;
|
||||
let useCase: JoinLeagueUseCase;
|
||||
let logger: {
|
||||
debug: Mock;
|
||||
info: Mock;
|
||||
warn: Mock;
|
||||
error: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
repository = new InMemoryLeagueMembershipRepository();
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
useCase = new JoinLeagueUseCase(
|
||||
repository as unknown as ILeagueMembershipRepository,
|
||||
logger as unknown as Logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates an active member when driver has no membership', async () => {
|
||||
const leagueId = 'league-1';
|
||||
const driverId = 'driver-1';
|
||||
|
||||
const result = await useCase.execute({ leagueId, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const membership = result.unwrap();
|
||||
expect(membership.leagueId).toBe(leagueId);
|
||||
expect(membership.driverId).toBe(driverId);
|
||||
expect(membership.role).toBe('member');
|
||||
expect(membership.status).toBe('active');
|
||||
expect(membership.joinedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns error when driver already has membership for league', async () => {
|
||||
const leagueId = 'league-1';
|
||||
const driverId = 'driver-1';
|
||||
|
||||
repository.seedMembership(LeagueMembership.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
}));
|
||||
|
||||
const result = await useCase.execute({ leagueId, driverId });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'ALREADY_MEMBER',
|
||||
details: { message: 'Already a member or have a pending request' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,624 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Result } from '@core/racing/domain/entities/Result';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
|
||||
import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
|
||||
import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
|
||||
|
||||
class InMemoryRaceRepository implements IRaceRepository {
|
||||
private races = new Map<string, Race>();
|
||||
|
||||
constructor(races: Race[]) {
|
||||
for (const race of races) {
|
||||
this.races.set(race.id, race);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Race | null> {
|
||||
return this.races.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Race[]> {
|
||||
return [...this.races.values()];
|
||||
}
|
||||
|
||||
async findByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findUpcomingByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findCompletedByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByStatus(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDateRange(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(race: Race): Promise<Race> {
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
}
|
||||
|
||||
async update(race: Race): Promise<Race> {
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.races.delete(id);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return this.races.has(id);
|
||||
}
|
||||
|
||||
getStored(id: string): Race | null {
|
||||
return this.races.get(id) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues = new Map<string, League>();
|
||||
|
||||
constructor(leagues: League[]) {
|
||||
for (const league of leagues) {
|
||||
this.leagues.set(league.id, league);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<League | null> {
|
||||
return this.leagues.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
return [...this.leagues.values()];
|
||||
}
|
||||
|
||||
async findByOwnerId(): Promise<League[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(league: League): Promise<League> {
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
}
|
||||
|
||||
async update(league: League): Promise<League> {
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.leagues.delete(id);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return this.leagues.has(id);
|
||||
}
|
||||
|
||||
async searchByName(): Promise<League[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers = new Map<string, Driver>();
|
||||
|
||||
constructor(drivers: Array<{ id: string; name: string; country: string }>) {
|
||||
for (const driver of drivers) {
|
||||
this.drivers.set(driver.id, Driver.create({
|
||||
id: driver.id,
|
||||
iracingId: `iracing-${driver.id}`,
|
||||
name: driver.name,
|
||||
country: driver.country,
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Driver | null> {
|
||||
return this.drivers.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Driver[]> {
|
||||
return [...this.drivers.values()];
|
||||
}
|
||||
|
||||
async findByIds(ids: string[]): Promise<Driver[]> {
|
||||
return ids
|
||||
.map(id => this.drivers.get(id))
|
||||
.filter((d): d is Driver => !!d);
|
||||
}
|
||||
|
||||
async create(): Promise<Driver> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async update(): Promise<Driver> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async exists(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async findByIRacingId(): Promise<Driver | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async existsByIRacingId(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrations = new Map<string, Set<string>>();
|
||||
|
||||
constructor(seed: Array<{ raceId: string; driverId: string }> = []) {
|
||||
for (const { raceId, driverId } of seed) {
|
||||
if (!this.registrations.has(raceId)) {
|
||||
this.registrations.set(raceId, new Set());
|
||||
}
|
||||
this.registrations.get(raceId)!.add(driverId);
|
||||
}
|
||||
}
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
return this.registrations.get(raceId)?.has(driverId) ?? false;
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
return Array.from(this.registrations.get(raceId) ?? []);
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
return this.registrations.get(raceId)?.size ?? 0;
|
||||
}
|
||||
|
||||
async register(registration: { raceId: string; driverId: string }): Promise<void> {
|
||||
if (!this.registrations.has(registration.raceId)) {
|
||||
this.registrations.set(registration.raceId, new Set());
|
||||
}
|
||||
this.registrations.get(registration.raceId)!.add(registration.driverId);
|
||||
}
|
||||
|
||||
async withdraw(raceId: string, driverId: string): Promise<void> {
|
||||
this.registrations.get(raceId)?.delete(driverId);
|
||||
}
|
||||
|
||||
async getDriverRegistrations(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryResultRepository implements IResultRepository {
|
||||
private results = new Map<string, Result[]>();
|
||||
|
||||
constructor(results: Result[]) {
|
||||
for (const result of results) {
|
||||
const list = this.results.get(result.raceId) ?? [];
|
||||
list.push(result);
|
||||
this.results.set(result.raceId, list);
|
||||
}
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<Result[]> {
|
||||
return this.results.get(raceId) ?? [];
|
||||
}
|
||||
|
||||
async findById(): Promise<Result | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDriverId(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDriverIdAndLeagueId(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(result: Result): Promise<Result> {
|
||||
const list = this.results.get(result.raceId) ?? [];
|
||||
list.push(result);
|
||||
this.results.set(result.raceId, list);
|
||||
return result;
|
||||
}
|
||||
|
||||
async createMany(results: Result[]): Promise<Result[]> {
|
||||
for (const result of results) {
|
||||
await this.create(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async update(): Promise<Result> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async deleteByRaceId(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async exists(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async existsByRaceId(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private memberships: LeagueMembership[] = [];
|
||||
|
||||
seedMembership(membership: LeagueMembership): void {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
m => m.leagueId === leagueId && m.driverId === driverId,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
async getLeagueMembers(): Promise<LeagueMembership[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getJoinRequests(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
this.memberships.push(membership);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async saveJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async removeJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
}
|
||||
|
||||
class TestDriverRatingProvider implements DriverRatingProvider {
|
||||
private ratings = new Map<string, number>();
|
||||
|
||||
seed(driverId: string, rating: number): void {
|
||||
this.ratings.set(driverId, rating);
|
||||
}
|
||||
|
||||
getRating(driverId: string): number | null {
|
||||
return this.ratings.get(driverId) ?? null;
|
||||
}
|
||||
|
||||
getRatings(driverIds: string[]): Map<string, number> {
|
||||
const map = new Map<string, number>();
|
||||
for (const id of driverIds) {
|
||||
const rating = this.ratings.get(id);
|
||||
if (rating != null) {
|
||||
map.set(id, rating);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class TestImageService implements IImageServicePort {
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `avatar-${driverId}`;
|
||||
}
|
||||
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `team-logo-${teamId}`;
|
||||
}
|
||||
|
||||
getLeagueCover(leagueId: string): string {
|
||||
return `league-cover-${leagueId}`;
|
||||
}
|
||||
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
return `league-logo-${leagueId}`;
|
||||
}
|
||||
}
|
||||
|
||||
class MockLogger implements Logger {
|
||||
debug = vi.fn();
|
||||
info = vi.fn();
|
||||
warn = vi.fn();
|
||||
error = vi.fn();
|
||||
}
|
||||
|
||||
describe('GetRaceDetailUseCase', () => {
|
||||
it('builds entry list and registration flags for an upcoming race', async () => {
|
||||
// Given (arrange a scheduled race with one registered driver)
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'League for testing',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: league.id,
|
||||
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
track: 'Test Track',
|
||||
car: 'GT3',
|
||||
sessionType: 'main',
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const driverId = 'driver-1';
|
||||
const otherDriverId = 'driver-2';
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([league]);
|
||||
const driverRepo = new InMemoryDriverRepository([
|
||||
{ id: driverId, name: 'Alice Racer', country: 'US' },
|
||||
{ id: otherDriverId, name: 'Bob Driver', country: 'GB' },
|
||||
]);
|
||||
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository([
|
||||
{ raceId: race.id, driverId },
|
||||
{ raceId: race.id, driverId: otherDriverId },
|
||||
]);
|
||||
|
||||
const resultRepo = new InMemoryResultRepository([]);
|
||||
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
membershipRepo.seedMembership(LeagueMembership.create({
|
||||
leagueId: league.id,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
}));
|
||||
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
ratingProvider.seed(driverId, 1500);
|
||||
ratingProvider.seed(otherDriverId, 1600);
|
||||
|
||||
const imageService = new TestImageService();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
);
|
||||
|
||||
// When (execute the query for the current driver)
|
||||
const result = await useCase.execute({ raceId: race.id, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const viewModel = result.unwrap();
|
||||
|
||||
// Then (verify race, league and registration flags)
|
||||
expect(viewModel!.race?.id).toBe(race.id);
|
||||
expect(viewModel!.league?.id).toBe(league.id);
|
||||
expect(viewModel!.registration.isUserRegistered).toBe(true);
|
||||
expect(viewModel!.registration.canRegister).toBe(true);
|
||||
|
||||
// Then (entry list contains both drivers with rating and avatar)
|
||||
expect(viewModel!.entryList.length).toBe(2);
|
||||
const currentDriver = viewModel!.entryList.find(e => e.id === driverId);
|
||||
const otherDriver = viewModel!.entryList.find(e => e.id === otherDriverId);
|
||||
|
||||
expect(currentDriver).toBeDefined();
|
||||
expect(currentDriver!.isCurrentUser).toBe(true);
|
||||
expect(currentDriver!.rating).toBe(1500);
|
||||
expect(currentDriver!.avatarUrl).toBe(`avatar-${driverId}`);
|
||||
|
||||
expect(otherDriver).toBeDefined();
|
||||
expect(otherDriver!.isCurrentUser).toBe(false);
|
||||
expect(otherDriver!.rating).toBe(1600);
|
||||
});
|
||||
|
||||
it('computes rating change for a completed race result using legacy formula', async () => {
|
||||
// Given (a completed race with a result for the current driver)
|
||||
const league = League.create({
|
||||
id: 'league-2',
|
||||
name: 'Results League',
|
||||
description: 'League with results',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
|
||||
const race = Race.create({
|
||||
id: 'race-2',
|
||||
leagueId: league.id,
|
||||
scheduledAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
track: 'Historic Circuit',
|
||||
car: 'LMP2',
|
||||
sessionType: 'main',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
const driverId = 'driver-results';
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([league]);
|
||||
const driverRepo = new InMemoryDriverRepository([
|
||||
{ id: driverId, name: 'Result Hero', country: 'DE' },
|
||||
]);
|
||||
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository([
|
||||
{ raceId: race.id, driverId },
|
||||
]);
|
||||
|
||||
const resultEntity = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
position: 1,
|
||||
fastestLap: 90.123,
|
||||
incidents: 0,
|
||||
startPosition: 3,
|
||||
});
|
||||
|
||||
const resultRepo = new InMemoryResultRepository([resultEntity]);
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
membershipRepo.seedMembership(LeagueMembership.create({
|
||||
leagueId: league.id,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
}));
|
||||
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
ratingProvider.seed(driverId, 2000);
|
||||
|
||||
const imageService = new TestImageService();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
);
|
||||
|
||||
// When (executing the query for the completed race)
|
||||
const result = await useCase.execute({ raceId: race.id, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const viewModel = result.unwrap();
|
||||
expect(viewModel.userResult).not.toBeNull();
|
||||
|
||||
// Then (rating change uses the same formula as the legacy UI)
|
||||
// For P1: baseChange = 25, positionBonus = (20 - 1) * 2 = 38, total = 63
|
||||
expect(viewModel!.userResult!.ratingChange).toBe(63);
|
||||
expect(viewModel!.userResult!.position).toBe(1);
|
||||
expect(viewModel!.userResult!.startPosition).toBe(3);
|
||||
expect(viewModel!.userResult!.positionChange).toBe(2);
|
||||
expect(viewModel!.userResult!.isPodium).toBe(true);
|
||||
expect(viewModel!.userResult!.isClean).toBe(true);
|
||||
});
|
||||
|
||||
it('presents an error when race does not exist', async () => {
|
||||
// Given (no race in the repository)
|
||||
const raceRepo = new InMemoryRaceRepository([]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([]);
|
||||
const driverRepo = new InMemoryDriverRepository([]);
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository();
|
||||
const resultRepo = new InMemoryResultRepository([]);
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
const imageService = new TestImageService();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' });
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'RACE_NOT_FOUND' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('CancelRaceUseCase', () => {
|
||||
it('cancels a scheduled race and persists it via the repository', async () => {
|
||||
// Given (a scheduled race in the repository)
|
||||
const race = Race.create({
|
||||
id: 'cancel-me',
|
||||
leagueId: 'league-cancel',
|
||||
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
track: 'Cancel Circuit',
|
||||
car: 'GT4',
|
||||
sessionType: 'main',
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const logger = new MockLogger();
|
||||
const useCase = new CancelRaceUseCase(raceRepo, logger);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({ raceId: race.id });
|
||||
|
||||
// Then (the stored race is now cancelled)
|
||||
expect(result.isOk()).toBe(true);
|
||||
const updated = raceRepo.getStored(race.id);
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.status).toBe('cancelled');
|
||||
});
|
||||
|
||||
it('returns error when trying to cancel a non-existent race', async () => {
|
||||
// Given
|
||||
const raceRepo = new InMemoryRaceRepository([]);
|
||||
const logger = new MockLogger();
|
||||
const useCase = new CancelRaceUseCase(raceRepo, logger);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({ raceId: 'does-not-exist' });
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'RACE_NOT_FOUND' });
|
||||
});
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export { ImportRaceResultsApiUseCase } from './ImportRaceResultsApiUseCase';
|
||||
export { ImportRaceResultsUseCase } from './ImportRaceResultsUseCase';
|
||||
Reference in New Issue
Block a user