import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AvatarService } from './AvatarService'; import type { MediaApiClient } from '../../api/media/MediaApiClient'; import type { AvatarPresenter } from '../../presenters/AvatarPresenter'; import type { RequestAvatarGenerationInputDto, RequestAvatarGenerationOutputDto, GetAvatarOutputDto, UpdateAvatarInputDto, UpdateAvatarOutputDto } from '../../dtos'; import type { RequestAvatarGenerationViewModel, AvatarViewModel, UpdateAvatarViewModel } from '../../view-models'; describe('AvatarService', () => { let service: AvatarService; let mockApiClient: MediaApiClient; let mockPresenter: AvatarPresenter; beforeEach(() => { mockApiClient = { requestAvatarGeneration: vi.fn(), getAvatar: vi.fn(), updateAvatar: vi.fn(), } as unknown as MediaApiClient; mockPresenter = { presentRequestGeneration: vi.fn(), presentAvatar: vi.fn(), presentUpdate: vi.fn(), } as unknown as AvatarPresenter; service = new AvatarService(mockApiClient, mockPresenter); }); describe('constructor', () => { it('should create instance with injected dependencies', () => { expect(service).toBeInstanceOf(AvatarService); }); }); describe('requestAvatarGeneration', () => { it('should request avatar generation and transform via presenter', async () => { // Arrange const input: RequestAvatarGenerationInputDto = { driverId: 'driver-123', style: 'realistic', }; const mockDto: RequestAvatarGenerationOutputDto = { success: true, avatarUrl: 'https://example.com/avatar/generated.jpg', }; const mockViewModel: RequestAvatarGenerationViewModel = { success: true, avatarUrl: 'https://example.com/avatar/generated.jpg', }; vi.mocked(mockApiClient.requestAvatarGeneration).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentRequestGeneration).mockReturnValue(mockViewModel); // Act const result = await service.requestAvatarGeneration(input); // Assert expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); expect(mockPresenter.presentRequestGeneration).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle generation failure', async () => { // Arrange const input: RequestAvatarGenerationInputDto = { driverId: 'driver-123', }; const mockDto: RequestAvatarGenerationOutputDto = { success: false, error: 'Generation failed', }; const mockViewModel: RequestAvatarGenerationViewModel = { success: false, error: 'Generation failed', }; vi.mocked(mockApiClient.requestAvatarGeneration).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentRequestGeneration).mockReturnValue(mockViewModel); // Act const result = await service.requestAvatarGeneration(input); // Assert expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); expect(mockPresenter.presentRequestGeneration).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); expect(result.success).toBe(false); }); it('should propagate errors from API client', async () => { // Arrange const input: RequestAvatarGenerationInputDto = { driverId: 'driver-123', }; const error = new Error('Network error'); vi.mocked(mockApiClient.requestAvatarGeneration).mockRejectedValue(error); // Act & Assert await expect(service.requestAvatarGeneration(input)).rejects.toThrow('Network error'); expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); expect(mockPresenter.presentRequestGeneration).not.toHaveBeenCalled(); }); }); describe('getAvatar', () => { it('should fetch avatar and transform via presenter', async () => { // Arrange const driverId = 'driver-123'; const mockDto: GetAvatarOutputDto = { driverId: 'driver-123', avatarUrl: 'https://example.com/avatar.jpg', hasAvatar: true, }; const mockViewModel: AvatarViewModel = { driverId: 'driver-123', avatarUrl: 'https://example.com/avatar.jpg', hasAvatar: true, }; vi.mocked(mockApiClient.getAvatar).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentAvatar).mockReturnValue(mockViewModel); // Act const result = await service.getAvatar(driverId); // Assert expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); expect(mockPresenter.presentAvatar).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle driver without avatar', async () => { // Arrange const driverId = 'driver-123'; const mockDto: GetAvatarOutputDto = { driverId: 'driver-123', hasAvatar: false, }; const mockViewModel: AvatarViewModel = { driverId: 'driver-123', hasAvatar: false, }; vi.mocked(mockApiClient.getAvatar).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentAvatar).mockReturnValue(mockViewModel); // Act const result = await service.getAvatar(driverId); // Assert expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); expect(mockPresenter.presentAvatar).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); expect(result.hasAvatar).toBe(false); }); it('should propagate errors from API client', async () => { // Arrange const driverId = 'driver-123'; const error = new Error('Avatar not found'); vi.mocked(mockApiClient.getAvatar).mockRejectedValue(error); // Act & Assert await expect(service.getAvatar(driverId)).rejects.toThrow('Avatar not found'); expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); expect(mockPresenter.presentAvatar).not.toHaveBeenCalled(); }); }); describe('updateAvatar', () => { it('should update avatar and transform via presenter', async () => { // Arrange const input: UpdateAvatarInputDto = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.jpg', }; const mockDto: UpdateAvatarOutputDto = { success: true, }; const mockViewModel: UpdateAvatarViewModel = { success: true, }; vi.mocked(mockApiClient.updateAvatar).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentUpdate).mockReturnValue(mockViewModel); // Act const result = await service.updateAvatar(input); // Assert expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); expect(mockPresenter.presentUpdate).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle update failure', async () => { // Arrange const input: UpdateAvatarInputDto = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.jpg', }; const mockDto: UpdateAvatarOutputDto = { success: false, error: 'Update failed', }; const mockViewModel: UpdateAvatarViewModel = { success: false, error: 'Update failed', }; vi.mocked(mockApiClient.updateAvatar).mockResolvedValue(mockDto); vi.mocked(mockPresenter.presentUpdate).mockReturnValue(mockViewModel); // Act const result = await service.updateAvatar(input); // Assert expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); expect(mockPresenter.presentUpdate).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); expect(result.success).toBe(false); }); it('should propagate errors from API client', async () => { // Arrange const input: UpdateAvatarInputDto = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.jpg', }; const error = new Error('Update failed'); vi.mocked(mockApiClient.updateAvatar).mockRejectedValue(error); // Act & Assert await expect(service.updateAvatar(input)).rejects.toThrow('Update failed'); expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); expect(mockPresenter.presentUpdate).not.toHaveBeenCalled(); }); }); });