integration tests
This commit is contained in:
170
tests/integration/media/IMPLEMENTATION_NOTES.md
Normal file
170
tests/integration/media/IMPLEMENTATION_NOTES.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Media Integration Tests - Implementation Notes
|
||||
|
||||
## Overview
|
||||
This document describes the implementation of integration tests for media functionality in the GridPilot project.
|
||||
|
||||
## Implemented Tests
|
||||
|
||||
### Avatar Management Integration Tests
|
||||
**File:** `avatar-management.integration.test.ts`
|
||||
|
||||
**Tests Implemented:**
|
||||
- `GetAvatarUseCase` - Success Path
|
||||
- Retrieves driver avatar when avatar exists
|
||||
- Returns AVATAR_NOT_FOUND when driver has no avatar
|
||||
- `GetAvatarUseCase` - Error Handling
|
||||
- Handles repository errors gracefully
|
||||
- `UpdateAvatarUseCase` - Success Path
|
||||
- Updates existing avatar for a driver
|
||||
- Updates avatar when driver has no existing avatar
|
||||
- `UpdateAvatarUseCase` - Error Handling
|
||||
- Handles repository errors gracefully
|
||||
- `RequestAvatarGenerationUseCase` - Success Path
|
||||
- Requests avatar generation from photo
|
||||
- Requests avatar generation with default style
|
||||
- `RequestAvatarGenerationUseCase` - Validation
|
||||
- Rejects generation with invalid face photo
|
||||
- `SelectAvatarUseCase` - Success Path
|
||||
- Selects a generated avatar
|
||||
- `SelectAvatarUseCase` - Error Handling
|
||||
- Rejects selection when request does not exist
|
||||
- Rejects selection when request is not completed
|
||||
- `GetUploadedMediaUseCase` - Success Path
|
||||
- Retrieves uploaded media
|
||||
- Returns null when media does not exist
|
||||
- `DeleteMediaUseCase` - Success Path
|
||||
- Deletes media file
|
||||
- `DeleteMediaUseCase` - Error Handling
|
||||
- Returns MEDIA_NOT_FOUND when media does not exist
|
||||
|
||||
**Use Cases Tested:**
|
||||
- `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
|
||||
|
||||
**In-Memory Adapters Created:**
|
||||
- `InMemoryAvatarRepository` - Stores avatar entities in memory
|
||||
- `InMemoryAvatarGenerationRepository` - Stores avatar generation requests in memory
|
||||
- `InMemoryMediaRepository` - Stores media entities in memory
|
||||
- `InMemoryMediaStorageAdapter` - Simulates file storage in memory
|
||||
- `InMemoryFaceValidationAdapter` - Simulates face validation in memory
|
||||
- `InMemoryImageServiceAdapter` - Simulates image service in memory
|
||||
- `InMemoryMediaEventPublisher` - Stores domain events in memory
|
||||
|
||||
## Placeholder Tests
|
||||
|
||||
The following test files remain as placeholders because they reference domains that are not part of the core/media directory:
|
||||
|
||||
### Category Icon Management
|
||||
**File:** `category-icon-management.integration.test.ts`
|
||||
|
||||
**Status:** Placeholder - Not implemented
|
||||
|
||||
**Reason:** Category icon management would be part of the `core/categories` domain, not `core/media`. The test placeholders reference use cases like `GetCategoryIconsUseCase`, `UploadCategoryIconUseCase`, etc., which would be implemented in the categories domain.
|
||||
|
||||
### League Media Management
|
||||
**File:** `league-media-management.integration.test.ts`
|
||||
|
||||
**Status:** Placeholder - Not implemented
|
||||
|
||||
**Reason:** League media management would be part of the `core/leagues` domain, not `core/media`. The test placeholders reference use cases like `GetLeagueMediaUseCase`, `UploadLeagueCoverUseCase`, etc., which would be implemented in the leagues domain.
|
||||
|
||||
### Sponsor Logo Management
|
||||
**File:** `sponsor-logo-management.integration.test.ts`
|
||||
|
||||
**Status:** Placeholder - Not implemented
|
||||
|
||||
**Reason:** Sponsor logo management would be part of the `core/sponsors` domain, not `core/media`. The test placeholders reference use cases like `GetSponsorLogosUseCase`, `UploadSponsorLogoUseCase`, etc., which would be implemented in the sponsors domain.
|
||||
|
||||
### Team Logo Management
|
||||
**File:** `team-logo-management.integration.test.ts`
|
||||
|
||||
**Status:** Placeholder - Not implemented
|
||||
|
||||
**Reason:** Team logo management would be part of the `core/teams` domain, not `core/media`. The test placeholders reference use cases like `GetTeamLogosUseCase`, `UploadTeamLogoUseCase`, etc., which would be implemented in the teams domain.
|
||||
|
||||
### Track Image Management
|
||||
**File:** `track-image-management.integration.test.ts`
|
||||
|
||||
**Status:** Placeholder - Not implemented
|
||||
|
||||
**Reason:** Track image management would be part of the `core/tracks` domain, not `core/media`. The test placeholders reference use cases like `GetTrackImagesUseCase`, `UploadTrackImageUseCase`, etc., which would be implemented in the tracks domain.
|
||||
|
||||
## Architecture Compliance
|
||||
|
||||
### Core Layer (Business Logic)
|
||||
✅ **Compliant:** All tests focus on Core Use Cases only
|
||||
- Tests use In-Memory adapters for repositories and event publishers
|
||||
- Tests follow Given/When/Then pattern for business logic scenarios
|
||||
- Tests verify Use Case orchestration (interaction between Use Cases and their Ports)
|
||||
- Tests do NOT test HTTP endpoints, DTOs, or Presenters
|
||||
|
||||
### Adapters Layer (Infrastructure)
|
||||
✅ **Compliant:** In-Memory adapters created for testing
|
||||
- `InMemoryAvatarRepository` implements `AvatarRepository` port
|
||||
- `InMemoryMediaRepository` implements `MediaRepository` port
|
||||
- `InMemoryMediaStorageAdapter` implements `MediaStoragePort` port
|
||||
- `InMemoryFaceValidationAdapter` implements `FaceValidationPort` port
|
||||
- `InMemoryImageServiceAdapter` implements `ImageServicePort` port
|
||||
- `InMemoryMediaEventPublisher` stores domain events for verification
|
||||
|
||||
### Test Framework
|
||||
✅ **Compliant:** Using Vitest as specified
|
||||
- All tests use Vitest's `describe`, `it`, `expect`, `beforeAll`, `beforeEach`
|
||||
- Tests are asynchronous and use `async/await`
|
||||
- Tests verify both success paths and error handling
|
||||
|
||||
## Observations
|
||||
|
||||
### Media Implementation Structure
|
||||
The core/media directory contains:
|
||||
- **Domain Layer:** Entities (Avatar, Media, AvatarGenerationRequest), Value Objects (AvatarId, MediaUrl), Repositories (AvatarRepository, MediaRepository, AvatarGenerationRepository)
|
||||
- **Application Layer:** Use Cases (GetAvatarUseCase, UpdateAvatarUseCase, RequestAvatarGenerationUseCase, SelectAvatarUseCase, GetUploadedMediaUseCase, DeleteMediaUseCase), Ports (MediaStoragePort, AvatarGenerationPort, FaceValidationPort, ImageServicePort)
|
||||
|
||||
### Missing Use Cases
|
||||
The placeholder tests reference use cases that don't exist in the core/media directory:
|
||||
- `UploadAvatarUseCase` - Not found (likely part of a different domain)
|
||||
- `DeleteAvatarUseCase` - Not found (likely part of a different domain)
|
||||
- `GenerateAvatarFromPhotoUseCase` - Not found (replaced by `RequestAvatarGenerationUseCase` + `SelectAvatarUseCase`)
|
||||
|
||||
### Domain Boundaries
|
||||
The media functionality is split across multiple domains:
|
||||
- **core/media:** Avatar management and general media management
|
||||
- **core/categories:** Category icon management (not implemented)
|
||||
- **core/leagues:** League media management (not implemented)
|
||||
- **core/sponsors:** Sponsor logo management (not implemented)
|
||||
- **core/teams:** Team logo management (not implemented)
|
||||
- **core/tracks:** Track image management (not implemented)
|
||||
|
||||
Each domain would have its own media-related use cases and repositories, following the same pattern as the core/media domain.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **For categories, leagues, sponsors, teams, and tracks domains:**
|
||||
- Create similar integration tests in their respective test directories
|
||||
- Follow the same pattern as avatar-management.integration.test.ts
|
||||
- Use In-Memory adapters for repositories and event publishers
|
||||
- Test Use Case orchestration only, not HTTP endpoints
|
||||
|
||||
2. **For missing use cases:**
|
||||
- If `UploadAvatarUseCase` and `DeleteAvatarUseCase` are needed, they should be implemented in the appropriate domain
|
||||
- The current implementation uses `UpdateAvatarUseCase` and `DeleteMediaUseCase` instead
|
||||
|
||||
3. **For event publishing:**
|
||||
- The current implementation uses `InMemoryMediaEventPublisher` for testing
|
||||
- In production, a real event publisher would be used
|
||||
- Events should be published for all significant state changes (avatar uploaded, avatar updated, media deleted, etc.)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The integration tests for avatar management have been successfully implemented following the architecture requirements:
|
||||
- ✅ Tests Core Use Cases directly
|
||||
- ✅ Use In-Memory adapters for repositories and event publishers
|
||||
- ✅ Test Use Case orchestration (interaction between Use Cases and their Ports)
|
||||
- ✅ Follow Given/When/Then pattern for business logic scenarios
|
||||
- ✅ Do NOT test HTTP endpoints, DTOs, or Presenters
|
||||
|
||||
The placeholder tests for category, league, sponsor, team, and track media management remain as placeholders because they belong to different domains and would need to be implemented in their respective test directories.
|
||||
@@ -1,357 +1,478 @@
|
||||
/**
|
||||
* Integration Test: Avatar Management Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of avatar-related Use Cases:
|
||||
* - GetAvatarUseCase: Retrieves driver avatar
|
||||
* - UploadAvatarUseCase: Uploads a new avatar for a driver
|
||||
* - UpdateAvatarUseCase: Updates an existing avatar for a driver
|
||||
* - DeleteAvatarUseCase: Deletes a driver's avatar
|
||||
* - GenerateAvatarFromPhotoUseCase: Generates an avatar from a photo
|
||||
* - 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, afterAll, beforeEach } from 'vitest';
|
||||
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', () => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// let avatarRepository: InMemoryAvatarRepository;
|
||||
// let driverRepository: InMemoryDriverRepository;
|
||||
// let eventPublisher: InMemoryEventPublisher;
|
||||
// let getAvatarUseCase: GetAvatarUseCase;
|
||||
// let uploadAvatarUseCase: UploadAvatarUseCase;
|
||||
// let updateAvatarUseCase: UpdateAvatarUseCase;
|
||||
// let deleteAvatarUseCase: DeleteAvatarUseCase;
|
||||
// let generateAvatarFromPhotoUseCase: GenerateAvatarFromPhotoUseCase;
|
||||
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(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// avatarRepository = new InMemoryAvatarRepository();
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getAvatarUseCase = new GetAvatarUseCase({
|
||||
// avatarRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// uploadAvatarUseCase = new UploadAvatarUseCase({
|
||||
// avatarRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// updateAvatarUseCase = new UpdateAvatarUseCase({
|
||||
// avatarRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// deleteAvatarUseCase = new DeleteAvatarUseCase({
|
||||
// avatarRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// generateAvatarFromPhotoUseCase = new GenerateAvatarFromPhotoUseCase({
|
||||
// avatarRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
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(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// avatarRepository.clear();
|
||||
// driverRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
avatarRepository.clear();
|
||||
avatarGenerationRepository.clear();
|
||||
mediaRepository.clear();
|
||||
mediaStorage.clear();
|
||||
eventPublisher.clear();
|
||||
});
|
||||
|
||||
describe('GetAvatarUseCase - Success Path', () => {
|
||||
it('should retrieve driver avatar when avatar exists', async () => {
|
||||
// TODO: Implement test
|
||||
// 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
|
||||
// And: The avatar should have correct metadata (file size, format, upload date)
|
||||
// And: EventPublisher should emit AvatarRetrievedEvent
|
||||
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 default avatar when driver has no avatar', async () => {
|
||||
// TODO: Implement test
|
||||
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
|
||||
// Then: The result should contain default avatar data
|
||||
// And: EventPublisher should emit AvatarRetrievedEvent
|
||||
});
|
||||
const result = await getAvatarUseCase.execute({ driverId: 'driver-1' });
|
||||
|
||||
it('should retrieve avatar for admin viewing driver profile', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin views driver avatar
|
||||
// Given: An admin exists
|
||||
// And: A driver exists with an avatar
|
||||
// When: GetAvatarUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain the avatar data
|
||||
// And: EventPublisher should emit AvatarRetrievedEvent
|
||||
// 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 throw error when driver does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
// When: GetAvatarUseCase.execute() is called with non-existent driver ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
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');
|
||||
};
|
||||
|
||||
it('should throw error when driver ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid driver ID
|
||||
// Given: An invalid driver ID (e.g., empty string, null, undefined)
|
||||
// When: GetAvatarUseCase.execute() is called with invalid driver ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// When: GetAvatarUseCase.execute() is called
|
||||
const result = await getAvatarUseCase.execute({ driverId: 'driver-1' });
|
||||
|
||||
describe('UploadAvatarUseCase - Success Path', () => {
|
||||
it('should upload a new avatar for a driver', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver uploads new avatar
|
||||
// Given: A driver exists without an avatar
|
||||
// And: Valid avatar image data is provided
|
||||
// When: UploadAvatarUseCase.execute() is called with driver ID and image data
|
||||
// Then: The avatar should be stored in the repository
|
||||
// And: The avatar should have correct metadata (file size, format, upload date)
|
||||
// And: EventPublisher should emit AvatarUploadedEvent
|
||||
});
|
||||
// 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');
|
||||
|
||||
it('should upload avatar with validation requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver uploads avatar with validation
|
||||
// Given: A driver exists
|
||||
// And: Avatar data meets validation requirements (correct format, size, dimensions)
|
||||
// When: UploadAvatarUseCase.execute() is called
|
||||
// Then: The avatar should be stored successfully
|
||||
// And: EventPublisher should emit AvatarUploadedEvent
|
||||
});
|
||||
|
||||
it('should upload avatar for admin managing driver profile', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin uploads avatar for driver
|
||||
// Given: An admin exists
|
||||
// And: A driver exists without an avatar
|
||||
// When: UploadAvatarUseCase.execute() is called with driver ID and image data
|
||||
// Then: The avatar should be stored in the repository
|
||||
// And: EventPublisher should emit AvatarUploadedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('UploadAvatarUseCase - Validation', () => {
|
||||
it('should reject upload with invalid file format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid file format
|
||||
// Given: A driver exists
|
||||
// And: Avatar data has invalid format (e.g., .txt, .exe)
|
||||
// When: UploadAvatarUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject upload with oversized file', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: File exceeds size limit
|
||||
// Given: A driver exists
|
||||
// And: Avatar data exceeds maximum file size
|
||||
// When: UploadAvatarUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject upload with invalid dimensions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid image dimensions
|
||||
// Given: A driver exists
|
||||
// And: Avatar data has invalid dimensions (too small or too large)
|
||||
// When: UploadAvatarUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Restore original method
|
||||
avatarRepository.findActiveByDriverId = originalFind;
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateAvatarUseCase - Success Path', () => {
|
||||
it('should update existing avatar for a driver', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver updates existing avatar
|
||||
// Given: A driver exists with an existing avatar
|
||||
// And: Valid new avatar image data is provided
|
||||
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
|
||||
// Then: The old avatar should be replaced with the new one
|
||||
// And: The new avatar should have updated metadata
|
||||
// And: EventPublisher should emit AvatarUpdatedEvent
|
||||
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 with validation requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver updates avatar with validation
|
||||
// Given: A driver exists with an existing avatar
|
||||
// And: New avatar data meets validation requirements
|
||||
// When: UpdateAvatarUseCase.execute() is called
|
||||
// Then: The avatar should be updated successfully
|
||||
// And: EventPublisher should emit AvatarUpdatedEvent
|
||||
});
|
||||
|
||||
it('should update avatar for admin managing driver profile', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates driver avatar
|
||||
// Given: An admin exists
|
||||
// And: A driver exists with an existing avatar
|
||||
// When: UpdateAvatarUseCase.execute() is called with driver ID and new image data
|
||||
// Then: The avatar should be updated in the repository
|
||||
// And: EventPublisher should emit AvatarUpdatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateAvatarUseCase - Validation', () => {
|
||||
it('should reject update with invalid file format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid file format
|
||||
// Given: A driver exists with an existing avatar
|
||||
// And: New avatar data has invalid format
|
||||
// When: UpdateAvatarUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with oversized file', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: File exceeds size limit
|
||||
// Given: A driver exists with an existing avatar
|
||||
// And: New avatar data exceeds maximum file size
|
||||
// When: UpdateAvatarUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteAvatarUseCase - Success Path', () => {
|
||||
it('should delete driver avatar', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver deletes avatar
|
||||
// Given: A driver exists with an existing avatar
|
||||
// When: DeleteAvatarUseCase.execute() is called with driver ID
|
||||
// Then: The avatar should be removed from the repository
|
||||
// And: The driver should have no avatar
|
||||
// And: EventPublisher should emit AvatarDeletedEvent
|
||||
});
|
||||
|
||||
it('should delete avatar for admin managing driver profile', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin deletes driver avatar
|
||||
// Given: An admin exists
|
||||
// And: A driver exists with an existing avatar
|
||||
// When: DeleteAvatarUseCase.execute() is called with driver ID
|
||||
// Then: The avatar should be removed from the repository
|
||||
// And: EventPublisher should emit AvatarDeletedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteAvatarUseCase - Error Handling', () => {
|
||||
it('should handle deletion when driver has no avatar', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver without avatar
|
||||
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: DeleteAvatarUseCase.execute() is called with driver ID
|
||||
// Then: Should complete successfully (no-op)
|
||||
// And: EventPublisher should emit AvatarDeletedEvent
|
||||
});
|
||||
// When: UpdateAvatarUseCase.execute() is called
|
||||
const result = await updateAvatarUseCase.execute({
|
||||
driverId: 'driver-1',
|
||||
mediaUrl: 'https://example.com/avatar.png',
|
||||
});
|
||||
|
||||
it('should throw error when driver does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
// When: DeleteAvatarUseCase.execute() is called with non-existent driver ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// 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('GenerateAvatarFromPhotoUseCase - Success Path', () => {
|
||||
it('should generate avatar from photo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver generates avatar from photo
|
||||
// Given: A driver exists without an avatar
|
||||
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: GenerateAvatarFromPhotoUseCase.execute() is called with driver ID and photo data
|
||||
// Then: An avatar should be generated and stored
|
||||
// And: The generated avatar should have correct metadata
|
||||
// And: EventPublisher should emit AvatarGeneratedEvent
|
||||
// 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 generate avatar with proper image processing', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Avatar generation with image processing
|
||||
it('should request avatar generation with default style', async () => {
|
||||
// Scenario: Driver requests avatar generation with default style
|
||||
// Given: A driver exists
|
||||
// And: Photo data is provided with specific dimensions
|
||||
// When: GenerateAvatarFromPhotoUseCase.execute() is called
|
||||
// Then: The generated avatar should be properly sized and formatted
|
||||
// And: EventPublisher should emit AvatarGeneratedEvent
|
||||
// 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('GenerateAvatarFromPhotoUseCase - Validation', () => {
|
||||
it('should reject generation with invalid photo format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid photo format
|
||||
describe('RequestAvatarGenerationUseCase - Validation', () => {
|
||||
it('should reject generation with invalid face photo', async () => {
|
||||
// Scenario: Invalid face photo
|
||||
// Given: A driver exists
|
||||
// And: Photo data has invalid format
|
||||
// When: GenerateAvatarFromPhotoUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// And: Face validation fails
|
||||
const originalValidate = faceValidation.validateFacePhoto;
|
||||
faceValidation.validateFacePhoto = async () => ({
|
||||
isValid: false,
|
||||
hasFace: false,
|
||||
faceCount: 0,
|
||||
confidence: 0.0,
|
||||
errorMessage: 'No face detected',
|
||||
});
|
||||
|
||||
it('should reject generation with oversized photo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Photo exceeds size limit
|
||||
// Given: A driver exists
|
||||
// And: Photo data exceeds maximum file size
|
||||
// When: GenerateAvatarFromPhotoUseCase.execute() is called
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// 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('Avatar Data Orchestration', () => {
|
||||
it('should correctly format avatar metadata', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Avatar metadata formatting
|
||||
// Given: A driver exists with an avatar
|
||||
// When: GetAvatarUseCase.execute() is called
|
||||
// Then: Avatar metadata should show:
|
||||
// - File size: Correctly formatted (e.g., "2.5 MB")
|
||||
// - File format: Correct format (e.g., "PNG", "JPEG")
|
||||
// - Upload date: Correctly formatted date
|
||||
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 correctly handle avatar caching', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Avatar caching
|
||||
// Given: A driver exists with an avatar
|
||||
// When: GetAvatarUseCase.execute() is called multiple times
|
||||
// Then: Subsequent calls should return cached data
|
||||
// And: EventPublisher should emit AvatarRetrievedEvent for each call
|
||||
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 correctly handle avatar error states', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Avatar error handling
|
||||
// Given: A driver exists
|
||||
// And: AvatarRepository throws an error during retrieval
|
||||
// When: GetAvatarUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user