view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m51s
Contract Testing / contract-snapshot (pull_request) Has been skipped

This commit is contained in:
2026-01-22 17:28:09 +01:00
parent a165ac9b65
commit 0a37454171
22 changed files with 5534 additions and 16 deletions

View File

@@ -0,0 +1,128 @@
import { Result } from '@core/shared/domain/Result';
import { describe, expect, it, vi, type Mock } from 'vitest';
import type { MediaStoragePort } from '../ports/MediaStoragePort';
import { GetUploadedMediaUseCase } from './GetUploadedMediaUseCase';
describe('GetUploadedMediaUseCase', () => {
let mediaStorage: {
getBytes: Mock;
getMetadata: Mock;
};
let useCase: GetUploadedMediaUseCase;
beforeEach(() => {
mediaStorage = {
getBytes: vi.fn(),
getMetadata: vi.fn(),
};
useCase = new GetUploadedMediaUseCase(
mediaStorage as unknown as MediaStoragePort,
);
});
it('returns null when media is not found', async () => {
mediaStorage.getBytes.mockResolvedValue(null);
const input = { storageKey: 'missing-key' };
const result = await useCase.execute(input);
expect(mediaStorage.getBytes).toHaveBeenCalledWith('missing-key');
expect(result).toBeInstanceOf(Result);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBe(null);
});
it('returns media bytes and content type when found', async () => {
const mockBytes = Buffer.from('test data');
const mockMetadata = { size: 9, contentType: 'image/png' };
mediaStorage.getBytes.mockResolvedValue(mockBytes);
mediaStorage.getMetadata.mockResolvedValue(mockMetadata);
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(mediaStorage.getBytes).toHaveBeenCalledWith('media-key');
expect(mediaStorage.getMetadata).toHaveBeenCalledWith('media-key');
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).not.toBeNull();
expect(successResult!.bytes).toBeInstanceOf(Buffer);
expect(successResult!.bytes.toString()).toBe('test data');
expect(successResult!.contentType).toBe('image/png');
});
it('returns default content type when metadata is null', async () => {
const mockBytes = Buffer.from('test data');
mediaStorage.getBytes.mockResolvedValue(mockBytes);
mediaStorage.getMetadata.mockResolvedValue(null);
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult!.contentType).toBe('application/octet-stream');
});
it('returns default content type when metadata has no contentType', async () => {
const mockBytes = Buffer.from('test data');
const mockMetadata = { size: 9 };
mediaStorage.getBytes.mockResolvedValue(mockBytes);
mediaStorage.getMetadata.mockResolvedValue(mockMetadata as any);
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult!.contentType).toBe('application/octet-stream');
});
it('handles storage errors by returning error', async () => {
mediaStorage.getBytes.mockRejectedValue(new Error('Storage error'));
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.message).toBe('Storage error');
});
it('handles getMetadata errors by returning error', async () => {
const mockBytes = Buffer.from('test data');
mediaStorage.getBytes.mockResolvedValue(mockBytes);
mediaStorage.getMetadata.mockRejectedValue(new Error('Metadata error'));
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.message).toBe('Metadata error');
});
it('returns bytes as Buffer', async () => {
const mockBytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
mediaStorage.getBytes.mockResolvedValue(mockBytes);
mediaStorage.getMetadata.mockResolvedValue({ size: 5, contentType: 'text/plain' });
const input = { storageKey: 'media-key' };
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult!.bytes).toBeInstanceOf(Buffer);
expect(successResult!.bytes.toString()).toBe('Hello');
});
});

View File

