/** * In-Memory Media Resolver Adapter * * Stub implementation for testing purposes. * Resolves MediaReference objects to fake URLs without external dependencies. * * Part of the adapters layer, implementing the MediaResolverPort interface. */ import { MediaResolverPort } from '@core/ports/media/MediaResolverPort'; import { MediaReference } from '@core/domain/media/MediaReference'; /** * Configuration for InMemoryMediaResolverAdapter */ export interface InMemoryMediaResolverConfig { /** * Base URL to use for generated URLs * @default 'https://fake-media.example.com' */ baseUrl?: string; /** * Whether to simulate network delays * @default false */ simulateDelay?: boolean; /** * Delay in milliseconds when simulateDelay is true * @default 50 */ delayMs?: number; /** * Whether to return null for certain reference types (simulating missing media) * @default false */ simulateMissingMedia?: boolean; } /** * In-Memory Media Resolver Adapter * * Stub implementation that resolves media references to fake URLs. * Designed for use in tests and development environments. * * @example * ```typescript * const adapter = new InMemoryMediaResolverAdapter({ * baseUrl: 'https://test.example.com', * simulateDelay: true * }); * * const ref = MediaReference.createSystemDefault('avatar'); * const url = await adapter.resolve(ref); * // Returns: '/media/default/male-default-avatar.png' * ``` */ export class InMemoryMediaResolverAdapter implements MediaResolverPort { private readonly config: Required; constructor(config: InMemoryMediaResolverConfig = {}) { this.config = { baseUrl: config.baseUrl ?? 'https://fake-media.example.com', simulateDelay: config.simulateDelay ?? false, delayMs: config.delayMs ?? 50, simulateMissingMedia: config.simulateMissingMedia ?? false, }; } /** * Resolve a media reference to a path-only URL * * @param ref - The media reference to resolve * @returns Promise resolving to path string or null */ async resolve(ref: MediaReference): Promise { // Simulate network delay if configured if (this.config.simulateDelay) { await this.delay(this.config.delayMs); } // Simulate missing media for some cases if (this.config.simulateMissingMedia && this.shouldReturnNull()) { return null; } switch (ref.type) { case 'system-default': let filename: string; if (ref.variant === 'avatar' && ref.avatarVariant) { filename = `${ref.avatarVariant}-default-avatar.png`; } else if (ref.variant === 'avatar') { filename = `neutral-default-avatar.png`; } else { filename = `${ref.variant}.png`; } return `/media/default/${filename}`; case 'generated': // Parse the generationRequestId to extract type and id // Format: "{type}-{id}" where id can contain hyphens if (ref.generationRequestId) { const firstHyphenIndex = ref.generationRequestId.indexOf('-'); if (firstHyphenIndex !== -1) { const type = ref.generationRequestId.substring(0, firstHyphenIndex); const id = ref.generationRequestId.substring(firstHyphenIndex + 1); // Use the correct API routes if (type === 'team') { return `/media/teams/${id}/logo`; } else if (type === 'league') { return `/media/leagues/${id}/logo`; } else if (type === 'driver') { return `/media/avatar/${id}`; } // Fallback for other types return `/media/generated/${type}/${id}`; } } // Fallback for unexpected format return null; case 'uploaded': return `/media/uploaded/${ref.mediaId}`; case 'none': return null; default: return null; } } /** * Simulate network delay */ private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Determine if this reference should return null (simulating missing media) */ private shouldReturnNull(): boolean { // Randomly return null for 20% of cases return Math.random() < 0.2; } /** * Get the configured base URL */ getBaseUrl(): string { return this.config.baseUrl; } /** * Update configuration */ updateConfig(config: Partial): void { Object.assign(this.config, config); } /** * Reset to default configuration */ reset(): void { this.config.baseUrl = 'https://fake-media.example.com'; this.config.simulateDelay = false; this.config.delayMs = 50; this.config.simulateMissingMedia = false; } } /** * Factory function to create a configured in-memory resolver */ export function createInMemoryResolver( config: InMemoryMediaResolverConfig = {} ): MediaResolverPort { return new InMemoryMediaResolverAdapter(config); } /** * Pre-configured resolver for common test scenarios */ export const TestResolvers = { /** * Fast resolver with no delays */ fast: () => new InMemoryMediaResolverAdapter({ baseUrl: 'https://test.example.com', simulateDelay: false, }), /** * Slow resolver that simulates network latency */ slow: () => new InMemoryMediaResolverAdapter({ baseUrl: 'https://test.example.com', simulateDelay: true, delayMs: 200, }), /** * Unreliable resolver that sometimes returns null */ unreliable: () => new InMemoryMediaResolverAdapter({ baseUrl: 'https://test.example.com', simulateMissingMedia: true, }), /** * Custom base URL resolver */ withBaseUrl: (baseUrl: string) => new InMemoryMediaResolverAdapter({ baseUrl, simulateDelay: false, }), /** * Local development resolver */ local: () => new InMemoryMediaResolverAdapter({ baseUrl: 'http://localhost:3000/media', simulateDelay: false, }), } as const;