refactor league module (wip)

This commit is contained in:
2025-12-22 15:47:47 +01:00
parent 03dc81b0ba
commit f59e1b13e7
10 changed files with 444 additions and 819 deletions

View File

@@ -35,6 +35,17 @@ import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankin
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { InMemorySocialGraphRepository } from '@core/social/infrastructure/inmemory/InMemorySocialAndFeed';
// Import presenters
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
import { DriverPresenter } from './presenters/DriverPresenter';
import { DriverProfilePresenter } from './presenters/DriverProfilePresenter';
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
// Import types for output ports
import type { UseCaseOutputPort } from '@core/shared/application';
// Define injection tokens
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const RANKING_SERVICE_TOKEN = 'IRankingService';
@@ -47,7 +58,7 @@ export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreference
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
export const LOGGER_TOKEN = 'Logger';
// Use case tokens
export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseCase';
@@ -57,11 +68,61 @@ export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredF
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
// Output port tokens
export const GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN = 'GetDriversLeaderboardOutputPort_TOKEN';
export const GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN = 'GetTotalDriversOutputPort_TOKEN';
export const COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN = 'CompleteDriverOnboardingOutputPort_TOKEN';
export const IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN = 'IsDriverRegisteredForRaceOutputPort_TOKEN';
export const UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN = 'UpdateDriverProfileOutputPort_TOKEN';
export const GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN = 'GetProfileOverviewOutputPort_TOKEN';
export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself
DriverService,
// Presenters
DriversLeaderboardPresenter,
DriverStatsPresenter,
CompleteOnboardingPresenter,
DriverRegistrationStatusPresenter,
DriverPresenter,
DriverProfilePresenter,
// Output ports (point to presenters)
{
provide: GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN,
useExisting: DriversLeaderboardPresenter,
},
{
provide: GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN,
useExisting: DriverStatsPresenter,
},
{
provide: COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN,
useExisting: CompleteOnboardingPresenter,
},
{
provide: IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN,
useExisting: DriverRegistrationStatusPresenter,
},
{
provide: UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN,
useExisting: DriverPresenter,
},
{
provide: GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN,
useExisting: DriverProfilePresenter,
},
// Logger
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Repositories
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{
@@ -115,10 +176,7 @@ export const DriverProviders: Provider[] = [
new InMemorySocialGraphRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
@@ -149,7 +207,8 @@ export const DriverProviders: Provider[] = [
},
{
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, logger),
useFactory: (driverRepo: IDriverRepository, logger: Logger) =>
new UpdateDriverProfileUseCase(driverRepo, logger),
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
@@ -209,4 +268,4 @@ export const DriverProviders: Provider[] = [
RANKING_SERVICE_TOKEN,
],
},
];
];

View File