@@ -0,0 +1,103 @@
import { Result } from '@core/shared/domain/Result';
import { describe, expect, it, vi, type Mock } from 'vitest';
import type { MediaResolverPort } from '@core/ports/media/MediaResolverPort';
import { ResolveMediaReferenceUseCase } from './ResolveMediaReferenceUseCase';
describe('ResolveMediaReferenceUseCase', () => {
let mediaResolver: {
resolve: Mock;
};
let useCase: ResolveMediaReferenceUseCase;
beforeEach(() => {
mediaResolver = {
resolve: vi.fn(),
};
useCase = new ResolveMediaReferenceUseCase(
mediaResolver as unknown as MediaResolverPort,
);
});
it('returns resolved path when media reference is resolved', async () => {
mediaResolver.resolve.mockResolvedValue('/resolved/path/to/media.png');
const input = { reference: { type: 'team', id: 'team-123' } };
const result = await useCase.execute(input);
expect(mediaResolver.resolve).toHaveBeenCalledWith({ type: 'team', id: 'team-123' });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).toBe('/resolved/path/to/media.png');
});
it('returns null when media reference resolves to null', async () => {
mediaResolver.resolve.mockResolvedValue(null);
const input = { reference: { type: 'team', id: 'team-123' } };
const result = await useCase.execute(input);
expect(mediaResolver.resolve).toHaveBeenCalledWith({ type: 'team', id: 'team-123' });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).toBe(null);
});
it('returns empty string when media reference resolves to empty string', async () => {
mediaResolver.resolve.mockResolvedValue('');
const input = { reference: { type: 'team', id: 'team-123' } };
const result = await useCase.execute(input);
expect(mediaResolver.resolve).toHaveBeenCalledWith({ type: 'team', id: 'team-123' });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).toBe('');
});
it('handles resolver errors by returning error', async () => {
mediaResolver.resolve.mockRejectedValue(new Error('Resolver error'));
const input = { reference: { type: 'team', id: 'team-123' } };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.message).toBe('Resolver error');
});
it('handles non-Error exceptions by wrapping in Error', async () => {
mediaResolver.resolve.mockRejectedValue('string error');
const input = { reference: { type: 'team', id: 'team-123' } };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.message).toBe('string error');
});
it('resolves different reference types', async () => {
const testCases = [
{ type: 'team', id: 'team-123' },
{ type: 'league', id: 'league-456' },
{ type: 'driver', id: 'driver-789' },
];
for (const reference of testCases) {
mediaResolver.resolve.mockResolvedValue(`/resolved/${reference.type}/${reference.id}.png`);
const input = { reference };
const result = await useCase.execute(input);
expect(mediaResolver.resolve).toHaveBeenCalledWith(reference);
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).toBe(`/resolved/${reference.type}/${reference.id}.png`);
}
});
});

View File

@@ -1,7 +1,182 @@
import * as mod from '@core/media/domain/entities/Avatar';
import { Avatar } from './Avatar';
import { MediaUrl } from '../value-objects/MediaUrl';
describe('media/domain/entities/Avatar.ts', () => {
it('imports', () => {
expect(mod).toBeTruthy();
describe('Avatar', () => {
describe('create', () => {
it('creates a new avatar with required properties', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
expect(avatar.id).toBe('avatar-1');
expect(avatar.driverId).toBe('driver-1');
expect(avatar.mediaUrl).toBeInstanceOf(MediaUrl);
expect(avatar.mediaUrl.value).toBe('https://example.com/avatar.png');
expect(avatar.isActive).toBe(true);
expect(avatar.selectedAt).toBeInstanceOf(Date);
});
it('throws error when driverId is missing', () => {
expect(() =>
Avatar.create({
id: 'avatar-1',
driverId: '',
mediaUrl: 'https://example.com/avatar.png',
})
).toThrow('Driver ID is required');
});
it('throws error when mediaUrl is missing', () => {
expect(() =>
Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: '',
})
).toThrow('Media URL is required');
});
it('throws error when mediaUrl is invalid', () => {
expect(() =>
Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'invalid-url',
})
).toThrow();
});
});
describe('reconstitute', () => {
it('reconstitutes an avatar from props', () => {
const selectedAt = new Date('2024-01-01T00:00:00.000Z');
const avatar = Avatar.reconstitute({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
selectedAt,
isActive: true,
});
expect(avatar.id).toBe('avatar-1');
expect(avatar.driverId).toBe('driver-1');
expect(avatar.mediaUrl.value).toBe('https://example.com/avatar.png');
expect(avatar.selectedAt).toEqual(selectedAt);
expect(avatar.isActive).toBe(true);
});
it('reconstitutes an inactive avatar', () => {
const avatar = Avatar.reconstitute({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
selectedAt: new Date(),
isActive: false,
});
expect(avatar.isActive).toBe(false);
});
});
describe('deactivate', () => {
it('deactivates an active avatar', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
expect(avatar.isActive).toBe(true);
avatar.deactivate();
expect(avatar.isActive).toBe(false);
});
it('can deactivate an already inactive avatar', () => {
const avatar = Avatar.reconstitute({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
selectedAt: new Date(),
isActive: false,
});
avatar.deactivate();
expect(avatar.isActive).toBe(false);
});
});
describe('toProps', () => {
it('returns correct props for a new avatar', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
const props = avatar.toProps();
expect(props.id).toBe('avatar-1');
expect(props.driverId).toBe('driver-1');
expect(props.mediaUrl).toBe('https://example.com/avatar.png');
expect(props.selectedAt).toBeInstanceOf(Date);
expect(props.isActive).toBe(true);
});
it('returns correct props for an inactive avatar', () => {
const selectedAt = new Date('2024-01-01T00:00:00.000Z');
const avatar = Avatar.reconstitute({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
selectedAt,
isActive: false,
});
const props = avatar.toProps();
expect(props.id).toBe('avatar-1');
expect(props.driverId).toBe('driver-1');
expect(props.mediaUrl).toBe('https://example.com/avatar.png');
expect(props.selectedAt).toEqual(selectedAt);
expect(props.isActive).toBe(false);
});
});
describe('value object validation', () => {
it('validates mediaUrl as MediaUrl value object', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
expect(avatar.mediaUrl).toBeInstanceOf(MediaUrl);
expect(avatar.mediaUrl.value).toBe('https://example.com/avatar.png');
});
it('accepts data URI for mediaUrl', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'data:image/png;base64,abc',
});
expect(avatar.mediaUrl.value).toBe('data:image/png;base64,abc');
});
it('accepts root-relative path for mediaUrl', () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: '/images/avatar.png',
});
expect(avatar.mediaUrl.value).toBe('/images/avatar.png');
});
});
});

