website refactor

This commit is contained in:
2026-01-16 12:55:48 +01:00
parent 0208334c59
commit 20a42c52fd
83 changed files with 1610 additions and 1238 deletions

View File

@@ -9,7 +9,7 @@ describe('Default avatar assets (HTTP)', () => {
const originalEnv = { ...process.env };
let module: TestingModule | undefined;
let app: any;
let app: import("@nestjs/common").INestApplication;
beforeAll(async () => {
vi.resetModules();

View File

@@ -35,6 +35,8 @@ describe('MediaController', () => {
deleteMedia: ReturnType<typeof vi.fn>;
getAvatar: ReturnType<typeof vi.fn>;
updateAvatar: ReturnType<typeof vi.fn>;
getUploadedMedia: ReturnType<typeof vi.fn>;
resolveMediaReference: ReturnType<typeof vi.fn>;
};
let generationService: MediaGenerationService & {
generateDriverAvatar: ReturnType<typeof vi.fn>;
@@ -57,6 +59,8 @@ describe('MediaController', () => {
deleteMedia: vi.fn(),
getAvatar: vi.fn(),
updateAvatar: vi.fn(),
getUploadedMedia: vi.fn(),
resolveMediaReference: vi.fn(),
},
},
{
@@ -97,8 +101,8 @@ describe('MediaController', () => {
}).compile();
controller = module.get<MediaController>(MediaController);
service = module.get(MediaService) as any;
generationService = module.get(MediaGenerationService) as any;
service = module.get(MediaService) as never;
generationService = module.get(MediaGenerationService) as never;
});
const createMockResponse = (): Response => {
@@ -375,6 +379,7 @@ describe('MediaController', () => {
};
const mockService = {
getMedia: vi.fn().mockResolvedValue({ id: mediaId }),
getUploadedMedia: vi.fn().mockResolvedValue({ bytes: pngBuffer, contentType: 'image/png' }),
};
const module = await Test.createTestingModule({
@@ -409,8 +414,7 @@ describe('MediaController', () => {
await testController.getUploadedMedia(mediaId, res);
expect(mockService.getMedia).toHaveBeenCalledWith(mediaId);
expect(mockStorage.getBytes).toHaveBeenCalledWith('uploaded/media-123');
expect(mockStorage.getMetadata).toHaveBeenCalledWith('uploaded/media-123');
expect(mockService.getUploadedMedia).toHaveBeenCalledWith('uploaded/media-123');
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'image/png');
expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'public, max-age=31536000, immutable');
expect(res.status).toHaveBeenCalledWith(200);
@@ -569,7 +573,7 @@ describe('MediaController', () => {
});
describe('auth guards (HTTP)', () => {
let app: any;
let app: import("@nestjs/common").INestApplication;
const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
getCurrentSession: vi.fn(async () => null),
@@ -603,6 +607,8 @@ describe('MediaController', () => {
uploadMedia: vi.fn(),
getAvatar: vi.fn(),
updateAvatar: vi.fn(),
getUploadedMedia: vi.fn(async () => ({ bytes: Buffer.from([]), contentType: 'image/png' })),
resolveMediaReference: vi.fn(async () => '/path'),
},
},
{
@@ -657,9 +663,9 @@ describe('MediaController', () => {
const reflector = new Reflector();
app.useGlobalGuards(
new AuthenticationGuard(sessionPort as any),
new AuthorizationGuard(reflector, authorizationService as any),
new FeatureAvailabilityGuard(reflector, policyService as any),
new AuthenticationGuard(sessionPort as never),
new AuthorizationGuard(reflector, authorizationService as never),
new FeatureAvailabilityGuard(reflector, policyService as never),
);
await app.init();

View File

@@ -19,9 +19,7 @@ import type { MulterFile } from './types/MulterFile';
import { MediaGenerationService } from '@core/media/domain/services/MediaGenerationService';
import type { Logger } from '@core/shared/application/Logger';
import { MediaReference } from '@core/domain/media/MediaReference';
import { MediaResolverAdapter } from '@adapters/media/MediaResolverAdapter';
import { LOGGER_TOKEN, MEDIA_STORAGE_PORT_TOKEN } from './MediaTokens';
import type { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort';
import { LOGGER_TOKEN } from './MediaTokens';
import path from 'node:path';
import fs from 'node:fs/promises';
@@ -36,8 +34,6 @@ export class MediaController {
@Inject(MediaService) private readonly mediaService: MediaService,
@Inject(MediaGenerationService) private readonly mediaGenerationService: MediaGenerationService,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(MediaResolverAdapter) private readonly mediaResolver: MediaResolverAdapter,
@Inject(MEDIA_STORAGE_PORT_TOKEN) private readonly mediaStorage: MediaStoragePort,
) {}
@Post('avatar/generate')
@@ -272,23 +268,19 @@ export class MediaController {
// The mediaId is used as the storage key
const storageKey = `uploaded/${mediaId}`;
// Get file bytes from storage
const bytes = await this.mediaStorage.getBytes!(storageKey);
if (!bytes) {
// Get file bytes from storage via service
const result = await this.mediaService.getUploadedMedia(storageKey);
if (!result) {
this.logger.warn('[MediaController] Uploaded media file not found', { mediaId, storageKey });
res.status(HttpStatus.NOT_FOUND).json({ error: 'Media file not found' });
return;
}
// Get metadata to determine content type
const metadata = await this.mediaStorage.getMetadata!(storageKey);
const contentType = metadata?.contentType || 'application/octet-stream';
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Type', result.contentType);
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.status(HttpStatus.OK).send(bytes);
res.status(HttpStatus.OK).send(result.bytes);
this.logger.info('[MediaController] Uploaded media served', { mediaId, storageKey, size: bytes.length });
this.logger.info('[MediaController] Uploaded media served', { mediaId, storageKey, size: result.bytes.length });
}
@Public()
@@ -339,7 +331,7 @@ export class MediaController {
if (ref) {
refHash = ref.hash();
resolvedPath = await this.mediaResolver.resolve(ref);
resolvedPath = await this.mediaService.resolveMediaReference(ref);
if (!resolvedPath) {
notes.push('Resolver returned null');
@@ -382,7 +374,7 @@ export class MediaController {
): Promise<void> {
this.logger.debug('[MediaController] Getting media details', { mediaId });
const dto: GetMediaOutputDTO | null = await this.mediaService.getMedia(mediaId);
if (dto) {
this.logger.info('[MediaController] Media details found', { mediaId });
res.status(HttpStatus.OK).json(dto);

View File

@@ -8,6 +8,7 @@ import { FaceValidationPort } from '@core/media/application/ports/FaceValidation
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort';
import type { Logger } from '@core/shared/application';
import type { MediaResolverPort } from '@core/ports/media/MediaResolverPort';
// Import use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
@@ -16,6 +17,8 @@ import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCa
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import { ResolveMediaReferenceUseCase } from '@core/media/application/use-cases/ResolveMediaReferenceUseCase';
import { GetUploadedMediaUseCase } from '@core/media/application/use-cases/GetUploadedMediaUseCase';
// Import presenters
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
@@ -39,6 +42,9 @@ import {
DELETE_MEDIA_USE_CASE_TOKEN,
GET_AVATAR_USE_CASE_TOKEN,
UPDATE_AVATAR_USE_CASE_TOKEN,
RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN,
GET_UPLOADED_MEDIA_USE_CASE_TOKEN,
MEDIA_RESOLVER_PORT_TOKEN,
} from './MediaTokens';
export * from './MediaTokens';
@@ -92,7 +98,7 @@ const initLogger = InitializationLogger.getInstance();
export const MediaProviders: Provider[] = createLoggedProviders([
MediaGenerationService,
{
provide: MediaResolverAdapter,
provide: MEDIA_RESOLVER_PORT_TOKEN,
useFactory: () => new MediaResolverAdapter({}),
},
RequestAvatarGenerationPresenter,
@@ -156,4 +162,16 @@ export const MediaProviders: Provider[] = createLoggedProviders([
new UpdateAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
], initLogger);
{
provide: RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN,
useFactory: (mediaResolver: MediaResolverPort) =>
new ResolveMediaReferenceUseCase(mediaResolver),
inject: [MEDIA_RESOLVER_PORT_TOKEN],
},
{
provide: GET_UPLOADED_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaStorage: MediaStoragePort) =>
new GetUploadedMediaUseCase(mediaStorage),
inject: [MEDIA_STORAGE_PORT_TOKEN],
},
], initLogger);

View File

@@ -16,28 +16,30 @@ describe('MediaService', () => {
};
const service = new MediaService(
requestAvatarGenerationUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
requestAvatarGenerationPresenter as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
requestAvatarGenerationUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
requestAvatarGenerationPresenter as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as any, suitColor: 'red' as any }),
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as never, suitColor: 'red' as never }),
).resolves.toEqual({ success: true, requestId: 'r1', avatarUrls: ['u1'], errorMessage: '' });
expect(requestAvatarGenerationUseCase.execute).toHaveBeenCalledWith({
userId: 'u1',
facePhotoData: {} as any,
facePhotoData: {} as never,
suitColor: 'red',
});
expect(requestAvatarGenerationPresenter.transform).toHaveBeenCalledWith({ requestId: 'r1', status: 'completed', avatarUrls: ['u1'] });
@@ -45,23 +47,25 @@ describe('MediaService', () => {
it('requestAvatarGeneration returns failure DTO on error', async () => {
const service = new MediaService(
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'fail' } })) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'fail' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as any, suitColor: 'red' as any }),
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as never, suitColor: 'red' as never }),
).resolves.toEqual({
success: false,
requestId: '',
@@ -78,30 +82,32 @@ describe('MediaService', () => {
const uploadMediaUseCase = { execute: vi.fn(async () => Result.ok({ mediaId: 'm1', url: 'https://example.com/m1.png' })) };
const service = new MediaService(
{ execute: vi.fn() } as any,
uploadMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
uploadMediaPresenter as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
uploadMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
uploadMediaPresenter as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(
service.uploadMedia({ file: {} as any, userId: 'u1', metadata: { a: 1 } } as any),
service.uploadMedia({ file: {} as never, userId: 'u1', metadata: { a: 1 } } as never),
).resolves.toEqual({
success: true,
mediaId: 'm1',
});
expect(uploadMediaUseCase.execute).toHaveBeenCalledWith({
file: {} as any,
file: {} as never,
uploadedBy: 'u1',
metadata: { a: 1 },
});
@@ -116,23 +122,25 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
uploadMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
uploadMediaPresenter as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
uploadMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
uploadMediaPresenter as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.uploadMedia({ file: {} as any } as any)).resolves.toEqual({ success: true, mediaId: 'm1' });
expect(uploadMediaUseCase.execute).toHaveBeenCalledWith({ file: {} as any, uploadedBy: '', metadata: {} });
await expect(service.uploadMedia({ file: {} as never } as never)).resolves.toEqual({ success: true, mediaId: 'm1' });
expect(uploadMediaUseCase.execute).toHaveBeenCalledWith({ file: {} as never, uploadedBy: '', metadata: {} });
expect(uploadMediaPresenter.transform).toHaveBeenCalledWith({ mediaId: 'm1', url: 'https://example.com/m1.png' });
});
@@ -142,22 +150,24 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
uploadMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
uploadMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.uploadMedia({ file: {} as any, userId: 'u1' } as any)).resolves.toEqual({
await expect(service.uploadMedia({ file: {} as never, userId: 'u1' } as never)).resolves.toEqual({
success: false,
error: 'nope',
});
@@ -172,19 +182,21 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
getMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
getMediaPresenter as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
getMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
getMediaPresenter as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
const result = await service.getMedia('m1');
@@ -195,19 +207,21 @@ describe('MediaService', () => {
it('getMedia returns null on MEDIA_NOT_FOUND', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'MEDIA_NOT_FOUND', details: { message: 'n/a' } })) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'MEDIA_NOT_FOUND', details: { message: 'n/a' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getMedia('m1')).resolves.toBeNull();
@@ -215,19 +229,21 @@ describe('MediaService', () => {
it('getMedia throws on non-not-found error', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getMedia('m1')).rejects.toThrow('boom');
@@ -241,19 +257,21 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
deleteMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
deleteMediaPresenter as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
deleteMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
deleteMediaPresenter as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.deleteMedia('m1')).resolves.toEqual({ success: true });
@@ -263,19 +281,21 @@ describe('MediaService', () => {
it('deleteMedia returns failure DTO on error', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.deleteMedia('m1')).resolves.toEqual({ success: false, error: 'nope' });
@@ -290,19 +310,21 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
getAvatarUseCase as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
getAvatarPresenter as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
getAvatarUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
getAvatarPresenter as never,
{ responseModel: {} } as never,
);
await expect(service.getAvatar('d1')).resolves.toEqual({ avatarUrl: 'https://example.com/avatar.png' });
@@ -312,19 +334,21 @@ describe('MediaService', () => {
it('getAvatar returns null on AVATAR_NOT_FOUND', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'AVATAR_NOT_FOUND', details: { message: 'n/a' } })) } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'AVATAR_NOT_FOUND', details: { message: 'n/a' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getAvatar('d1')).resolves.toBeNull();
@@ -332,19 +356,21 @@ describe('MediaService', () => {
it('getAvatar throws on non-not-found error', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getAvatar('d1')).rejects.toThrow('boom');
@@ -358,44 +384,48 @@ describe('MediaService', () => {
};
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
updateAvatarUseCase as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
updateAvatarPresenter as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
updateAvatarUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
updateAvatarPresenter as never,
);
await expect(service.updateAvatar('d1', { avatarUrl: 'u1' } as any)).resolves.toEqual({ success: true });
await expect(service.updateAvatar('d1', { avatarUrl: 'u1' } as never)).resolves.toEqual({ success: true });
expect(updateAvatarUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1', mediaUrl: 'u1' });
expect(updateAvatarPresenter.transform).toHaveBeenCalledWith({ avatarId: 'a1', driverId: 'd1' });
});
it('updateAvatar returns failure DTO on error', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
);
await expect(service.updateAvatar('d1', { avatarUrl: 'u' } as any)).resolves.toEqual({
await expect(service.updateAvatar('d1', { avatarUrl: 'u' } as never)).resolves.toEqual({
success: false,
error: 'nope',
});
@@ -403,23 +433,25 @@ describe('MediaService', () => {
it('requestAvatarGeneration uses fallback errorMessage when no details.message', async () => {
const service = new MediaService(
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as never)) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as any, suitColor: 'red' as any }),
service.requestAvatarGeneration({ userId: 'u1', facePhotoData: {} as never, suitColor: 'red' as never }),
).resolves.toEqual({
success: false,
requestId: '',
@@ -430,26 +462,28 @@ describe('MediaService', () => {
it('uploadMedia uses fallback error when no details.message', async () => {
const uploadMediaUseCase = {
execute: vi.fn(async () => Result.err({ code: 'UPLOAD_FAILED' } as any)),
execute: vi.fn(async () => Result.err({ code: 'UPLOAD_FAILED' } as never)),
};
const service = new MediaService(
{ execute: vi.fn() } as any,
uploadMediaUseCase as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
uploadMediaUseCase as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.uploadMedia({ file: {} as any, userId: 'u1' } as any)).resolves.toEqual({
await expect(service.uploadMedia({ file: {} as never, userId: 'u1' } as never)).resolves.toEqual({
success: false,
error: 'Upload failed',
});
@@ -457,19 +491,21 @@ describe('MediaService', () => {
it('getMedia throws fallback message when no details.message and not not-found', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as never)) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getMedia('m1')).rejects.toThrow('Failed to get media');
@@ -477,19 +513,21 @@ describe('MediaService', () => {
it('deleteMedia uses fallback message when no details.message', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as never)) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.deleteMedia('m1')).resolves.toEqual({ success: false, error: 'Failed to delete media' });
@@ -497,19 +535,21 @@ describe('MediaService', () => {
it('getAvatar throws fallback message when no details.message and not not-found', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
{ execute: vi.fn() } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as never)) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
);
await expect(service.getAvatar('d1')).rejects.toThrow('Failed to get avatar');
@@ -517,24 +557,26 @@ describe('MediaService', () => {
it('updateAvatar uses fallback message when no details.message', async () => {
const service = new MediaService(
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
logger as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: {} } as any,
{ responseModel: { success: true } } as any,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as never)) } as never,
{ execute: vi.fn() } as never,
{ execute: vi.fn() } as never,
logger as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: {} } as never,
{ responseModel: { success: true } } as never,
);
await expect(service.updateAvatar('d1', { avatarUrl: 'u' } as any)).resolves.toEqual({
await expect(service.updateAvatar('d1', { avatarUrl: 'u' } as never)).resolves.toEqual({
success: false,
error: 'Failed to update avatar',
});
});
});
});

View File

@@ -12,6 +12,7 @@ import type { ValidateFaceInputDTO } from './dtos/ValidateFaceInputDTO';
import type { ValidateFaceOutputDTO } from './dtos/ValidateFaceOutputDTO';
import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest';
import type { MulterFile } from './types/MulterFile';
import type { MediaReference } from '@core/domain/media/MediaReference';
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
type UploadMediaInput = UploadMediaInputDTO;
@@ -24,6 +25,8 @@ import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCa
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import { ResolveMediaReferenceUseCase } from '@core/media/application/use-cases/ResolveMediaReferenceUseCase';
import { GetUploadedMediaUseCase, type GetUploadedMediaResult } from '@core/media/application/use-cases/GetUploadedMediaUseCase';
// Presenters (now transformers)
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
@@ -40,6 +43,8 @@ import {
DELETE_MEDIA_USE_CASE_TOKEN,
GET_AVATAR_USE_CASE_TOKEN,
UPDATE_AVATAR_USE_CASE_TOKEN,
RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN,
GET_UPLOADED_MEDIA_USE_CASE_TOKEN,
LOGGER_TOKEN,
} from './MediaTokens';
import type { Logger } from '@core/shared/application';
@@ -59,6 +64,10 @@ export class MediaService {
private readonly getAvatarUseCase: GetAvatarUseCase,
@Inject(UPDATE_AVATAR_USE_CASE_TOKEN)
private readonly updateAvatarUseCase: UpdateAvatarUseCase,
@Inject(RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN)
private readonly resolveMediaReferenceUseCase: ResolveMediaReferenceUseCase,
@Inject(GET_UPLOADED_MEDIA_USE_CASE_TOKEN)
private readonly getUploadedMediaUseCase: GetUploadedMediaUseCase,
@Inject(LOGGER_TOKEN)
private readonly logger: Logger,
private readonly requestAvatarGenerationPresenter: RequestAvatarGenerationPresenter,
@@ -211,4 +220,20 @@ export class MediaService {
return { isValid: true };
}
}
async resolveMediaReference(reference: MediaReference): Promise<string | null> {
const result = await this.resolveMediaReferenceUseCase.execute({ reference });
if (result.isErr()) {
throw new Error(result.unwrapErr().message);
}
return result.unwrap();
}
async getUploadedMedia(storageKey: string): Promise<GetUploadedMediaResult | null> {
const result = await this.getUploadedMediaUseCase.execute({ storageKey });
if (result.isErr()) {
throw new Error(result.unwrapErr().message);
}
return result.unwrap();
}
}

View File

@@ -11,4 +11,7 @@ export const UPLOAD_MEDIA_USE_CASE_TOKEN = 'UploadMediaUseCase';
export const GET_MEDIA_USE_CASE_TOKEN = 'GetMediaUseCase';
export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase';
export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
export const RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN = 'ResolveMediaReferenceUseCase';
export const GET_UPLOADED_MEDIA_USE_CASE_TOKEN = 'GetUploadedMediaUseCase';
export const MEDIA_RESOLVER_PORT_TOKEN = 'MediaResolverPort';