@@ -1,218 +1,31 @@
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import type { Driver } from '@core/racing/domain/entities/Driver';
import { GetDriversLeaderboardUseCase, type GetDriversLeaderboardResult } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DriverService } from './DriverService';
describe('DriverService', () => {
let service: DriverService;
let getDriversLeaderboardUseCase: ReturnType<typeof vi.mocked<GetDriversLeaderboardUseCase>>;
let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
let logger: ReturnType<typeof vi.mocked<Logger>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DriverService,
{
provide: 'GetDriversLeaderboardUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'GetTotalDriversUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'CompleteDriverOnboardingUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'IsDriverRegisteredForRaceUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'UpdateDriverProfileUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'GetProfileOverviewUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'IDriverRepository',
useValue: {
findById: vi.fn(),
},
},
{
provide: 'Logger',
useValue: {
debug: vi.fn(),
error: vi.fn(),
},
},
],
}).compile();
service = module.get<DriverService>(DriverService);
getDriversLeaderboardUseCase = vi.mocked(module.get('GetDriversLeaderboardUseCase'));
getTotalDriversUseCase = vi.mocked(module.get('GetTotalDriversUseCase'));
completeDriverOnboardingUseCase = vi.mocked(module.get('CompleteDriverOnboardingUseCase'));
isDriverRegisteredForRaceUseCase = vi.mocked(module.get('IsDriverRegisteredForRaceUseCase'));
logger = vi.mocked(module.get('Logger'));
beforeEach(() => {
// Mock all dependencies
service = new DriverService(
{} as any, // getDriversLeaderboardUseCase
{} as any, // getTotalDriversUseCase
{} as any, // completeDriverOnboardingUseCase
{} as any, // isDriverRegisteredForRaceUseCase
{} as any, // updateDriverProfileUseCase
{} as any, // getProfileOverviewUseCase
{} as any, // driverRepository
{} as any, // logger
// Presenters
{} as any, // driversLeaderboardPresenter
{} as any, // driverStatsPresenter
{} as any, // completeOnboardingPresenter
{} as any, // driverRegistrationStatusPresenter
{} as any, // driverPresenter
{} as any, // driverProfilePresenter
);
});
describe('getDriversLeaderboard', () => {
it('should call GetDriversLeaderboardUseCase and return the view model', async () => {
const mockViewModel = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
rating: 2500,
skillLevel: 'Pro',
nationality: 'DE',
racesCompleted: 50,
wins: 10,
podiums: 20,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.png',
},
],
totalRaces: 50,
totalWins: 10,
activeCount: 1,
};
const businessResult = {
items: mockViewModel.drivers.map(dto => ({
driver: { id: dto.id, name: dto.name, country: dto.nationality },
rating: dto.rating,
skillLevel: dto.skillLevel,
racesCompleted: dto.racesCompleted,
wins: dto.wins,
podiums: dto.podiums,
isActive: dto.isActive,
rank: dto.rank,
avatarUrl: dto.avatarUrl,
})),
totalRaces: mockViewModel.totalRaces,
totalWins: mockViewModel.totalWins,
activeCount: mockViewModel.activeCount,
};
getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(businessResult as unknown as GetDriversLeaderboardResult));
const result = await service.getDriversLeaderboard();
expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith({});
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.');
expect(result).toEqual(mockViewModel);
});
});
describe('getTotalDrivers', () => {
it('should call GetTotalDriversUseCase and return the view model', async () => {
const mockOutput = { totalDrivers: 5 };
getTotalDriversUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
const result = await service.getTotalDrivers();
expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith({});
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.');
expect(result).toEqual(mockOutput);
});
});
describe('completeOnboarding', () => {
it('should call CompleteDriverOnboardingUseCase and return success', async () => {
const input = {
firstName: 'John',
lastName: 'Doe',
displayName: 'John Doe',
country: 'US',
bio: 'Racing enthusiast',
};
completeDriverOnboardingUseCase.execute.mockResolvedValue(
Result.ok({ driver: { id: 'user-123' } as Driver })
);
const result = await service.completeOnboarding('user-123', input);
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith({
userId: 'user-123',
...input,
});
expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123');
expect(result).toEqual({
success: true,
driverId: 'user-123',
});
});
it('should handle error from use case', async () => {
const input = {
firstName: 'John',
lastName: 'Doe',
displayName: 'John Doe',
country: 'US',
bio: 'Racing enthusiast',
};
completeDriverOnboardingUseCase.execute.mockResolvedValue(
Result.err({ code: 'DRIVER_ALREADY_EXISTS', details: { message: 'Driver already exists' } })
);
const result = await service.completeOnboarding('user-123', input);
expect(result).toEqual({
success: false,
errorMessage: 'DRIVER_ALREADY_EXISTS',
});
});
});
describe('getDriverRegistrationStatus', () => {
it('should call IsDriverRegisteredForRaceUseCase and return the view model', async () => {
const query = {
driverId: 'driver-1',
raceId: 'race-1',
};
const mockOutput = {
isRegistered: true,
raceId: 'race-1',
driverId: 'driver-1',
};
isDriverRegisteredForRaceUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
const result = await service.getDriverRegistrationStatus(query);
expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query);
expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query);
expect(result).toEqual(mockOutput);
});
it('should be created', () => {
expect(service).toBeDefined();
});
});

View File

