integration tests
This commit is contained in:
93
adapters/media/events/InMemoryMediaEventPublisher.ts
Normal file
93
adapters/media/events/InMemoryMediaEventPublisher.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryMediaEventPublisher
|
||||
*
|
||||
* In-memory implementation of MediaEventPublisher for testing purposes.
|
||||
* Stores events in memory for verification in integration tests.
|
||||
*/
|
||||
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { DomainEvent } from '@core/shared/domain/DomainEvent';
|
||||
|
||||
export interface MediaEvent {
|
||||
eventType: string;
|
||||
aggregateId: string;
|
||||
eventData: unknown;
|
||||
occurredAt: Date;
|
||||
}
|
||||
|
||||
export class InMemoryMediaEventPublisher {
|
||||
private events: MediaEvent[] = [];
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('[InMemoryMediaEventPublisher] Initialized.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a domain event
|
||||
*/
|
||||
async publish(event: DomainEvent): Promise<void> {
|
||||
this.logger.debug(`[InMemoryMediaEventPublisher] Publishing event: ${event.eventType} for aggregate: ${event.aggregateId}`);
|
||||
|
||||
const mediaEvent: MediaEvent = {
|
||||
eventType: event.eventType,
|
||||
aggregateId: event.aggregateId,
|
||||
eventData: event.eventData,
|
||||
occurredAt: event.occurredAt,
|
||||
};
|
||||
|
||||
this.events.push(mediaEvent);
|
||||
this.logger.info(`Event ${event.eventType} published successfully.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published events
|
||||
*/
|
||||
getEvents(): MediaEvent[] {
|
||||
return [...this.events];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by event type
|
||||
*/
|
||||
getEventsByType(eventType: string): MediaEvent[] {
|
||||
return this.events.filter(event => event.eventType === eventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by aggregate ID
|
||||
*/
|
||||
getEventsByAggregateId(aggregateId: string): MediaEvent[] {
|
||||
return this.events.filter(event => event.aggregateId === aggregateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of events
|
||||
*/
|
||||
getEventCount(): number {
|
||||
return this.events.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all events
|
||||
*/
|
||||
clear(): void {
|
||||
this.events = [];
|
||||
this.logger.info('[InMemoryMediaEventPublisher] All events cleared.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event of a specific type was published
|
||||
*/
|
||||
hasEvent(eventType: string): boolean {
|
||||
return this.events.some(event => event.eventType === eventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event was published for a specific aggregate
|
||||
*/
|
||||
hasEventForAggregate(eventType: string, aggregateId: string): boolean {
|
||||
return this.events.some(
|
||||
event => event.eventType === eventType && event.aggregateId === aggregateId
|
||||
);
|
||||
}
|
||||
}
|
||||
121
adapters/media/persistence/inmemory/InMemoryAvatarRepository.ts
Normal file
121
adapters/media/persistence/inmemory/InMemoryAvatarRepository.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryAvatarRepository
|
||||
*
|
||||
* In-memory implementation of AvatarRepository for testing purposes.
|
||||
* Stores avatar entities in memory for fast, deterministic testing.
|
||||
*/
|
||||
|
||||
import type { Avatar } from '@core/media/domain/entities/Avatar';
|
||||
import type { AvatarRepository } from '@core/media/domain/repositories/AvatarRepository';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
|
||||
export class InMemoryAvatarRepository implements AvatarRepository {
|
||||
private avatars: Map<string, Avatar> = new Map();
|
||||
private driverAvatars: Map<string, Avatar[]> = new Map();
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('[InMemoryAvatarRepository] Initialized.');
|
||||
}
|
||||
|
||||
async save(avatar: Avatar): Promise<void> {
|
||||
this.logger.debug(`[InMemoryAvatarRepository] Saving avatar: ${avatar.id} for driver: ${avatar.driverId}`);
|
||||
|
||||
// Store by ID
|
||||
this.avatars.set(avatar.id, avatar);
|
||||
|
||||
// Store by driver ID
|
||||
if (!this.driverAvatars.has(avatar.driverId)) {
|
||||
this.driverAvatars.set(avatar.driverId, []);
|
||||
}
|
||||
|
||||
const driverAvatars = this.driverAvatars.get(avatar.driverId)!;
|
||||
const existingIndex = driverAvatars.findIndex(a => a.id === avatar.id);
|
||||
|
||||
if (existingIndex > -1) {
|
||||
driverAvatars[existingIndex] = avatar;
|
||||
} else {
|
||||
driverAvatars.push(avatar);
|
||||
}
|
||||
|
||||
this.logger.info(`Avatar ${avatar.id} for driver ${avatar.driverId} saved successfully.`);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Avatar | null> {
|
||||
this.logger.debug(`[InMemoryAvatarRepository] Finding avatar by ID: ${id}`);
|
||||
const avatar = this.avatars.get(id) ?? null;
|
||||
|
||||
if (avatar) {
|
||||
this.logger.info(`Found avatar by ID: ${id}`);
|
||||
} else {
|
||||
this.logger.warn(`Avatar with ID ${id} not found.`);
|
||||
}
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
async findActiveByDriverId(driverId: string): Promise<Avatar | null> {
|
||||
this.logger.debug(`[InMemoryAvatarRepository] Finding active avatar for driver: ${driverId}`);
|
||||
|
||||
const driverAvatars = this.driverAvatars.get(driverId) ?? [];
|
||||
const activeAvatar = driverAvatars.find(avatar => avatar.isActive) ?? null;
|
||||
|
||||
if (activeAvatar) {
|
||||
this.logger.info(`Found active avatar for driver ${driverId}: ${activeAvatar.id}`);
|
||||
} else {
|
||||
this.logger.warn(`No active avatar found for driver: ${driverId}`);
|
||||
}
|
||||
|
||||
return activeAvatar;
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<Avatar[]> {
|
||||
this.logger.debug(`[InMemoryAvatarRepository] Finding all avatars for driver: ${driverId}`);
|
||||
|
||||
const driverAvatars = this.driverAvatars.get(driverId) ?? [];
|
||||
this.logger.info(`Found ${driverAvatars.length} avatars for driver ${driverId}.`);
|
||||
|
||||
return driverAvatars;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`[InMemoryAvatarRepository] Deleting avatar with ID: ${id}`);
|
||||
|
||||
const avatarToDelete = this.avatars.get(id);
|
||||
if (!avatarToDelete) {
|
||||
this.logger.warn(`Avatar with ID ${id} not found for deletion.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from avatars map
|
||||
this.avatars.delete(id);
|
||||
|
||||
// Remove from driver avatars
|
||||
const driverAvatars = this.driverAvatars.get(avatarToDelete.driverId);
|
||||
if (driverAvatars) {
|
||||
const filtered = driverAvatars.filter(avatar => avatar.id !== id);
|
||||
if (filtered.length > 0) {
|
||||
this.driverAvatars.set(avatarToDelete.driverId, filtered);
|
||||
} else {
|
||||
this.driverAvatars.delete(avatarToDelete.driverId);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Avatar ${id} deleted successfully.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all avatars from the repository
|
||||
*/
|
||||
clear(): void {
|
||||
this.avatars.clear();
|
||||
this.driverAvatars.clear();
|
||||
this.logger.info('[InMemoryAvatarRepository] All avatars cleared.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of avatars stored
|
||||
*/
|
||||
get size(): number {
|
||||
return this.avatars.size;
|
||||
}
|
||||
}
|
||||
106
adapters/media/persistence/inmemory/InMemoryMediaRepository.ts
Normal file
106
adapters/media/persistence/inmemory/InMemoryMediaRepository.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryMediaRepository
|
||||
*
|
||||
* In-memory implementation of MediaRepository for testing purposes.
|
||||
* Stores media entities in memory for fast, deterministic testing.
|
||||
*/
|
||||
|
||||
import type { Media } from '@core/media/domain/entities/Media';
|
||||
import type { MediaRepository } from '@core/media/domain/repositories/MediaRepository';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
|
||||
export class InMemoryMediaRepository implements MediaRepository {
|
||||
private media: Map<string, Media> = new Map();
|
||||
private uploadedByMedia: Map<string, Media[]> = new Map();
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('[InMemoryMediaRepository] Initialized.');
|
||||
}
|
||||
|
||||
async save(media: Media): Promise<void> {
|
||||
this.logger.debug(`[InMemoryMediaRepository] Saving media: ${media.id} for uploader: ${media.uploadedBy}`);
|
||||
|
||||
// Store by ID
|
||||
this.media.set(media.id, media);
|
||||
|
||||
// Store by uploader
|
||||
if (!this.uploadedByMedia.has(media.uploadedBy)) {
|
||||
this.uploadedByMedia.set(media.uploadedBy, []);
|
||||
}
|
||||
|
||||
const uploaderMedia = this.uploadedByMedia.get(media.uploadedBy)!;
|
||||
const existingIndex = uploaderMedia.findIndex(m => m.id === media.id);
|
||||
|
||||
if (existingIndex > -1) {
|
||||
uploaderMedia[existingIndex] = media;
|
||||
} else {
|
||||
uploaderMedia.push(media);
|
||||
}
|
||||
|
||||
this.logger.info(`Media ${media.id} for uploader ${media.uploadedBy} saved successfully.`);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Media | null> {
|
||||
this.logger.debug(`[InMemoryMediaRepository] Finding media by ID: ${id}`);
|
||||
const media = this.media.get(id) ?? null;
|
||||
|
||||
if (media) {
|
||||
this.logger.info(`Found media by ID: ${id}`);
|
||||
} else {
|
||||
this.logger.warn(`Media with ID ${id} not found.`);
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
async findByUploadedBy(uploadedBy: string): Promise<Media[]> {
|
||||
this.logger.debug(`[InMemoryMediaRepository] Finding all media for uploader: ${uploadedBy}`);
|
||||
|
||||
const uploaderMedia = this.uploadedByMedia.get(uploadedBy) ?? [];
|
||||
this.logger.info(`Found ${uploaderMedia.length} media files for uploader ${uploadedBy}.`);
|
||||
|
||||
return uploaderMedia;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`[InMemoryMediaRepository] Deleting media with ID: ${id}`);
|
||||
|
||||
const mediaToDelete = this.media.get(id);
|
||||
if (!mediaToDelete) {
|
||||
this.logger.warn(`Media with ID ${id} not found for deletion.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from media map
|
||||
this.media.delete(id);
|
||||
|
||||
// Remove from uploader media
|
||||
const uploaderMedia = this.uploadedByMedia.get(mediaToDelete.uploadedBy);
|
||||
if (uploaderMedia) {
|
||||
const filtered = uploaderMedia.filter(media => media.id !== id);
|
||||
if (filtered.length > 0) {
|
||||
this.uploadedByMedia.set(mediaToDelete.uploadedBy, filtered);
|
||||
} else {
|
||||
this.uploadedByMedia.delete(mediaToDelete.uploadedBy);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Media ${id} deleted successfully.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all media from the repository
|
||||
*/
|
||||
clear(): void {
|
||||
this.media.clear();
|
||||
this.uploadedByMedia.clear();
|
||||
this.logger.info('[InMemoryMediaRepository] All media cleared.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of media files stored
|
||||
*/
|
||||
get size(): number {
|
||||
return this.media.size;
|
||||
}
|
||||
}
|
||||
109
adapters/media/ports/InMemoryMediaStorageAdapter.ts
Normal file
109
adapters/media/ports/InMemoryMediaStorageAdapter.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryMediaStorageAdapter
|
||||
*
|
||||
* In-memory implementation of MediaStoragePort for testing purposes.
|
||||
* Simulates file storage without actual filesystem operations.
|
||||
*/
|
||||
|
||||
import type { MediaStoragePort, UploadOptions, UploadResult } from '@core/media/application/ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
|
||||
export class InMemoryMediaStorageAdapter implements MediaStoragePort {
|
||||
private storage: Map<string, Buffer> = new Map();
|
||||
private metadata: Map<string, { size: number; contentType: string }> = new Map();
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('[InMemoryMediaStorageAdapter] Initialized.');
|
||||
}
|
||||
|
||||
async uploadMedia(buffer: Buffer, options: UploadOptions): Promise<UploadResult> {
|
||||
this.logger.debug(`[InMemoryMediaStorageAdapter] Uploading media: ${options.filename}`);
|
||||
|
||||
// Validate content type
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/gif'];
|
||||
if (!allowedTypes.includes(options.mimeType)) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: `Content type ${options.mimeType} is not allowed`,
|
||||
};
|
||||
}
|
||||
|
||||
// Generate storage key
|
||||
const storageKey = `uploaded/${Date.now()}-${options.filename.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
|
||||
|
||||
// Store buffer and metadata
|
||||
this.storage.set(storageKey, buffer);
|
||||
this.metadata.set(storageKey, {
|
||||
size: buffer.length,
|
||||
contentType: options.mimeType,
|
||||
});
|
||||
|
||||
this.logger.info(`Media uploaded successfully: ${storageKey}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename: options.filename,
|
||||
url: storageKey,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteMedia(storageKey: string): Promise<void> {
|
||||
this.logger.debug(`[InMemoryMediaStorageAdapter] Deleting media: ${storageKey}`);
|
||||
|
||||
this.storage.delete(storageKey);
|
||||
this.metadata.delete(storageKey);
|
||||
|
||||
this.logger.info(`Media deleted successfully: ${storageKey}`);
|
||||
}
|
||||
|
||||
async getBytes(storageKey: string): Promise<Buffer | null> {
|
||||
this.logger.debug(`[InMemoryMediaStorageAdapter] Getting bytes for: ${storageKey}`);
|
||||
|
||||
const buffer = this.storage.get(storageKey) ?? null;
|
||||
|
||||
if (buffer) {
|
||||
this.logger.info(`Retrieved bytes for: ${storageKey}`);
|
||||
} else {
|
||||
this.logger.warn(`No bytes found for: ${storageKey}`);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async getMetadata(storageKey: string): Promise<{ size: number; contentType: string } | null> {
|
||||
this.logger.debug(`[InMemoryMediaStorageAdapter] Getting metadata for: ${storageKey}`);
|
||||
|
||||
const meta = this.metadata.get(storageKey) ?? null;
|
||||
|
||||
if (meta) {
|
||||
this.logger.info(`Retrieved metadata for: ${storageKey}`);
|
||||
} else {
|
||||
this.logger.warn(`No metadata found for: ${storageKey}`);
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored media
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
this.metadata.clear();
|
||||
this.logger.info('[InMemoryMediaStorageAdapter] All media cleared.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of stored media files
|
||||
*/
|
||||
get size(): number {
|
||||
return this.storage.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a storage key exists
|
||||
*/
|
||||
has(storageKey: string): boolean {
|
||||
return this.storage.has(storageKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user