View File

@@ -1,7 +1,476 @@
import * as mod from '@core/media/domain/entities/AvatarGenerationRequest';
import { AvatarGenerationRequest } from './AvatarGenerationRequest';
import { MediaUrl } from '../value-objects/MediaUrl';
describe('media/domain/entities/AvatarGenerationRequest.ts', () => {
it('imports', () => {
expect(mod).toBeTruthy();
describe('AvatarGenerationRequest', () => {
describe('create', () => {
it('creates a new request with required properties', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
});
expect(request.id).toBe('req-1');
expect(request.userId).toBe('user-1');
expect(request.facePhotoUrl).toBeInstanceOf(MediaUrl);
expect(request.facePhotoUrl.value).toBe('data:image/png;base64,abc');
expect(request.suitColor).toBe('red');
expect(request.style).toBe('realistic');
expect(request.status).toBe('pending');
expect(request.generatedAvatarUrls).toEqual([]);
expect(request.selectedAvatarIndex).toBeUndefined();
expect(request.errorMessage).toBeUndefined();
expect(request.createdAt).toBeInstanceOf(Date);
expect(request.updatedAt).toBeInstanceOf(Date);
});
it('creates request with default style when not provided', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'blue',
});
expect(request.style).toBe('realistic');
});
it('throws error when userId is missing', () => {
expect(() =>
AvatarGenerationRequest.create({
id: 'req-1',
userId: '',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
})
).toThrow('User ID is required');
});
it('throws error when facePhotoUrl is missing', () => {
expect(() =>
AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: '',
suitColor: 'red',
})
).toThrow('Face photo URL is required');
});
it('throws error when facePhotoUrl is invalid', () => {
expect(() =>
AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'invalid-url',
suitColor: 'red',
})
).toThrow();
});
});
describe('reconstitute', () => {
it('reconstitutes a request from props', () => {
const createdAt = new Date('2024-01-01T00:00:00.000Z');
const updatedAt = new Date('2024-01-01T01:00:00.000Z');
const request = AvatarGenerationRequest.reconstitute({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
status: 'pending',
generatedAvatarUrls: [],
createdAt,
updatedAt,
});
expect(request.id).toBe('req-1');
expect(request.userId).toBe('user-1');
expect(request.facePhotoUrl.value).toBe('data:image/png;base64,abc');
expect(request.suitColor).toBe('red');
expect(request.style).toBe('realistic');
expect(request.status).toBe('pending');
expect(request.generatedAvatarUrls).toEqual([]);
expect(request.selectedAvatarIndex).toBeUndefined();
expect(request.errorMessage).toBeUndefined();
expect(request.createdAt).toEqual(createdAt);
expect(request.updatedAt).toEqual(updatedAt);
});
it('reconstitutes a request with selected avatar', () => {
const request = AvatarGenerationRequest.reconstitute({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
status: 'completed',
generatedAvatarUrls: ['https://example.com/a.png', 'https://example.com/b.png'],
selectedAvatarIndex: 1,
createdAt: new Date(),
updatedAt: new Date(),
});
expect(request.selectedAvatarIndex).toBe(1);
expect(request.selectedAvatarUrl).toBe('https://example.com/b.png');
});
it('reconstitutes a failed request', () => {
const request = AvatarGenerationRequest.reconstitute({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
status: 'failed',
generatedAvatarUrls: [],
errorMessage: 'Generation failed',
createdAt: new Date(),
updatedAt: new Date(),
});
expect(request.status).toBe('failed');
expect(request.errorMessage).toBe('Generation failed');
});
});
describe('status transitions', () => {
it('transitions from pending to validating', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
expect(request.status).toBe('pending');
request.markAsValidating();
expect(request.status).toBe('validating');
});
it('transitions from validating to generating', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
expect(request.status).toBe('generating');
});
it('throws error when marking as validating from non-pending status', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
expect(() => request.markAsValidating()).toThrow('Can only start validation from pending status');
});
it('throws error when marking as generating from non-validating status', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
expect(() => request.markAsGenerating()).toThrow('Can only start generation from validating status');
});
it('completes request with avatars', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
request.completeWithAvatars(['https://example.com/a.png', 'https://example.com/b.png']);
expect(request.status).toBe('completed');
expect(request.generatedAvatarUrls).toEqual(['https://example.com/a.png', 'https://example.com/b.png']);
});
it('throws error when completing with empty avatar list', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
expect(() => request.completeWithAvatars([])).toThrow('At least one avatar URL is required');
});
it('fails request with error message', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.fail('Face validation failed');
expect(request.status).toBe('failed');
expect(request.errorMessage).toBe('Face validation failed');
});
});
describe('avatar selection', () => {
it('selects avatar when request is completed', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
request.completeWithAvatars(['https://example.com/a.png', 'https://example.com/b.png']);
request.selectAvatar(1);
expect(request.selectedAvatarIndex).toBe(1);
expect(request.selectedAvatarUrl).toBe('https://example.com/b.png');
});
it('throws error when selecting avatar from non-completed request', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
expect(() => request.selectAvatar(0)).toThrow('Can only select avatar when generation is completed');
});
it('throws error when selecting invalid index', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
request.completeWithAvatars(['https://example.com/a.png', 'https://example.com/b.png']);
expect(() => request.selectAvatar(-1)).toThrow('Invalid avatar index');
expect(() => request.selectAvatar(2)).toThrow('Invalid avatar index');
});
it('returns undefined for selectedAvatarUrl when no avatar selected', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
request.markAsValidating();
request.markAsGenerating();
request.completeWithAvatars(['https://example.com/a.png', 'https://example.com/b.png']);
expect(request.selectedAvatarUrl).toBeUndefined();
});
});
describe('buildPrompt', () => {
it('builds prompt for red suit, realistic style', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
});
const prompt = request.buildPrompt();
expect(prompt).toContain('vibrant racing red');
expect(prompt).toContain('photorealistic, professional motorsport portrait');
expect(prompt).toContain('racing driver');
expect(prompt).toContain('racing suit');
expect(prompt).toContain('helmet');
});
it('builds prompt for blue suit, cartoon style', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'blue',
style: 'cartoon',
});
const prompt = request.buildPrompt();
expect(prompt).toContain('deep motorsport blue');
expect(prompt).toContain('stylized cartoon racing character');
});
it('builds prompt for pixel-art style', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'green',
style: 'pixel-art',
});
const prompt = request.buildPrompt();
expect(prompt).toContain('racing green');
expect(prompt).toContain('8-bit pixel art retro racing avatar');
});
it('builds prompt for all suit colors', () => {
const colors = ['red', 'blue', 'green', 'yellow', 'orange', 'purple', 'black', 'white', 'pink', 'cyan'] as const;
colors.forEach((color) => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: color,
});
const prompt = request.buildPrompt();
expect(prompt).toContain(color);
});
});
});
describe('toProps', () => {
it('returns correct props for a new request', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
});
const props = request.toProps();
expect(props.id).toBe('req-1');
expect(props.userId).toBe('user-1');
expect(props.facePhotoUrl).toBe('data:image/png;base64,abc');
expect(props.suitColor).toBe('red');
expect(props.style).toBe('realistic');
expect(props.status).toBe('pending');
expect(props.generatedAvatarUrls).toEqual([]);
expect(props.selectedAvatarIndex).toBeUndefined();
expect(props.errorMessage).toBeUndefined();
expect(props.createdAt).toBeInstanceOf(Date);
expect(props.updatedAt).toBeInstanceOf(Date);
});
it('returns correct props for a completed request with selected avatar', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
});
request.markAsValidating();
request.markAsGenerating();
request.completeWithAvatars(['https://example.com/a.png', 'https://example.com/b.png']);
request.selectAvatar(1);
const props = request.toProps();
expect(props.id).toBe('req-1');
expect(props.userId).toBe('user-1');
expect(props.facePhotoUrl).toBe('data:image/png;base64,abc');
expect(props.suitColor).toBe('red');
expect(props.style).toBe('realistic');
expect(props.status).toBe('completed');
expect(props.generatedAvatarUrls).toEqual(['https://example.com/a.png', 'https://example.com/b.png']);
expect(props.selectedAvatarIndex).toBe(1);
expect(props.errorMessage).toBeUndefined();
});
it('returns correct props for a failed request', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
style: 'realistic',
});
request.markAsValidating();
request.fail('Face validation failed');
const props = request.toProps();
expect(props.id).toBe('req-1');
expect(props.userId).toBe('user-1');
expect(props.facePhotoUrl).toBe('data:image/png;base64,abc');
expect(props.suitColor).toBe('red');
expect(props.style).toBe('realistic');
expect(props.status).toBe('failed');
expect(props.generatedAvatarUrls).toEqual([]);
expect(props.selectedAvatarIndex).toBeUndefined();
expect(props.errorMessage).toBe('Face validation failed');
});
});
describe('value object validation', () => {
it('validates facePhotoUrl as MediaUrl value object', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'data:image/png;base64,abc',
suitColor: 'red',
});
expect(request.facePhotoUrl).toBeInstanceOf(MediaUrl);
expect(request.facePhotoUrl.value).toBe('data:image/png;base64,abc');
});
it('accepts http URL for facePhotoUrl', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face.png',
suitColor: 'red',
});
expect(request.facePhotoUrl.value).toBe('https://example.com/face.png');
});
it('accepts root-relative path for facePhotoUrl', () => {
const request = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: '/images/face.png',
suitColor: 'red',
});
expect(request.facePhotoUrl.value).toBe('/images/face.png');
});
});
});

