harden media

This commit is contained in:
2025-12-31 15:39:28 +01:00
parent 92226800df
commit 8260bf7baf
413 changed files with 8361 additions and 1544 deletions

View File

@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from './InMemoryDriverRepository';
import { Driver } from '@core/racing/domain/entities/Driver';
import type { Logger } from '@core/shared/application';
import { MediaReference } from '@core/domain/media/MediaReference';
describe('InMemoryDriverRepository', () => {
let repository: InMemoryDriverRepository;
@@ -17,13 +18,23 @@ describe('InMemoryDriverRepository', () => {
repository = new InMemoryDriverRepository(mockLogger);
});
const createTestDriver = (id: string, iracingId: string, name: string, country: string) => {
return Driver.create({
const createTestDriver = (id: string, iracingId: string, name: string, country: string, avatarRef?: MediaReference) => {
const props: {
id: string;
iracingId: string;
name: string;
country: string;
avatarRef?: MediaReference;
} = {
id,
iracingId,
name,
country,
});
};
if (avatarRef !== undefined) {
props.avatarRef = avatarRef;
}
return Driver.create(props);
};
describe('constructor', () => {
@@ -188,4 +199,115 @@ describe('InMemoryDriverRepository', () => {
expect(result).toBe(false);
});
});
describe('serialization with MediaReference', () => {
it('should serialize driver with uploaded avatarRef', async () => {
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createUploaded('media-123'));
await repository.create(driver);
const serialized = repository.serialize(driver);
expect(serialized.avatarRef).toEqual({ type: 'uploaded', mediaId: 'media-123' });
});
it('should serialize driver with system-default avatarRef', async () => {
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createSystemDefault('avatar'));
await repository.create(driver);
const serialized = repository.serialize(driver);
expect(serialized.avatarRef).toEqual({ type: 'system-default', variant: 'avatar' });
});
it('should serialize driver with generated avatarRef', async () => {
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createGenerated('gen-123'));
await repository.create(driver);
const serialized = repository.serialize(driver);
expect(serialized.avatarRef).toEqual({ type: 'generated', generationRequestId: 'gen-123' });
});
it('should deserialize driver with uploaded avatarRef', () => {
const data = {
id: '1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
bio: null,
joinedAt: new Date().toISOString(),
category: null,
avatarRef: { type: 'uploaded', mediaId: 'media-123' },
};
const driver = repository.deserialize(data);
expect(driver.id).toBe('1');
expect(driver.avatarRef.type).toBe('uploaded');
expect(driver.avatarRef.mediaId).toBe('media-123');
});
it('should deserialize driver with system-default avatarRef', () => {
const data = {
id: '1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
bio: null,
joinedAt: new Date().toISOString(),
category: null,
avatarRef: { type: 'system-default', variant: 'avatar' },
};
const driver = repository.deserialize(data);
expect(driver.id).toBe('1');
expect(driver.avatarRef.type).toBe('system-default');
expect(driver.avatarRef.variant).toBe('avatar');
});
it('should deserialize driver with generated avatarRef', () => {
const data = {
id: '1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
bio: null,
joinedAt: new Date().toISOString(),
category: null,
avatarRef: { type: 'generated', generationRequestId: 'gen-123' },
};
const driver = repository.deserialize(data);
expect(driver.id).toBe('1');
expect(driver.avatarRef.type).toBe('generated');
expect(driver.avatarRef.generationRequestId).toBe('gen-123');
});
it('should deserialize driver without avatarRef (backward compatibility)', () => {
const data = {
id: '1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
bio: null,
joinedAt: new Date().toISOString(),
category: null,
};
const driver = repository.deserialize(data);
expect(driver.id).toBe('1');
expect(driver.avatarRef.type).toBe('system-default');
expect(driver.avatarRef.variant).toBe('avatar');
});
it('should roundtrip serialize and deserialize with avatarRef', async () => {
const originalDriver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createUploaded('media-456'));
await repository.create(originalDriver);
const serialized = repository.serialize(originalDriver);
const deserialized = repository.deserialize(serialized);
expect(deserialized.id).toBe(originalDriver.id);
expect(deserialized.iracingId.toString()).toBe(originalDriver.iracingId.toString());
expect(deserialized.name.toString()).toBe(originalDriver.name.toString());
expect(deserialized.country.toString()).toBe(originalDriver.country.toString());
expect(deserialized.avatarRef.equals(originalDriver.avatarRef)).toBe(true);
});
});
});

View File

