refactor media module

This commit is contained in:
2025-12-22 15:58:20 +01:00
parent f59e1b13e7
commit c19c26ffe7
17 changed files with 118 additions and 120 deletions

View File

@@ -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<MediaService>;
let service: MediaService & {
requestAvatarGeneration: ReturnType<typeof vi.fn>;
uploadMedia: ReturnType<typeof vi.fn>;
getMedia: ReturnType<typeof vi.fn>;
deleteMedia: ReturnType<typeof vi.fn>;
getAvatar: ReturnType<typeof vi.fn>;
updateAvatar: ReturnType<typeof vi.fn>;
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -35,7 +44,14 @@ describe('MediaController', () => {
}).compile();
controller = module.get<MediaController>(MediaController);
service = module.get(MediaService) as jest.Mocked<MediaService>;
service = module.get(MediaService) as MediaService & {
requestAvatarGeneration: ReturnType<typeof vi.fn>;
uploadMedia: ReturnType<typeof vi.fn>;
getMedia: ReturnType<typeof vi.fn>;
deleteMedia: ReturnType<typeof vi.fn>;
getAvatar: ReturnType<typeof vi.fn>;
updateAvatar: ReturnType<typeof vi.fn>;
};
});
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,
};

View File

@@ -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<void> {

View File

@@ -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<string, unknown> },
input: UploadMediaInput & { file: MulterFile } & { userId?: string; metadata?: Record<string, unknown> },
): Promise<UploadMediaOutputDTO> {
this.logger.debug('[MediaService] Uploading media.');

View File

@@ -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()

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetAvatarOutputDTO {
@ApiProperty()
@IsString()
avatarUrl: string;
avatarUrl: string = '';
}

View File

@@ -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()

View File

@@ -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 = '';
}

View File

@@ -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()

View File

@@ -4,9 +4,9 @@ import { IsString } from 'class-validator';
export class UpdateAvatarInputDTO {
@ApiProperty()
@IsString()
driverId: string;
driverId: string = '';
@ApiProperty()
@IsString()
avatarUrl: string;
avatarUrl: string = '';
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -14,15 +14,21 @@ export class GetMediaPresenter implements UseCaseOutputPort<GetMediaResult> {
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 {

View File

@@ -12,11 +12,16 @@ export class UploadMediaPresenter implements UseCaseOutputPort<UploadMediaResult
}
present(result: UploadMediaResult): void {
this.model = {
const model: UploadMediaResponseModel = {
success: true,
mediaId: result.mediaId,
url: result.url,
};
if (result.url !== undefined) {
model.url = result.url;
}
this.model = model;
}
getResponseModel(): UploadMediaResponseModel | null {

View File

@@ -0,0 +1,17 @@
/**
* Multer file type definition
* Used for file upload functionality in media domain
*/
export interface MulterFile {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
size: number;
buffer: Buffer;
stream: NodeJS.ReadableStream;
destination: string;
filename: string;
path: string;
}