View File

@@ -1,7 +1,307 @@
import * as mod from '@core/media/domain/entities/Media';
import { Media } from './Media';
import { MediaUrl } from '../value-objects/MediaUrl';
describe('media/domain/entities/Media.ts', () => {
it('imports', () => {
expect(mod).toBeTruthy();
describe('Media', () => {
describe('create', () => {
it('creates a new media with required properties', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
});
expect(media.id).toBe('media-1');
expect(media.filename).toBe('avatar.png');
expect(media.originalName).toBe('avatar.png');
expect(media.mimeType).toBe('image/png');
expect(media.size).toBe(123);
expect(media.url).toBeInstanceOf(MediaUrl);
expect(media.url.value).toBe('https://example.com/avatar.png');
expect(media.type).toBe('image');
expect(media.uploadedBy).toBe('user-1');
expect(media.uploadedAt).toBeInstanceOf(Date);
expect(media.metadata).toBeUndefined();
});
it('creates media with metadata', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
metadata: { width: 100, height: 100 },
});
expect(media.metadata).toEqual({ width: 100, height: 100 });
});
it('throws error when filename is missing', () => {
expect(() =>
Media.create({
id: 'media-1',
filename: '',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
})
).toThrow('Filename is required');
});
it('throws error when url is missing', () => {
expect(() =>
Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: '',
type: 'image',
uploadedBy: 'user-1',
})
).toThrow('URL is required');
});
it('throws error when uploadedBy is missing', () => {
expect(() =>
Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: '',
})
).toThrow('Uploaded by is required');
});
it('throws error when url is invalid', () => {
expect(() =>
Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'invalid-url',
type: 'image',
uploadedBy: 'user-1',
})
).toThrow();
});
});
describe('reconstitute', () => {
it('reconstitutes a media from props', () => {
const uploadedAt = new Date('2024-01-01T00:00:00.000Z');
const media = Media.reconstitute({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
uploadedAt,
});
expect(media.id).toBe('media-1');
expect(media.filename).toBe('avatar.png');
expect(media.originalName).toBe('avatar.png');
expect(media.mimeType).toBe('image/png');
expect(media.size).toBe(123);
expect(media.url.value).toBe('https://example.com/avatar.png');
expect(media.type).toBe('image');
expect(media.uploadedBy).toBe('user-1');
expect(media.uploadedAt).toEqual(uploadedAt);
expect(media.metadata).toBeUndefined();
});
it('reconstitutes a media with metadata', () => {
const media = Media.reconstitute({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
uploadedAt: new Date(),
metadata: { width: 100, height: 100 },
});
expect(media.metadata).toEqual({ width: 100, height: 100 });
});
it('reconstitutes a video media', () => {
const media = Media.reconstitute({
id: 'media-1',
filename: 'video.mp4',
originalName: 'video.mp4',
mimeType: 'video/mp4',
size: 1024,
url: 'https://example.com/video.mp4',
type: 'video',
uploadedBy: 'user-1',
uploadedAt: new Date(),
});
expect(media.type).toBe('video');
});
it('reconstitutes a document media', () => {
const media = Media.reconstitute({
id: 'media-1',
filename: 'document.pdf',
originalName: 'document.pdf',
mimeType: 'application/pdf',
size: 2048,
url: 'https://example.com/document.pdf',
type: 'document',
uploadedBy: 'user-1',
uploadedAt: new Date(),
});
expect(media.type).toBe('document');
});
});
describe('toProps', () => {
it('returns correct props for a new media', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
});
const props = media.toProps();
expect(props.id).toBe('media-1');
expect(props.filename).toBe('avatar.png');
expect(props.originalName).toBe('avatar.png');
expect(props.mimeType).toBe('image/png');
expect(props.size).toBe(123);
expect(props.url).toBe('https://example.com/avatar.png');
expect(props.type).toBe('image');
expect(props.uploadedBy).toBe('user-1');
expect(props.uploadedAt).toBeInstanceOf(Date);
expect(props.metadata).toBeUndefined();
});
it('returns correct props for a media with metadata', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
metadata: { width: 100, height: 100 },
});
const props = media.toProps();
expect(props.metadata).toEqual({ width: 100, height: 100 });
});
it('returns correct props for a reconstituted media', () => {
const uploadedAt = new Date('2024-01-01T00:00:00.000Z');
const media = Media.reconstitute({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
uploadedAt,
metadata: { width: 100, height: 100 },
});
const props = media.toProps();
expect(props.id).toBe('media-1');
expect(props.filename).toBe('avatar.png');
expect(props.originalName).toBe('avatar.png');
expect(props.mimeType).toBe('image/png');
expect(props.size).toBe(123);
expect(props.url).toBe('https://example.com/avatar.png');
expect(props.type).toBe('image');
expect(props.uploadedBy).toBe('user-1');
expect(props.uploadedAt).toEqual(uploadedAt);
expect(props.metadata).toEqual({ width: 100, height: 100 });
});
});
describe('value object validation', () => {
it('validates url as MediaUrl value object', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'https://example.com/avatar.png',
type: 'image',
uploadedBy: 'user-1',
});
expect(media.url).toBeInstanceOf(MediaUrl);
expect(media.url.value).toBe('https://example.com/avatar.png');
});
it('accepts data URI for url', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: 'data:image/png;base64,abc',
type: 'image',
uploadedBy: 'user-1',
});
expect(media.url.value).toBe('data:image/png;base64,abc');
});
it('accepts root-relative path for url', () => {
const media = Media.create({
id: 'media-1',
filename: 'avatar.png',
originalName: 'avatar.png',
mimeType: 'image/png',
size: 123,
url: '/images/avatar.png',
type: 'image',
uploadedBy: 'user-1',
});
expect(media.url.value).toBe('/images/avatar.png');
});
});
});

