From c19c26ffe7d145f1b69556f628dc1fad648ee83f Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 22 Dec 2025 15:58:20 +0100 Subject: [PATCH] refactor media module --- .../domain/dashboard/DashboardService.test.ts | 48 ----------- .../src/domain/media/MediaController.test.ts | 26 ++++-- apps/api/src/domain/media/MediaController.ts | 5 +- apps/api/src/domain/media/MediaService.ts | 3 +- .../domain/media/dtos/DeleteMediaOutputDTO.ts | 2 +- .../domain/media/dtos/GetAvatarOutputDTO.ts | 2 +- .../domain/media/dtos/GetMediaOutputDTO.ts | 8 +- .../dtos/RequestAvatarGenerationInputDTO.ts | 6 +- .../dtos/RequestAvatarGenerationOutputDTO.ts | 2 +- .../domain/media/dtos/UpdateAvatarInputDTO.ts | 4 +- .../media/dtos/UpdateAvatarOutputDTO.ts | 2 +- .../domain/media/dtos/UploadMediaInputDTO.ts | 4 +- .../domain/media/dtos/UploadMediaOutputDTO.ts | 2 +- .../media/presenters/GetMediaPresenter.ts | 12 ++- .../media/presenters/UploadMediaPresenter.ts | 9 +- apps/api/src/domain/media/types/MulterFile.ts | 17 ++++ .../src/domain/payments/PaymentsService.ts | 86 +++++++++---------- 17 files changed, 118 insertions(+), 120 deletions(-) delete mode 100644 apps/api/src/domain/dashboard/DashboardService.test.ts create mode 100644 apps/api/src/domain/media/types/MulterFile.ts diff --git a/apps/api/src/domain/dashboard/DashboardService.test.ts b/apps/api/src/domain/dashboard/DashboardService.test.ts deleted file mode 100644 index c1c5d16bf..000000000 --- a/apps/api/src/domain/dashboard/DashboardService.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { vi } from 'vitest'; -import { DashboardService } from './DashboardService'; -import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; -import type { Logger } from '@core/shared/application/Logger'; -import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; - -describe('DashboardService', () => { - let service: DashboardService; - let mockUseCase: ReturnType>; - let mockPresenter: ReturnType>; - let mockLogger: ReturnType>; - - beforeEach(() => { - mockUseCase = { - execute: vi.fn(), - } as any; - - mockPresenter = { - present: vi.fn(), - getResponseModel: vi.fn(), - } as any; - - mockLogger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - } as any; - - service = new DashboardService( - mockLogger, - mockUseCase, - mockPresenter - ); - }); - - it('should get dashboard overview', async () => { - const mockResult = { totalUsers: 100 }; - mockUseCase.execute.mockResolvedValue(undefined); - mockPresenter.getResponseModel.mockReturnValue(mockResult); - - const result = await service.getDashboardOverview('driver-1'); - - expect(mockUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver-1' }); - expect(mockPresenter.getResponseModel).toHaveBeenCalled(); - expect(result).toBe(mockResult); - }); -}); \ No newline at end of file diff --git a/apps/api/src/domain/media/MediaController.test.ts b/apps/api/src/domain/media/MediaController.test.ts index 95b53b3da..ef3ebbcaa 100644 --- a/apps/api/src/domain/media/MediaController.test.ts +++ b/apps/api/src/domain/media/MediaController.test.ts @@ -11,10 +11,19 @@ import { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO'; import { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO'; import { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; +import { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO'; +import type { MulterFile } from './types/MulterFile'; describe('MediaController', () => { let controller: MediaController; - let service: jest.Mocked; + let service: MediaService & { + requestAvatarGeneration: ReturnType; + uploadMedia: ReturnType; + getMedia: ReturnType; + deleteMedia: ReturnType; + getAvatar: ReturnType; + updateAvatar: ReturnType; + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -35,7 +44,14 @@ describe('MediaController', () => { }).compile(); controller = module.get(MediaController); - service = module.get(MediaService) as jest.Mocked; + service = module.get(MediaService) as MediaService & { + requestAvatarGeneration: ReturnType; + uploadMedia: ReturnType; + getMedia: ReturnType; + deleteMedia: ReturnType; + getAvatar: ReturnType; + updateAvatar: ReturnType; + }; }); const createMockResponse = (): Response => { @@ -92,7 +108,7 @@ describe('MediaController', () => { describe('uploadMedia', () => { it('should upload media and return 201 on success', async () => { - const file: Express.Multer.File = { filename: 'file.jpg' } as Express.Multer.File; + const file: MulterFile = { filename: 'file.jpg' } as MulterFile; const input: UploadMediaInputDTO = { type: 'image' } as UploadMediaInputDTO; const dto: UploadMediaOutputDTO = { success: true, @@ -111,7 +127,7 @@ describe('MediaController', () => { }); it('should return 400 when upload fails', async () => { - const file: Express.Multer.File = { filename: 'file.jpg' } as Express.Multer.File; + const file: MulterFile = { filename: 'file.jpg' } as MulterFile; const input: UploadMediaInputDTO = { type: 'image' } as UploadMediaInputDTO; const dto: UploadMediaOutputDTO = { success: false, @@ -217,7 +233,7 @@ describe('MediaController', () => { describe('updateAvatar', () => { it('should update avatar and return result', async () => { const driverId = 'driver-123'; - const input: UpdateAvatarInputDTO = { avatarUrl: 'https://example.com/new-avatar.png' }; + const input: UpdateAvatarInputDTO = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.png' }; const dto: UpdateAvatarOutputDTO = { success: true, }; diff --git a/apps/api/src/domain/media/MediaController.ts b/apps/api/src/domain/media/MediaController.ts index f04a3d82b..e8bcabb5d 100644 --- a/apps/api/src/domain/media/MediaController.ts +++ b/apps/api/src/domain/media/MediaController.ts @@ -1,6 +1,6 @@ import { Controller, Post, Get, Delete, Put, Body, HttpStatus, Res, Param, UseInterceptors, UploadedFile } from '@nestjs/common'; import { ApiTags, ApiResponse, ApiOperation, ApiParam, ApiConsumes } from '@nestjs/swagger'; -import { Response } from 'express'; +import type { Response } from 'express'; import { FileInterceptor } from '@nestjs/platform-express'; import { MediaService } from './MediaService'; import { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO'; @@ -12,6 +12,7 @@ import { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO'; import { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO'; import { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; +import type { MulterFile } from './types/MulterFile'; type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO; type UploadMediaInput = UploadMediaInputDTO; @@ -44,7 +45,7 @@ export class MediaController { @ApiConsumes('multipart/form-data') @ApiResponse({ status: 201, description: 'Media uploaded successfully', type: UploadMediaOutputDTO }) async uploadMedia( - @UploadedFile() file: Express.Multer.File, + @UploadedFile() file: MulterFile, @Body() input: UploadMediaInput, @Res() res: Response, ): Promise { diff --git a/apps/api/src/domain/media/MediaService.ts b/apps/api/src/domain/media/MediaService.ts index a371f75d1..e42cf48b6 100644 --- a/apps/api/src/domain/media/MediaService.ts +++ b/apps/api/src/domain/media/MediaService.ts @@ -9,6 +9,7 @@ import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO'; import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest'; +import type { MulterFile } from './types/MulterFile'; type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO; type UploadMediaInput = UploadMediaInputDTO; @@ -92,7 +93,7 @@ export class MediaService { } async uploadMedia( - input: UploadMediaInput & { file: Express.Multer.File } & { userId?: string; metadata?: Record }, + input: UploadMediaInput & { file: MulterFile } & { userId?: string; metadata?: Record }, ): Promise { this.logger.debug('[MediaService] Uploading media.'); diff --git a/apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts b/apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts index 9b1ce702c..a99412058 100644 --- a/apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsString, IsOptional } from 'class-validator'; export class DeleteMediaOutputDTO { @ApiProperty() @IsBoolean() - success: boolean; + success: boolean = false; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/media/dtos/GetAvatarOutputDTO.ts b/apps/api/src/domain/media/dtos/GetAvatarOutputDTO.ts index b9c4f5d4a..3205c1e6a 100644 --- a/apps/api/src/domain/media/dtos/GetAvatarOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/GetAvatarOutputDTO.ts @@ -4,5 +4,5 @@ import { IsString } from 'class-validator'; export class GetAvatarOutputDTO { @ApiProperty() @IsString() - avatarUrl: string; + avatarUrl: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/media/dtos/GetMediaOutputDTO.ts b/apps/api/src/domain/media/dtos/GetMediaOutputDTO.ts index ac186bda0..2d284d61a 100644 --- a/apps/api/src/domain/media/dtos/GetMediaOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/GetMediaOutputDTO.ts @@ -4,15 +4,15 @@ import { IsString, IsOptional, IsNumber } from 'class-validator'; export class GetMediaOutputDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - url: string; + url: string = ''; @ApiProperty() @IsString() - type: string; + type: string = ''; @ApiProperty() @IsString() @@ -20,7 +20,7 @@ export class GetMediaOutputDTO { category?: string; @ApiProperty() - uploadedAt: Date; + uploadedAt: Date = new Date(); @ApiProperty() @IsNumber() diff --git a/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts b/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts index 9a7f7e92c..bf0337939 100644 --- a/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts +++ b/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts @@ -5,15 +5,15 @@ export class RequestAvatarGenerationInputDTO { @ApiProperty() @IsString() @IsNotEmpty() - userId: string; + userId: string = ''; @ApiProperty() @IsString() @IsNotEmpty() - facePhotoData: string; + facePhotoData: string = ''; @ApiProperty() @IsString() @IsNotEmpty() - suitColor: string; + suitColor: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts b/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts index 9f65917e3..f3c96d363 100644 --- a/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsString } from 'class-validator'; export class RequestAvatarGenerationOutputDTO { @ApiProperty({ type: Boolean }) @IsBoolean() - success: boolean; + success: boolean = false; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/media/dtos/UpdateAvatarInputDTO.ts b/apps/api/src/domain/media/dtos/UpdateAvatarInputDTO.ts index 647c13d0b..2544061ca 100644 --- a/apps/api/src/domain/media/dtos/UpdateAvatarInputDTO.ts +++ b/apps/api/src/domain/media/dtos/UpdateAvatarInputDTO.ts @@ -4,9 +4,9 @@ import { IsString } from 'class-validator'; export class UpdateAvatarInputDTO { @ApiProperty() @IsString() - driverId: string; + driverId: string = ''; @ApiProperty() @IsString() - avatarUrl: string; + avatarUrl: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/media/dtos/UpdateAvatarOutputDTO.ts b/apps/api/src/domain/media/dtos/UpdateAvatarOutputDTO.ts index 5f5b46d58..f0740e1a8 100644 --- a/apps/api/src/domain/media/dtos/UpdateAvatarOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/UpdateAvatarOutputDTO.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsString, IsOptional } from 'class-validator'; export class UpdateAvatarOutputDTO { @ApiProperty() @IsBoolean() - success: boolean; + success: boolean = false; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/media/dtos/UploadMediaInputDTO.ts b/apps/api/src/domain/media/dtos/UploadMediaInputDTO.ts index b727a9d91..87ab29fef 100644 --- a/apps/api/src/domain/media/dtos/UploadMediaInputDTO.ts +++ b/apps/api/src/domain/media/dtos/UploadMediaInputDTO.ts @@ -3,11 +3,11 @@ import { IsString, IsOptional } from 'class-validator'; export class UploadMediaInputDTO { @ApiProperty({ type: 'string', format: 'binary' }) - file: Express.Multer.File; + file: any; @ApiProperty() @IsString() - type: string; + type: string = ''; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/media/dtos/UploadMediaOutputDTO.ts b/apps/api/src/domain/media/dtos/UploadMediaOutputDTO.ts index 153bd6751..ef2b319b4 100644 --- a/apps/api/src/domain/media/dtos/UploadMediaOutputDTO.ts +++ b/apps/api/src/domain/media/dtos/UploadMediaOutputDTO.ts @@ -4,7 +4,7 @@ import { IsString, IsBoolean, IsOptional } from 'class-validator'; export class UploadMediaOutputDTO { @ApiProperty() @IsBoolean() - success: boolean; + success: boolean = false; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/media/presenters/GetMediaPresenter.ts b/apps/api/src/domain/media/presenters/GetMediaPresenter.ts index 42cbcf111..13ae4fd17 100644 --- a/apps/api/src/domain/media/presenters/GetMediaPresenter.ts +++ b/apps/api/src/domain/media/presenters/GetMediaPresenter.ts @@ -14,15 +14,21 @@ export class GetMediaPresenter implements UseCaseOutputPort { present(result: GetMediaResult): void { const media = result.media; - this.model = { + const model: GetMediaResponseModel = { id: media.id, url: media.url, type: media.type, - // Best-effort mapping from arbitrary metadata - category: (media.metadata as { category?: string } | undefined)?.category, uploadedAt: media.uploadedAt, size: media.size, }; + + // Best-effort mapping from arbitrary metadata + const category = (media.metadata as { category?: string } | undefined)?.category; + if (category !== undefined) { + model.category = category; + } + + this.model = model; } getResponseModel(): GetMediaResponseModel | null { diff --git a/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts b/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts index f5836edc0..b7caa6bc7 100644 --- a/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts +++ b/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts @@ -12,11 +12,16 @@ export class UploadMediaPresenter implements UseCaseOutputPort { + async getPrizes(query: GetPrizesQuery): Promise { // TODO must return ResponseModel not Presenter this.logger.debug('[PaymentsService] Getting prizes', { query }); const presenter = new GetPrizesPresenter(); @@ -166,7 +166,7 @@ export class PaymentsService { return presenter; } - async createPrize(input: CreatePrizeInput): Promise { + async createPrize(input: CreatePrizeInput): Promise { // TODO must return ResponseModel not Presenter this.logger.debug('[PaymentsService] Creating prize', { input }); const presenter = new CreatePrizePresenter(); @@ -174,7 +174,7 @@ export class PaymentsService { return presenter; } - async awardPrize(input: AwardPrizeInput): Promise { + async awardPrize(input: AwardPrizeInput): Promise { // TODO must return ResponseModel not Presenter this.logger.debug('[PaymentsService] Awarding prize', { input }); const presenter = new AwardPrizePresenter(); @@ -182,7 +182,7 @@ export class PaymentsService { return presenter; } - async deletePrize(input: DeletePrizeInput): Promise { + async deletePrize(input: DeletePrizeInput): Promise { // TODO must return ResponseModel not Presenter this.logger.debug('[PaymentsService] Deleting prize', { input }); const presenter = new DeletePrizePresenter(); @@ -190,7 +190,7 @@ export class PaymentsService { return presenter; } - async getWallet(query: GetWalletQuery): Promise { + async getWallet(query: GetWalletQuery): Promise { // TODO must return ResponseModel not Presenter this.logger.debug('[PaymentsService] Getting wallet', { query }); const presenter = new GetWalletPresenter();