/** * Integration Test: Avatar Management Use Case Orchestration * * Tests the orchestration logic of avatar-related Use Cases: * - GetAvatarUseCase: Retrieves driver avatar * - UpdateAvatarUseCase: Updates an existing avatar for a driver * - RequestAvatarGenerationUseCase: Requests avatar generation from a photo * - SelectAvatarUseCase: Selects a generated avatar * - GetUploadedMediaUseCase: Retrieves uploaded media * - DeleteMediaUseCase: Deletes media files * - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers) * - Uses In-Memory adapters for fast, deterministic testing * * Focus: Business logic orchestration, NOT UI rendering */ import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { ConsoleLogger } from '@core/shared/logging/ConsoleLogger'; import { InMemoryAvatarRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarRepository'; import { InMemoryAvatarGenerationRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository'; import { InMemoryMediaRepository } from '@adapters/media/persistence/inmemory/InMemoryMediaRepository'; import { InMemoryMediaStorageAdapter } from '@adapters/media/ports/InMemoryMediaStorageAdapter'; import { InMemoryFaceValidationAdapter } from '@adapters/media/ports/InMemoryFaceValidationAdapter'; import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { InMemoryMediaEventPublisher } from '@adapters/media/events/InMemoryMediaEventPublisher'; import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase'; import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import { SelectAvatarUseCase } from '@core/media/application/use-cases/SelectAvatarUseCase'; import { GetUploadedMediaUseCase } from '@core/media/application/use-cases/GetUploadedMediaUseCase'; import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase'; import { Avatar } from '@core/media/domain/entities/Avatar'; import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest'; import { Media } from '@core/media/domain/entities/Media'; describe('Avatar Management Use Case Orchestration', () => { let avatarRepository: InMemoryAvatarRepository; let avatarGenerationRepository: InMemoryAvatarGenerationRepository; let mediaRepository: InMemoryMediaRepository; let mediaStorage: InMemoryMediaStorageAdapter; let faceValidation: InMemoryFaceValidationAdapter; let imageService: InMemoryImageServiceAdapter; let eventPublisher: InMemoryMediaEventPublisher; let logger: ConsoleLogger; let getAvatarUseCase: GetAvatarUseCase; let updateAvatarUseCase: UpdateAvatarUseCase; let requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase; let selectAvatarUseCase: SelectAvatarUseCase; let getUploadedMediaUseCase: GetUploadedMediaUseCase; let deleteMediaUseCase: DeleteMediaUseCase; beforeAll(() => { logger = new ConsoleLogger(); avatarRepository = new InMemoryAvatarRepository(logger); avatarGenerationRepository = new InMemoryAvatarGenerationRepository(logger); mediaRepository = new InMemoryMediaRepository(logger); mediaStorage = new InMemoryMediaStorageAdapter(logger); faceValidation = new InMemoryFaceValidationAdapter(logger); imageService = new InMemoryImageServiceAdapter(logger); eventPublisher = new InMemoryMediaEventPublisher(logger); getAvatarUseCase = new GetAvatarUseCase(avatarRepository, logger); updateAvatarUseCase = new UpdateAvatarUseCase(avatarRepository, logger); requestAvatarGenerationUseCase = new RequestAvatarGenerationUseCase( avatarGenerationRepository, faceValidation, imageService, logger ); selectAvatarUseCase = new SelectAvatarUseCase(avatarGenerationRepository, logger); getUploadedMediaUseCase = new GetUploadedMediaUseCase(mediaStorage); deleteMediaUseCase = new DeleteMediaUseCase(mediaRepository, mediaStorage, logger); }); beforeEach(() => { avatarRepository.clear(); avatarGenerationRepository.clear(); mediaRepository.clear(); mediaStorage.clear(); eventPublisher.clear(); }); describe('GetAvatarUseCase - Success Path', () => { it('should retrieve driver avatar when avatar exists', async () => { // Scenario: Driver with existing avatar // Given: A driver exists with an avatar const avatar = Avatar.create({ id: 'avatar-1', driverId: 'driver-1', mediaUrl: 'https://example.com/avatar.png', }); await avatarRepository.save(avatar); // When: GetAvatarUseCase.execute() is called with driver ID const result = await getAvatarUseCase.execute({ driverId: 'driver-1' }); // Then: The result should contain the avatar data expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.avatar.id).toBe('avatar-1'); expect(successResult.avatar.driverId).toBe('driver-1'); expect(successResult.avatar.mediaUrl).toBe('https://example.com/avatar.png'); expect(successResult.avatar.selectedAt).toBeInstanceOf(Date); }); it('should return AVATAR_NOT_FOUND when driver has no avatar', async () => { // Scenario: Driver without avatar // Given: A driver exists without an avatar // When: GetAvatarUseCase.execute() is called with driver ID const result = await getAvatarUseCase.execute({ driverId: 'driver-1' }); // Then: Should return AVATAR_NOT_FOUND error expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('AVATAR_NOT_FOUND'); expect(err.details.message).toBe('Avatar not found'); }); }); describe('GetAvatarUseCase - Error Handling', () => { it('should handle repository errors gracefully', async () => { // Scenario: Repository error // Given: AvatarRepository throws an error const originalFind = avatarRepository.findActiveByDriverId; avatarRepository.findActiveByDriverId = async () => { throw new Error('Database connection error'); }; // When: GetAvatarUseCase.execute() is called const result = await getAvatarUseCase.execute({ driverId: 'driver-1' }); // Then: Should return REPOSITORY_ERROR expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REPOSITORY_ERROR'); expect(err.details.message).toContain('Database connection error'); // Restore original method avatarRepository.findActiveByDriverId = originalFind; }); }); describe('UpdateAvatarUseCase - Success Path', () => { it('should update existing avatar for a driver', async () => { // Scenario: Driver updates existing avatar // Given: A driver exists with an existing avatar const existingAvatar = Avatar.create({ id: 'avatar-1', driverId: 'driver-1', mediaUrl: 'https://example.com/old-avatar.png', }); await avatarRepository.save(existingAvatar); // When: UpdateAvatarUseCase.execute() is called with driver ID and new image data const result = await updateAvatarUseCase.execute({ driverId: 'driver-1', mediaUrl: 'https://example.com/new-avatar.png', }); // Then: The old avatar should be deactivated and new one created expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.avatarId).toBeDefined(); expect(successResult.driverId).toBe('driver-1'); // Verify old avatar is deactivated const oldAvatar = await avatarRepository.findById('avatar-1'); expect(oldAvatar?.isActive).toBe(false); // Verify new avatar exists const newAvatar = await avatarRepository.findActiveByDriverId('driver-1'); expect(newAvatar).not.toBeNull(); expect(newAvatar?.mediaUrl.value).toBe('https://example.com/new-avatar.png'); }); it('should update avatar when driver has no existing avatar', async () => { // Scenario: Driver updates avatar when no avatar exists // Given: A driver exists without an avatar // When: UpdateAvatarUseCase.execute() is called const result = await updateAvatarUseCase.execute({ driverId: 'driver-1', mediaUrl: 'https://example.com/avatar.png', }); // Then: A new avatar should be created expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.avatarId).toBeDefined(); expect(successResult.driverId).toBe('driver-1'); // Verify new avatar exists const newAvatar = await avatarRepository.findActiveByDriverId('driver-1'); expect(newAvatar).not.toBeNull(); expect(newAvatar?.mediaUrl.value).toBe('https://example.com/avatar.png'); }); }); describe('UpdateAvatarUseCase - Error Handling', () => { it('should handle repository errors gracefully', async () => { // Scenario: Repository error // Given: AvatarRepository throws an error const originalSave = avatarRepository.save; avatarRepository.save = async () => { throw new Error('Database connection error'); }; // When: UpdateAvatarUseCase.execute() is called const result = await updateAvatarUseCase.execute({ driverId: 'driver-1', mediaUrl: 'https://example.com/avatar.png', }); // Then: Should return REPOSITORY_ERROR expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REPOSITORY_ERROR'); expect(err.details.message).toContain('Database connection error'); // Restore original method avatarRepository.save = originalSave; }); }); describe('RequestAvatarGenerationUseCase - Success Path', () => { it('should request avatar generation from photo', async () => { // Scenario: Driver requests avatar generation from photo // Given: A driver exists // And: Valid photo data is provided // When: RequestAvatarGenerationUseCase.execute() is called with driver ID and photo data const result = await requestAvatarGenerationUseCase.execute({ userId: 'user-1', facePhotoData: 'https://example.com/face-photo.jpg', suitColor: 'red', style: 'realistic', }); // Then: An avatar generation request should be created expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.requestId).toBeDefined(); expect(successResult.status).toBe('completed'); expect(successResult.avatarUrls).toBeDefined(); expect(successResult.avatarUrls?.length).toBeGreaterThan(0); // Verify request was saved const request = await avatarGenerationRepository.findById(successResult.requestId); expect(request).not.toBeNull(); expect(request?.status).toBe('completed'); }); it('should request avatar generation with default style', async () => { // Scenario: Driver requests avatar generation with default style // Given: A driver exists // When: RequestAvatarGenerationUseCase.execute() is called without style const result = await requestAvatarGenerationUseCase.execute({ userId: 'user-1', facePhotoData: 'https://example.com/face-photo.jpg', suitColor: 'blue', }); // Then: An avatar generation request should be created with default style expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.requestId).toBeDefined(); expect(successResult.status).toBe('completed'); }); }); describe('RequestAvatarGenerationUseCase - Validation', () => { it('should reject generation with invalid face photo', async () => { // Scenario: Invalid face photo // Given: A driver exists // And: Face validation fails const originalValidate = faceValidation.validateFacePhoto; faceValidation.validateFacePhoto = async () => ({ isValid: false, hasFace: false, faceCount: 0, confidence: 0.0, errorMessage: 'No face detected', }); // When: RequestAvatarGenerationUseCase.execute() is called const result = await requestAvatarGenerationUseCase.execute({ userId: 'user-1', facePhotoData: 'https://example.com/invalid-photo.jpg', suitColor: 'red', }); // Then: Should return FACE_VALIDATION_FAILED error expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('FACE_VALIDATION_FAILED'); expect(err.details.message).toContain('No face detected'); // Restore original method faceValidation.validateFacePhoto = originalValidate; }); }); describe('SelectAvatarUseCase - Success Path', () => { it('should select a generated avatar', async () => { // Scenario: Driver selects a generated avatar // Given: A completed avatar generation request exists const request = AvatarGenerationRequest.create({ id: 'request-1', userId: 'user-1', facePhotoUrl: 'https://example.com/face-photo.jpg', suitColor: 'red', style: 'realistic', }); request.completeWithAvatars([ 'https://example.com/avatar-1.png', 'https://example.com/avatar-2.png', 'https://example.com/avatar-3.png', ]); await avatarGenerationRepository.save(request); // When: SelectAvatarUseCase.execute() is called with request ID and selected index const result = await selectAvatarUseCase.execute({ requestId: 'request-1', selectedIndex: 1, }); // Then: The avatar should be selected expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.requestId).toBe('request-1'); expect(successResult.selectedAvatarUrl).toBe('https://example.com/avatar-2.png'); // Verify request was updated const updatedRequest = await avatarGenerationRepository.findById('request-1'); expect(updatedRequest?.selectedAvatarUrl).toBe('https://example.com/avatar-2.png'); }); }); describe('SelectAvatarUseCase - Error Handling', () => { it('should reject selection when request does not exist', async () => { // Scenario: Request does not exist // Given: No request exists with the given ID // When: SelectAvatarUseCase.execute() is called const result = await selectAvatarUseCase.execute({ requestId: 'non-existent-request', selectedIndex: 0, }); // Then: Should return REQUEST_NOT_FOUND error expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REQUEST_NOT_FOUND'); }); it('should reject selection when request is not completed', async () => { // Scenario: Request is not completed // Given: An incomplete avatar generation request exists const request = AvatarGenerationRequest.create({ id: 'request-1', userId: 'user-1', facePhotoUrl: 'https://example.com/face-photo.jpg', suitColor: 'red', style: 'realistic', }); await avatarGenerationRepository.save(request); // When: SelectAvatarUseCase.execute() is called const result = await selectAvatarUseCase.execute({ requestId: 'request-1', selectedIndex: 0, }); // Then: Should return REQUEST_NOT_COMPLETED error expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REQUEST_NOT_COMPLETED'); }); }); describe('GetUploadedMediaUseCase - Success Path', () => { it('should retrieve uploaded media', async () => { // Scenario: Retrieve uploaded media // Given: Media has been uploaded const uploadResult = await mediaStorage.uploadMedia( Buffer.from('test media content'), { filename: 'test-avatar.png', mimeType: 'image/png', } ); expect(uploadResult.success).toBe(true); const storageKey = uploadResult.url!; // When: GetUploadedMediaUseCase.execute() is called const result = await getUploadedMediaUseCase.execute({ storageKey }); // Then: The media should be retrieved expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult).not.toBeNull(); expect(successResult?.bytes).toBeInstanceOf(Buffer); expect(successResult?.contentType).toBe('image/png'); }); it('should return null when media does not exist', async () => { // Scenario: Media does not exist // Given: No media exists with the given storage key // When: GetUploadedMediaUseCase.execute() is called const result = await getUploadedMediaUseCase.execute({ storageKey: 'non-existent-key' }); // Then: Should return null expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult).toBeNull(); }); }); describe('DeleteMediaUseCase - Success Path', () => { it('should delete media file', async () => { // Scenario: Delete media file // Given: Media has been uploaded const uploadResult = await mediaStorage.uploadMedia( Buffer.from('test media content'), { filename: 'test-avatar.png', mimeType: 'image/png', } ); expect(uploadResult.success).toBe(true); const storageKey = uploadResult.url!; // Create media entity const media = Media.create({ id: 'media-1', filename: 'test-avatar.png', originalName: 'test-avatar.png', mimeType: 'image/png', size: 18, url: storageKey, type: 'image', uploadedBy: 'user-1', }); await mediaRepository.save(media); // When: DeleteMediaUseCase.execute() is called const result = await deleteMediaUseCase.execute({ mediaId: 'media-1' }); // Then: The media should be deleted expect(result.isOk()).toBe(true); const successResult = result.unwrap(); expect(successResult.mediaId).toBe('media-1'); expect(successResult.deleted).toBe(true); // Verify media is deleted from repository const deletedMedia = await mediaRepository.findById('media-1'); expect(deletedMedia).toBeNull(); // Verify media is deleted from storage const storageExists = mediaStorage.has(storageKey); expect(storageExists).toBe(false); }); }); describe('DeleteMediaUseCase - Error Handling', () => { it('should return MEDIA_NOT_FOUND when media does not exist', async () => { // Scenario: Media does not exist // Given: No media exists with the given ID // When: DeleteMediaUseCase.execute() is called const result = await deleteMediaUseCase.execute({ mediaId: 'non-existent-media' }); // Then: Should return MEDIA_NOT_FOUND error expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('MEDIA_NOT_FOUND'); }); }); });