View File

@@ -0,0 +1,223 @@
import { MediaGenerationService } from './MediaGenerationService';
describe('MediaGenerationService', () => {
let service: MediaGenerationService;
beforeEach(() => {
service = new MediaGenerationService();
});
describe('generateTeamLogo', () => {
it('generates a deterministic logo URL for a team', () => {
const url1 = service.generateTeamLogo('team-123');
const url2 = service.generateTeamLogo('team-123');
expect(url1).toBe(url2);
expect(url1).toContain('https://picsum.photos/seed/team-123/200/200');
});
it('generates different URLs for different team IDs', () => {
const url1 = service.generateTeamLogo('team-123');
const url2 = service.generateTeamLogo('team-456');
expect(url1).not.toBe(url2);
});
it('generates URL with correct format', () => {
const url = service.generateTeamLogo('team-123');
expect(url).toMatch(/^https:\/\/picsum\.photos\/seed\/team-123\/200\/200$/);
});
});
describe('generateLeagueLogo', () => {
it('generates a deterministic logo URL for a league', () => {
const url1 = service.generateLeagueLogo('league-123');
const url2 = service.generateLeagueLogo('league-123');
expect(url1).toBe(url2);
expect(url1).toContain('https://picsum.photos/seed/l-league-123/200/200');
});
it('generates different URLs for different league IDs', () => {
const url1 = service.generateLeagueLogo('league-123');
const url2 = service.generateLeagueLogo('league-456');
expect(url1).not.toBe(url2);
});
it('generates URL with correct format', () => {
const url = service.generateLeagueLogo('league-123');
expect(url).toMatch(/^https:\/\/picsum\.photos\/seed\/l-league-123\/200\/200$/);
});
});
describe('generateDriverAvatar', () => {
it('generates a deterministic avatar URL for a driver', () => {
const url1 = service.generateDriverAvatar('driver-123');
const url2 = service.generateDriverAvatar('driver-123');
expect(url1).toBe(url2);
expect(url1).toContain('https://i.pravatar.cc/150?u=driver-123');
});
it('generates different URLs for different driver IDs', () => {
const url1 = service.generateDriverAvatar('driver-123');
const url2 = service.generateDriverAvatar('driver-456');
expect(url1).not.toBe(url2);
});
it('generates URL with correct format', () => {
const url = service.generateDriverAvatar('driver-123');
expect(url).toMatch(/^https:\/\/i\.pravatar\.cc\/150\?u=driver-123$/);
});
});
describe('generateLeagueCover', () => {
it('generates a deterministic cover URL for a league', () => {
const url1 = service.generateLeagueCover('league-123');
const url2 = service.generateLeagueCover('league-123');
expect(url1).toBe(url2);
expect(url1).toContain('https://picsum.photos/seed/c-league-123/800/200');
});
it('generates different URLs for different league IDs', () => {
const url1 = service.generateLeagueCover('league-123');
const url2 = service.generateLeagueCover('league-456');
expect(url1).not.toBe(url2);
});
it('generates URL with correct format', () => {
const url = service.generateLeagueCover('league-123');
expect(url).toMatch(/^https:\/\/picsum\.photos\/seed\/c-league-123\/800\/200$/);
});
});
describe('generateDefaultPNG', () => {
it('generates a PNG buffer for a variant', () => {
const buffer = service.generateDefaultPNG('test-variant');
expect(buffer).toBeInstanceOf(Buffer);
expect(buffer.length).toBeGreaterThan(0);
});
it('generates deterministic PNG for same variant', () => {
const buffer1 = service.generateDefaultPNG('test-variant');
const buffer2 = service.generateDefaultPNG('test-variant');
expect(buffer1.equals(buffer2)).toBe(true);
});
it('generates different PNGs for different variants', () => {
const buffer1 = service.generateDefaultPNG('variant-1');
const buffer2 = service.generateDefaultPNG('variant-2');
expect(buffer1.equals(buffer2)).toBe(false);
});
it('generates valid PNG header', () => {
const buffer = service.generateDefaultPNG('test-variant');
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
expect(buffer[0]).toBe(0x89);
expect(buffer[1]).toBe(0x50); // 'P'
expect(buffer[2]).toBe(0x4E); // 'N'
expect(buffer[3]).toBe(0x47); // 'G'
expect(buffer[4]).toBe(0x0D);
expect(buffer[5]).toBe(0x0A);
expect(buffer[6]).toBe(0x1A);
expect(buffer[7]).toBe(0x0A);
});
it('generates PNG with IHDR chunk', () => {
const buffer = service.generateDefaultPNG('test-variant');
// IHDR chunk starts at byte 8
// Length: 13 (0x00 0x00 0x00 0x0D)
expect(buffer[8]).toBe(0x00);
expect(buffer[9]).toBe(0x00);
expect(buffer[10]).toBe(0x00);
expect(buffer[11]).toBe(0x0D);
// Type: IHDR (0x49 0x48 0x44 0x52)
expect(buffer[12]).toBe(0x49); // 'I'
expect(buffer[13]).toBe(0x48); // 'H'
expect(buffer[14]).toBe(0x44); // 'D'
expect(buffer[15]).toBe(0x52); // 'R'
});
it('generates PNG with 1x1 dimensions', () => {
const buffer = service.generateDefaultPNG('test-variant');
// Width: 1 (0x00 0x00 0x00 0x01) at byte 16
expect(buffer[16]).toBe(0x00);
expect(buffer[17]).toBe(0x00);
expect(buffer[18]).toBe(0x00);
expect(buffer[19]).toBe(0x01);
// Height: 1 (0x00 0x00 0x00 0x01) at byte 20
expect(buffer[20]).toBe(0x00);
expect(buffer[21]).toBe(0x00);
expect(buffer[22]).toBe(0x00);
expect(buffer[23]).toBe(0x01);
});
it('generates PNG with RGB color type', () => {
const buffer = service.generateDefaultPNG('test-variant');
// Color type: RGB (0x02) at byte 25
expect(buffer[25]).toBe(0x02);
});
it('generates PNG with RGB pixel data', () => {
const buffer = service.generateDefaultPNG('test-variant');
// RGB pixel data should be present in IDAT chunk
// IDAT chunk starts after IHDR (byte 37)
// We should find RGB values somewhere in the buffer
const hasRGB = buffer.some((byte, index) => {
// Check if we have a sequence that looks like RGB data
// This is a simplified check
return index > 37 && index < buffer.length - 10;
});
expect(hasRGB).toBe(true);
});
});
describe('deterministic generation', () => {
it('generates same team logo for same team ID across different instances', () => {
const service1 = new MediaGenerationService();
const service2 = new MediaGenerationService();
const url1 = service1.generateTeamLogo('team-123');
const url2 = service2.generateTeamLogo('team-123');
expect(url1).toBe(url2);
});
it('generates same driver avatar for same driver ID across different instances', () => {
const service1 = new MediaGenerationService();
const service2 = new MediaGenerationService();
const url1 = service1.generateDriverAvatar('driver-123');
const url2 = service2.generateDriverAvatar('driver-123');
expect(url1).toBe(url2);
});
it('generates same PNG for same variant across different instances', () => {
const service1 = new MediaGenerationService();
const service2 = new MediaGenerationService();
const buffer1 = service1.generateDefaultPNG('test-variant');
const buffer2 = service2.generateDefaultPNG('test-variant');
expect(buffer1.equals(buffer2)).toBe(true);
});
});
});