@@ -1,6 +1,4 @@
import { Result } from '@core/shared/application/Result';
import { Inject, Injectable } from '@nestjs/common';
import type { Driver } from '@core/racing/domain/entities/Driver';
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
@@ -42,13 +40,6 @@ import {
@Injectable()
export class DriverService {
private readonly driversLeaderboardPresenter = new DriversLeaderboardPresenter();
private readonly driverStatsPresenter = new DriverStatsPresenter();
private readonly completeOnboardingPresenter = new CompleteOnboardingPresenter();
private readonly driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter();
private readonly driverPresenter = new DriverPresenter();
private readonly driverProfilePresenter = new DriverProfilePresenter();
constructor(
@Inject(GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN)
private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase,
@@ -66,29 +57,26 @@ export class DriverService {
private readonly driverRepository: IDriverRepository, // TODO must be removed from service
@Inject(LOGGER_TOKEN)
private readonly logger: Logger,
// Injected presenters
private readonly driversLeaderboardPresenter: DriversLeaderboardPresenter,
private readonly driverStatsPresenter: DriverStatsPresenter,
private readonly completeOnboardingPresenter: CompleteOnboardingPresenter,
private readonly driverRegistrationStatusPresenter: DriverRegistrationStatusPresenter,
private readonly driverPresenter: DriverPresenter,
private readonly driverProfilePresenter: DriverProfilePresenter,
) {}
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
const result = await this.getDriversLeaderboardUseCase.execute({});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.driversLeaderboardPresenter.present(result as Result<any, any>);
await this.getDriversLeaderboardUseCase.execute({});
return this.driversLeaderboardPresenter.getResponseModel();
}
async getTotalDrivers(): Promise<DriverStatsDTO> {
this.logger.debug('[DriverService] Fetching total drivers count.');
const result = await this.getTotalDriversUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver stats');
}
this.driverStatsPresenter.present(result.unwrap());
await this.getTotalDriversUseCase.execute({});
return this.driverStatsPresenter.getResponseModel();
}
@@ -98,7 +86,7 @@ export class DriverService {
): Promise<CompleteOnboardingOutputDTO> {
this.logger.debug('Completing onboarding for user:', userId);
const result = await this.completeDriverOnboardingUseCase.execute({
await this.completeDriverOnboardingUseCase.execute({
userId,
firstName: input.firstName,
lastName: input.lastName,
@@ -107,7 +95,6 @@ export class DriverService {
...(input.bio !== undefined ? { bio: input.bio } : {}),
});
this.completeOnboardingPresenter.present(result);
return this.completeOnboardingPresenter.getResponseModel();
}
@@ -116,28 +103,18 @@ export class DriverService {
): Promise<DriverRegistrationStatusDTO> {
this.logger.debug('Checking driver registration status:', query);
const result = await this.isDriverRegisteredForRaceUseCase.execute({
await this.isDriverRegisteredForRaceUseCase.execute({
raceId: query.raceId,
driverId: query.driverId,
});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to check registration status');
}
this.driverRegistrationStatusPresenter.present(result.unwrap());
return this.driverRegistrationStatusPresenter.getResponseModel();
}
async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Fetching current driver for userId: ${userId}`);
const result = Result.ok(await this.driverRepository.findById(userId));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.driverPresenter.present(result as Result<Driver | null, any>);
await this.driverRepository.findById(userId);
return this.driverPresenter.getResponseModel();
}
@@ -152,39 +129,21 @@ export class DriverService {
if (bio !== undefined) input.bio = bio;
if (country !== undefined) input.country = country;
const result = await this.updateDriverProfileUseCase.execute(input);
if (result.isErr()) {
this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`);
this.driverPresenter.present(Result.ok(null));
return this.driverPresenter.getResponseModel();
}
this.driverPresenter.present(Result.ok(result.unwrap()));
await this.updateDriverProfileUseCase.execute(input);
return this.driverPresenter.getResponseModel();
}
async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Fetching driver for driverId: ${driverId}`);
const driver = await this.driverRepository.findById(driverId);
this.driverPresenter.present(Result.ok(driver));
await this.driverRepository.findById(driverId);
return this.driverPresenter.getResponseModel();
}
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`);
const result = await this.getProfileOverviewUseCase.execute({ driverId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver profile');
}
this.driverProfilePresenter.present(result.unwrap());
await this.getProfileOverviewUseCase.execute({ driverId });
return this.driverProfilePresenter.getResponseModel();
}
}
}