harden media
This commit is contained in:
82
adapters/bootstrap/racing/RacingDriverFactory.test.ts
Normal file
82
adapters/bootstrap/racing/RacingDriverFactory.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { RacingDriverFactory } from './RacingDriverFactory';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
describe('RacingDriverFactory', () => {
|
||||
describe('getDriverAvatarRef', () => {
|
||||
it('should return deterministic MediaReference based on driver ID', () => {
|
||||
const factory = new RacingDriverFactory(10, new Date(), 'inmemory');
|
||||
|
||||
// Test deterministic behavior
|
||||
const ref1 = factory.getDriverAvatarRef('driver-1');
|
||||
const ref2 = factory.getDriverAvatarRef('driver-1');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
expect(ref1.type).toBe('system-default');
|
||||
expect(ref1.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('should produce different refs for different IDs', () => {
|
||||
const factory = new RacingDriverFactory(10, new Date(), 'inmemory');
|
||||
|
||||
const ref1 = factory.getDriverAvatarRef('driver-1');
|
||||
const ref2 = factory.getDriverAvatarRef('driver-2');
|
||||
|
||||
// They should be different refs (though both are system-default avatar)
|
||||
// The hash will be different
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should use hash % 3 for variant selection', () => {
|
||||
const factory = new RacingDriverFactory(10, new Date(), 'inmemory');
|
||||
|
||||
// Test multiple IDs to ensure distribution
|
||||
const refs = [
|
||||
factory.getDriverAvatarRef('driver-1'),
|
||||
factory.getDriverAvatarRef('driver-2'),
|
||||
factory.getDriverAvatarRef('driver-3'),
|
||||
factory.getDriverAvatarRef('driver-4'),
|
||||
factory.getDriverAvatarRef('driver-5'),
|
||||
];
|
||||
|
||||
// All should be system-default avatar
|
||||
refs.forEach(ref => {
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create drivers with avatarRef set', () => {
|
||||
const factory = new RacingDriverFactory(5, new Date(), 'inmemory');
|
||||
const drivers = factory.create();
|
||||
|
||||
expect(drivers.length).toBe(5);
|
||||
|
||||
drivers.forEach(driver => {
|
||||
expect(driver.avatarRef).toBeDefined();
|
||||
expect(driver.avatarRef instanceof MediaReference).toBe(true);
|
||||
expect(driver.avatarRef.type).toBe('system-default');
|
||||
expect(driver.avatarRef.variant).toBe('avatar');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create deterministic drivers', () => {
|
||||
const factory1 = new RacingDriverFactory(3, new Date('2024-01-01'), 'inmemory');
|
||||
const factory2 = new RacingDriverFactory(3, new Date('2024-01-01'), 'inmemory');
|
||||
|
||||
const drivers1 = factory1.create();
|
||||
const drivers2 = factory2.create();
|
||||
|
||||
expect(drivers1.length).toBe(drivers2.length);
|
||||
|
||||
for (let i = 0; i < drivers1.length; i++) {
|
||||
const driver1 = drivers1[i]!;
|
||||
const driver2 = drivers2[i]!;
|
||||
expect(driver1.id).toBe(driver2.id);
|
||||
expect(driver1.avatarRef.equals(driver2.avatarRef)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
@@ -26,22 +27,23 @@ export class RacingDriverFactory {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get deterministic avatar URL for a driver based on their ID
|
||||
* Uses static files from the website public directory
|
||||
* Get deterministic MediaReference for a driver's avatar based on their ID
|
||||
* Uses hash % 3 to determine variant: 0 -> male, 1 -> female, 2 -> neutral
|
||||
*/
|
||||
getDriverAvatarUrl(driverId: string): string {
|
||||
// Deterministic selection based on driver ID
|
||||
getDriverAvatarRef(driverId: string): MediaReference {
|
||||
// Deterministic selection based on driver ID hash
|
||||
const numericSuffixMatch = driverId.match(/(\d+)$/);
|
||||
let useFemale = false;
|
||||
let useNeutral = false;
|
||||
let avatarVariant: 'male' | 'female' | 'neutral';
|
||||
|
||||
if (numericSuffixMatch && numericSuffixMatch[1]) {
|
||||
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
|
||||
// 40% female, 40% male, 20% neutral
|
||||
if (numericSuffix % 5 === 0) {
|
||||
useNeutral = true;
|
||||
} else if (numericSuffix % 2 === 0) {
|
||||
useFemale = true;
|
||||
const hashMod = numericSuffix % 3;
|
||||
if (hashMod === 0) {
|
||||
avatarVariant = 'male';
|
||||
} else if (hashMod === 1) {
|
||||
avatarVariant = 'female';
|
||||
} else {
|
||||
avatarVariant = 'neutral';
|
||||
}
|
||||
} else {
|
||||
// Fallback hash
|
||||
@@ -49,22 +51,18 @@ export class RacingDriverFactory {
|
||||
for (let i = 0; i < driverId.length; i++) {
|
||||
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
|
||||
}
|
||||
const hashValue = Math.abs(hash);
|
||||
if (hashValue % 5 === 0) {
|
||||
useNeutral = true;
|
||||
} else if (hashValue % 2 === 0) {
|
||||
useFemale = true;
|
||||
const hashMod = Math.abs(hash) % 3;
|
||||
if (hashMod === 0) {
|
||||
avatarVariant = 'male';
|
||||
} else if (hashMod === 1) {
|
||||
avatarVariant = 'female';
|
||||
} else {
|
||||
avatarVariant = 'neutral';
|
||||
}
|
||||
}
|
||||
|
||||
// Return static file paths that Next.js can serve
|
||||
if (useNeutral) {
|
||||
return '/images/avatars/neutral-default-avatar.jpeg';
|
||||
} else if (useFemale) {
|
||||
return '/images/avatars/female-default-avatar.jpeg';
|
||||
} else {
|
||||
return '/images/avatars/male-default-avatar.jpg';
|
||||
}
|
||||
// Create system-default reference with avatar variant
|
||||
return MediaReference.systemDefault(avatarVariant);
|
||||
}
|
||||
|
||||
create(): Driver[] {
|
||||
@@ -99,6 +97,8 @@ export class RacingDriverFactory {
|
||||
// Assign category - use all available categories
|
||||
const category = faker.helpers.arrayElement(categories);
|
||||
|
||||
const driverId = seedId(`driver-${i}`, this.persistence);
|
||||
|
||||
const driverData: {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
@@ -107,13 +107,15 @@ export class RacingDriverFactory {
|
||||
bio?: string;
|
||||
joinedAt?: Date;
|
||||
category?: string;
|
||||
avatarRef: MediaReference;
|
||||
} = {
|
||||
id: seedId(`driver-${i}`, this.persistence),
|
||||
id: driverId,
|
||||
iracingId: String(100000 + i),
|
||||
name: faker.person.fullName(),
|
||||
country: faker.helpers.arrayElement(countries),
|
||||
joinedAt,
|
||||
category,
|
||||
avatarRef: this.getDriverAvatarRef(driverId),
|
||||
};
|
||||
|
||||
if (hasBio) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { League, LeagueSettings } from '@core/racing/domain/entities/League';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
@@ -389,6 +390,7 @@ export class RacingLeagueFactory {
|
||||
websiteUrl?: string;
|
||||
};
|
||||
participantCount?: number;
|
||||
logoRef?: MediaReference;
|
||||
} = {
|
||||
id: leagueData.id,
|
||||
name: leagueData.name,
|
||||
@@ -398,6 +400,7 @@ export class RacingLeagueFactory {
|
||||
category: leagueData.category,
|
||||
createdAt: leagueData.createdAt,
|
||||
participantCount: leagueData.participantCount,
|
||||
logoRef: MediaReference.generated('league', leagueData.id),
|
||||
};
|
||||
|
||||
if (Object.keys(socialLinks).length > 0) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class RacingSponsorFactory {
|
||||
id: seedId('demo-sponsor-1', this.persistence),
|
||||
name: 'GridPilot Sim Racing Supply',
|
||||
contactEmail: 'partnerships@gridpilot.example',
|
||||
logoUrl: 'http://localhost:3000/images/header.jpeg',
|
||||
logoUrl: 'http://localhost:3001/images/header.jpeg',
|
||||
websiteUrl: 'https://gridpilot.example/sponsors/gridpilot-sim-racing-supply',
|
||||
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
||||
});
|
||||
@@ -74,13 +74,13 @@ export class RacingSponsorFactory {
|
||||
];
|
||||
|
||||
const logoPaths = [
|
||||
'http://localhost:3000/images/header.jpeg',
|
||||
'http://localhost:3000/images/ff1600.jpeg',
|
||||
'http://localhost:3000/images/avatars/male-default-avatar.jpg',
|
||||
'http://localhost:3000/images/avatars/female-default-avatar.jpeg',
|
||||
'http://localhost:3000/images/avatars/neutral-default-avatar.jpeg',
|
||||
'http://localhost:3000/images/leagues/placeholder-cover.svg',
|
||||
'http://localhost:3000/favicon.svg',
|
||||
'http://localhost:3001/images/header.jpeg',
|
||||
'http://localhost:3001/images/ff1600.jpeg',
|
||||
'http://localhost:3001/images/avatars/male-default-avatar.jpg',
|
||||
'http://localhost:3001/images/avatars/female-default-avatar.jpeg',
|
||||
'http://localhost:3001/images/avatars/neutral-default-avatar.jpeg',
|
||||
'http://localhost:3001/images/leagues/placeholder-cover.svg',
|
||||
'http://localhost:3001/favicon.svg',
|
||||
];
|
||||
|
||||
const websiteUrls = [
|
||||
|
||||
53
adapters/bootstrap/racing/RacingTeamFactory.test.ts
Normal file
53
adapters/bootstrap/racing/RacingTeamFactory.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { RacingTeamFactory } from './RacingTeamFactory';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
import { RacingDriverFactory } from './RacingDriverFactory';
|
||||
import { RacingLeagueFactory } from './RacingLeagueFactory';
|
||||
|
||||
describe('RacingTeamFactory', () => {
|
||||
describe('createTeams', () => {
|
||||
it('should create teams with generated logoRef', () => {
|
||||
const baseDate = new Date();
|
||||
const driverFactory = new RacingDriverFactory(10, baseDate, 'inmemory');
|
||||
const leagueFactory = new RacingLeagueFactory(baseDate, driverFactory.create(), 'inmemory');
|
||||
const teamFactory = new RacingTeamFactory(baseDate, 'inmemory');
|
||||
|
||||
const drivers = driverFactory.create();
|
||||
const leagues = leagueFactory.create();
|
||||
const teams = teamFactory.createTeams(drivers, leagues);
|
||||
|
||||
expect(teams.length).toBeGreaterThan(0);
|
||||
|
||||
teams.forEach(team => {
|
||||
expect(team.logoRef).toBeDefined();
|
||||
expect(team.logoRef instanceof MediaReference).toBe(true);
|
||||
expect(team.logoRef.type).toBe('generated');
|
||||
expect(team.logoRef.generationRequestId).toBe(`team-${team.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create deterministic teams', () => {
|
||||
const baseDate = new Date('2024-01-01');
|
||||
const driverFactory = new RacingDriverFactory(5, baseDate, 'inmemory');
|
||||
const leagueFactory = new RacingLeagueFactory(baseDate, driverFactory.create(), 'inmemory');
|
||||
|
||||
const drivers = driverFactory.create();
|
||||
const leagues = leagueFactory.create();
|
||||
|
||||
const teamFactory1 = new RacingTeamFactory(baseDate, 'inmemory');
|
||||
const teamFactory2 = new RacingTeamFactory(baseDate, 'inmemory');
|
||||
|
||||
const teams1 = teamFactory1.createTeams(drivers, leagues);
|
||||
const teams2 = teamFactory2.createTeams(drivers, leagues);
|
||||
|
||||
expect(teams1.length).toBe(teams2.length);
|
||||
|
||||
for (let i = 0; i < teams1.length; i++) {
|
||||
const team1 = teams1[i]!;
|
||||
const team2 = teams2[i]!;
|
||||
expect(team1.id).toBe(team2.id);
|
||||
expect(team1.logoRef.equals(team2.logoRef)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
import type { TeamJoinRequest, TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export interface TeamStats {
|
||||
logoUrl: string;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
specialization: 'endurance' | 'sprint' | 'mixed';
|
||||
region: string;
|
||||
@@ -37,8 +37,10 @@ export class RacingTeamFactory {
|
||||
// 30-50% of teams are recruiting
|
||||
const isRecruiting = faker.datatype.boolean({ probability: 0.4 });
|
||||
|
||||
const teamId = seedId(`team-${i}`, this.persistence);
|
||||
|
||||
return Team.create({
|
||||
id: seedId(`team-${i}`, this.persistence),
|
||||
id: teamId,
|
||||
name: faker.company.name() + ' Racing',
|
||||
tag: faker.string.alpha({ length: 4, casing: 'upper' }),
|
||||
description: faker.lorem.sentences(2),
|
||||
@@ -46,6 +48,7 @@ export class RacingTeamFactory {
|
||||
leagues: teamLeagues,
|
||||
isRecruiting,
|
||||
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
||||
logoRef: MediaReference.generated('team', teamId),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -200,16 +203,6 @@ export class RacingTeamFactory {
|
||||
generateTeamStats(teams: Team[]): Map<string, TeamStats> {
|
||||
const statsMap = new Map<string, TeamStats>();
|
||||
|
||||
// Available logo URLs (simulating media uploads)
|
||||
const logoUrls = [
|
||||
'/images/ff1600.jpeg',
|
||||
'/images/header.jpeg',
|
||||
'/images/avatars/male-default-avatar.jpg',
|
||||
'/images/avatars/female-default-avatar.jpeg',
|
||||
'/images/avatars/neutral-default-avatar.jpeg',
|
||||
'/images/leagues/placeholder-cover.svg',
|
||||
];
|
||||
|
||||
// Available regions
|
||||
const regions = ['Europe', 'North America', 'South America', 'Asia', 'Oceania', 'Africa'];
|
||||
|
||||
@@ -270,11 +263,7 @@ export class RacingTeamFactory {
|
||||
const languageCount = faker.number.int({ min: 1, max: 3 });
|
||||
const languages = faker.helpers.arrayElements(allLanguages, languageCount);
|
||||
|
||||
// Generate logo URL (varied)
|
||||
const logoUrl = logoUrls[i % logoUrls.length] ?? logoUrls[0];
|
||||
|
||||
statsMap.set(team.id.toString(), {
|
||||
logoUrl: logoUrl!,
|
||||
performanceLevel,
|
||||
specialization,
|
||||
region,
|
||||
|
||||
Reference in New Issue
Block a user