harden media
This commit is contained in:
229
adapters/media/MediaResolverInMemoryAdapter.ts
Normal file
229
adapters/media/MediaResolverInMemoryAdapter.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 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<InMemoryMediaResolverConfig>;
|
||||
|
||||
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<string | null> {
|
||||
// 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<void> {
|
||||
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<InMemoryMediaResolverConfig>): 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;
|
||||
Reference in New Issue
Block a user