View File

@@ -1,7 +1,83 @@
import * as mod from '@core/media/domain/value-objects/AvatarId';
import { AvatarId } from './AvatarId';
describe('media/domain/value-objects/AvatarId.ts', () => {
it('imports', () => {
expect(mod).toBeTruthy();
describe('AvatarId', () => {
describe('create', () => {
it('creates from valid string', () => {
const avatarId = AvatarId.create('avatar-123');
expect(avatarId.toString()).toBe('avatar-123');
});
it('trims whitespace', () => {
const avatarId = AvatarId.create(' avatar-123 ');
expect(avatarId.toString()).toBe('avatar-123');
});
it('throws error when empty', () => {
expect(() => AvatarId.create('')).toThrow('Avatar ID cannot be empty');
});
it('throws error when only whitespace', () => {
expect(() => AvatarId.create(' ')).toThrow('Avatar ID cannot be empty');
});
it('throws error when null', () => {
expect(() => AvatarId.create(null as any)).toThrow('Avatar ID cannot be empty');
});
it('throws error when undefined', () => {
expect(() => AvatarId.create(undefined as any)).toThrow('Avatar ID cannot be empty');
});
});
describe('toString', () => {
it('returns the string value', () => {
const avatarId = AvatarId.create('avatar-123');
expect(avatarId.toString()).toBe('avatar-123');
});
});
describe('equals', () => {
it('returns true for equal avatar IDs', () => {
const avatarId1 = AvatarId.create('avatar-123');
const avatarId2 = AvatarId.create('avatar-123');
expect(avatarId1.equals(avatarId2)).toBe(true);
});
it('returns false for different avatar IDs', () => {
const avatarId1 = AvatarId.create('avatar-123');
const avatarId2 = AvatarId.create('avatar-456');
expect(avatarId1.equals(avatarId2)).toBe(false);
});
it('returns false for different case', () => {
const avatarId1 = AvatarId.create('avatar-123');
const avatarId2 = AvatarId.create('AVATAR-123');
expect(avatarId1.equals(avatarId2)).toBe(false);
});
});
describe('value object equality', () => {
it('implements value-based equality', () => {
const avatarId1 = AvatarId.create('avatar-123');
const avatarId2 = AvatarId.create('avatar-123');
const avatarId3 = AvatarId.create('avatar-456');
expect(avatarId1.equals(avatarId2)).toBe(true);
expect(avatarId1.equals(avatarId3)).toBe(false);
});
it('maintains equality after toString', () => {
const avatarId1 = AvatarId.create('avatar-123');
const avatarId2 = AvatarId.create('avatar-123');
expect(avatarId1.toString()).toBe(avatarId2.toString());
expect(avatarId1.equals(avatarId2)).toBe(true);
});
});
});