@@ -1,6 +1,7 @@
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { Driver } from '@core/racing/domain/entities/Driver';
import { Logger } from '@core/shared/application';
import { MediaReference } from '@core/domain/media/MediaReference';
export class InMemoryDriverRepository implements IDriverRepository {
private drivers: Map<string, Driver> = new Map();
@@ -91,4 +92,49 @@ export class InMemoryDriverRepository implements IDriverRepository {
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with iRacing ID: ${iracingId}`);
return Promise.resolve(this.iracingIdIndex.has(iracingId));
}
}
// Serialization methods for persistence
serialize(driver: Driver): Record<string, unknown> {
return {
id: driver.id,
iracingId: driver.iracingId.toString(),
name: driver.name.toString(),
country: driver.country.toString(),
bio: driver.bio?.toString() ?? null,
joinedAt: driver.joinedAt.toDate().toISOString(),
category: driver.category ?? null,
avatarRef: driver.avatarRef.toJSON(),
};
}
deserialize(data: Record<string, unknown>): Driver {
const props: {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt: Date;
category?: string;
avatarRef?: MediaReference;
} = {
id: data.id as string,
iracingId: data.iracingId as string,
name: data.name as string,
country: data.country as string,
joinedAt: new Date(data.joinedAt as string),
};
if (data.bio !== null && data.bio !== undefined) {
props.bio = data.bio as string;
}
if (data.category !== null && data.category !== undefined) {
props.category = data.category as string;
}
if (data.avatarRef !== null && data.avatarRef !== undefined) {
props.avatarRef = MediaReference.fromJSON(data.avatarRef as Record<string, unknown>);
}
return Driver.rehydrate(props);
}
}

View File

@@ -1,6 +1,7 @@
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { League } from '@core/racing/domain/entities/League';
import { Logger } from '@core/shared/application';
import { MediaReference } from '@core/domain/media/MediaReference';
export class InMemoryLeagueRepository implements ILeagueRepository {
private leagues: Map<string, League> = new Map();
@@ -132,4 +133,71 @@ export class InMemoryLeagueRepository implements ILeagueRepository {
throw error;
}
}
}
// Serialization methods for persistence
serialize(league: League): Record<string, unknown> {
return {
id: league.id.toString(),
name: league.name.toString(),
description: league.description.toString(),
ownerId: league.ownerId.toString(),
settings: league.settings,
category: league.category ?? null,
createdAt: league.createdAt.toDate().toISOString(),
participantCount: league.getParticipantCount(),
socialLinks: league.socialLinks
? {
discordUrl: league.socialLinks.discordUrl,
youtubeUrl: league.socialLinks.youtubeUrl,
websiteUrl: league.socialLinks.websiteUrl,
}
: undefined,
logoRef: league.logoRef.toJSON(),
};
}
deserialize(data: Record<string, unknown>): League {
const props: {
id: string;
name: string;
description: string;
ownerId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: any;
category?: string;
createdAt: Date;
participantCount: number;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
logoRef?: MediaReference;
} = {
id: data.id as string,
name: data.name as string,
description: data.description as string,
ownerId: data.ownerId as string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: data.settings as any,
createdAt: new Date(data.createdAt as string),
participantCount: data.participantCount as number,
};
if (data.category !== null && data.category !== undefined) {
props.category = data.category as string;
}
if (data.socialLinks !== null && data.socialLinks !== undefined) {
props.socialLinks = data.socialLinks as {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
}
if (data.logoRef !== null && data.logoRef !== undefined) {
props.logoRef = MediaReference.fromJSON(data.logoRef as Record<string, unknown>);
}
return League.rehydrate(props);
}
}

View File

@@ -5,9 +5,10 @@
* Stores data in a Map structure.
*/
import type { Team } from '@core/racing/domain/entities/Team';
import { Team } from '@core/racing/domain/entities/Team';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { Logger } from '@core/shared/application';
import { MediaReference } from '@core/domain/media/MediaReference';
export class InMemoryTeamRepository implements ITeamRepository {
private teams: Map<string, Team>;
@@ -122,4 +123,53 @@ export class InMemoryTeamRepository implements ITeamRepository {
throw error;
}
}
// Serialization methods for persistence
serialize(team: Team): Record<string, unknown> {
return {
id: team.id,
name: team.name.toString(),
tag: team.tag.toString(),
description: team.description.toString(),
ownerId: team.ownerId.toString(),
leagues: team.leagues.map(l => l.toString()),
category: team.category ?? null,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toDate().toISOString(),
logoRef: team.logoRef.toJSON(),
};
}
deserialize(data: Record<string, unknown>): Team {
const props: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
category?: string;
isRecruiting: boolean;
createdAt: Date;
logoRef?: MediaReference;
} = {
id: data.id as string,
name: data.name as string,
tag: data.tag as string,
description: data.description as string,
ownerId: data.ownerId as string,
leagues: data.leagues as string[],
isRecruiting: data.isRecruiting as boolean,
createdAt: new Date(data.createdAt as string),
};
if (data.category !== null && data.category !== undefined) {
props.category = data.category as string;
}
if (data.logoRef !== null && data.logoRef !== undefined) {
props.logoRef = MediaReference.fromJSON(data.logoRef as Record<string, unknown>);
}
return Team.rehydrate(props);
}
}