more seeds

This commit is contained in:
2025-12-27 10:43:55 +01:00
parent 58d9a1c762
commit 91612e4256
16 changed files with 1713 additions and 72 deletions

View File

@@ -46,6 +46,13 @@ export const BootstrapProviders: Provider[] = [
useFactory: (
driverRepository: RacingSeedDependencies['driverRepository'],
leagueRepository: RacingSeedDependencies['leagueRepository'],
seasonRepository: RacingSeedDependencies['seasonRepository'],
seasonSponsorshipRepository: RacingSeedDependencies['seasonSponsorshipRepository'],
sponsorshipRequestRepository: RacingSeedDependencies['sponsorshipRequestRepository'],
leagueWalletRepository: RacingSeedDependencies['leagueWalletRepository'],
transactionRepository: RacingSeedDependencies['transactionRepository'],
protestRepository: RacingSeedDependencies['protestRepository'],
penaltyRepository: RacingSeedDependencies['penaltyRepository'],
raceRepository: RacingSeedDependencies['raceRepository'],
resultRepository: RacingSeedDependencies['resultRepository'],
standingRepository: RacingSeedDependencies['standingRepository'],
@@ -53,11 +60,19 @@ export const BootstrapProviders: Provider[] = [
raceRegistrationRepository: RacingSeedDependencies['raceRegistrationRepository'],
teamRepository: RacingSeedDependencies['teamRepository'],
teamMembershipRepository: RacingSeedDependencies['teamMembershipRepository'],
sponsorRepository: RacingSeedDependencies['sponsorRepository'],
feedRepository: RacingSeedDependencies['feedRepository'],
socialGraphRepository: RacingSeedDependencies['socialGraphRepository'],
): RacingSeedDependencies => ({
driverRepository,
leagueRepository,
seasonRepository,
seasonSponsorshipRepository,
sponsorshipRequestRepository,
leagueWalletRepository,
transactionRepository,
protestRepository,
penaltyRepository,
raceRepository,
resultRepository,
standingRepository,
@@ -65,12 +80,20 @@ export const BootstrapProviders: Provider[] = [
raceRegistrationRepository,
teamRepository,
teamMembershipRepository,
sponsorRepository,
feedRepository,
socialGraphRepository,
}),
inject: [
'IDriverRepository',
'ILeagueRepository',
'ISeasonRepository',
'ISeasonSponsorshipRepository',
'ISponsorshipRequestRepository',
'ILeagueWalletRepository',
'ITransactionRepository',
'IProtestRepository',
'IPenaltyRepository',
'IRaceRepository',
'IResultRepository',
'IStandingRepository',
@@ -78,6 +101,7 @@ export const BootstrapProviders: Provider[] = [
'IRaceRegistrationRepository',
'ITeamRepository',
'ITeamMembershipRepository',
'ISponsorRepository',
'IFeedRepository',
'ISocialGraphRepository',
],

View File

@@ -0,0 +1,224 @@
import { describe, expect, it } from 'vitest';
import { createRacingSeed } from '@adapters/bootstrap/racing/RacingSeed';
describe('Racing seed (bootstrap)', () => {
it('creates a large, internally consistent seed', () => {
const seed = createRacingSeed();
expect(seed.drivers.length).toBeGreaterThanOrEqual(50);
expect(seed.leagues.length).toBeGreaterThanOrEqual(10);
expect(seed.races.length).toBeGreaterThan(0);
const driverIds = new Set(seed.drivers.map((d) => d.id.toString()));
const leagueIds = new Set(seed.leagues.map((l) => l.id.toString()));
const teamIds = new Set(seed.teams.map((t) => t.id.toString()));
const raceById = new Map(seed.races.map((r) => [r.id.toString(), r] as const));
const sponsorIds = new Set(seed.sponsors.map((s) => s.id.toString()));
const seasonById = new Map(seed.seasons.map((s) => [s.id, s] as const));
const walletById = new Map(seed.leagueWallets.map((w) => [w.id.toString(), w] as const));
const protestById = new Map(seed.protests.map((p) => [p.id, p] as const));
// Deterministic demo IDs used by the website
expect(seed.sponsors.some((s) => s.id.toString() === 'demo-sponsor-1')).toBe(true);
// Seasons + sponsorship ecosystem
expect(seed.seasons.some((s) => s.id === 'season-1')).toBe(true);
expect(seed.seasons.some((s) => s.id === 'season-2')).toBe(true);
// Referential integrity: seasons must reference real leagues
for (const season of seed.seasons) {
expect(leagueIds.has(season.leagueId)).toBe(true);
}
// Referential integrity: season sponsorships must reference real season + sponsor
for (const sponsorship of seed.seasonSponsorships) {
expect(seasonById.has(sponsorship.seasonId)).toBe(true);
expect(sponsorIds.has(sponsorship.sponsorId)).toBe(true);
const season = seasonById.get(sponsorship.seasonId);
if (season && sponsorship.leagueId !== undefined) {
expect(sponsorship.leagueId).toBe(season.leagueId);
}
}
// Referential integrity: sponsorship requests must reference a real sponsor and target entity
for (const request of seed.sponsorshipRequests) {
expect(sponsorIds.has(request.sponsorId)).toBe(true);
if (request.entityType === 'season') {
expect(seasonById.has(request.entityId)).toBe(true);
} else if (request.entityType === 'team') {
expect(teamIds.has(request.entityId)).toBe(true);
} else if (request.entityType === 'race') {
expect(raceById.has(request.entityId)).toBe(true);
} else if (request.entityType === 'driver') {
expect(driverIds.has(request.entityId)).toBe(true);
}
}
const season1PendingRequests = seed.sponsorshipRequests.filter(
(r) => r.entityType === 'season' && r.entityId === 'season-1' && r.status === 'pending',
);
expect(season1PendingRequests.length).toBeGreaterThan(0);
// Wallet edge cases:
// - some leagues have no wallet at all
expect(seed.leagueWallets.some((w) => w.leagueId.toString() === 'league-2')).toBe(false);
expect(seed.leagueWallets.some((w) => w.leagueId.toString() === 'league-7')).toBe(false);
// - some wallets have no transactions (league-3 is intentionally quiet)
const league3Wallet = seed.leagueWallets.find((w) => w.leagueId.toString() === 'league-3');
if (league3Wallet) {
const league3TxCount = seed.leagueWalletTransactions.filter(
(tx) => tx.walletId.toString() === league3Wallet.id.toString(),
).length;
expect(league3TxCount).toBe(0);
}
// - demo wallet has a pending prize payout
const league5Wallet = seed.leagueWallets.find((w) => w.leagueId.toString() === 'league-5');
expect(league5Wallet).toBeDefined();
if (league5Wallet) {
const league5PendingPrizes = seed.leagueWalletTransactions.filter(
(tx) =>
tx.walletId.toString() === league5Wallet.id.toString() &&
tx.type === 'prize_payout' &&
tx.status === 'pending',
);
expect(league5PendingPrizes.length).toBeGreaterThan(0);
}
// Referential integrity: all transactions reference an existing wallet
for (const tx of seed.leagueWalletTransactions) {
expect(walletById.has(tx.walletId.toString())).toBe(true);
}
// Wallet invariant safety: no completed debit causes an overdraft when applied in factory order.
for (const wallet of seed.leagueWallets) {
const walletId = wallet.id.toString();
const completed = seed.leagueWalletTransactions
.filter((t) => t.walletId.toString() === walletId && t.status === 'completed')
.filter((t) => t.type !== 'prize_payout');
const sum = (values: number[]) => values.reduce((acc, v) => acc + v, 0);
const credits = completed.filter((t) => t.type === 'membership_payment' || t.type === 'sponsorship_payment');
const debits = completed.filter((t) => t.type === 'withdrawal' || t.type === 'refund');
const creditNet = sum(credits.map((t) => t.netAmount.amount));
const debitNet = sum(debits.map((t) => t.netAmount.amount));
const baseline = wallet.balance.amount - creditNet + debitNet;
expect(baseline).toBeGreaterThanOrEqual(0);
const parseSeq = (id: string): number | undefined => {
const match = id.match(/tx-league-\d+-(\d+)$/);
if (!match) return undefined;
const seq = Number(match[1]);
return Number.isFinite(seq) ? seq : undefined;
};
const inFactoryOrder = [...completed].sort((a, b) => {
const aSeq = parseSeq(a.id.toString());
const bSeq = parseSeq(b.id.toString());
if (aSeq !== undefined && bSeq !== undefined) return aSeq - bSeq;
if (aSeq !== undefined) return -1;
if (bSeq !== undefined) return 1;
return a.createdAt.getTime() - b.createdAt.getTime();
});
let running = baseline;
for (const tx of inFactoryOrder) {
const net = tx.netAmount.amount;
if (tx.type === 'withdrawal' || tx.type === 'refund') {
running -= net;
expect(running).toBeGreaterThanOrEqual(0);
} else {
running += net;
}
}
expect(running).toBeCloseTo(wallet.balance.amount, 6);
}
// Membership edge cases:
// - league-2 is intentionally empty
expect(seed.leagueMemberships.some((m) => m.leagueId.toString() === 'league-2')).toBe(false);
// - league-5 is "busy" (full/overbooked mix of statuses)
const league5Memberships = seed.leagueMemberships.filter((m) => m.leagueId.toString() === 'league-5');
expect(league5Memberships.length).toBeGreaterThan(0);
expect(league5Memberships.some((m) => m.status.toString() === 'active')).toBe(true);
expect(league5Memberships.some((m) => m.status.toString() === 'pending')).toBe(true);
// Stewarding referential integrity
for (const protest of seed.protests) {
expect(raceById.has(protest.raceId)).toBe(true);
expect(driverIds.has(protest.protestingDriverId)).toBe(true);
expect(driverIds.has(protest.accusedDriverId)).toBe(true);
const race = raceById.get(protest.raceId);
if (race) {
const leagueId = race.leagueId.toString();
const hasMembership = (driverId: string) =>
seed.leagueMemberships.some(
(m) => m.leagueId.toString() === leagueId && m.driverId.toString() === driverId,
);
expect(hasMembership(protest.protestingDriverId)).toBe(true);
expect(hasMembership(protest.accusedDriverId)).toBe(true);
}
}
for (const penalty of seed.penalties) {
expect(raceById.has(penalty.raceId)).toBe(true);
expect(driverIds.has(penalty.driverId)).toBe(true);
const race = raceById.get(penalty.raceId);
if (race) {
expect(penalty.leagueId).toBe(race.leagueId.toString());
}
if (penalty.protestId !== undefined) {
expect(protestById.has(penalty.protestId)).toBe(true);
const protest = protestById.get(penalty.protestId);
if (protest) {
expect(penalty.raceId).toBe(protest.raceId);
}
}
}
// Stewarding edge cases: demo league has multiple protest statuses
const protestStatusesLeague5 = new Set(
seed.protests
.filter((p) =>
seed.races.some((r) => r.id.toString() === p.raceId && r.leagueId.toString() === 'league-5'),
)
.map((p) => p.status.toString()),
);
expect(protestStatusesLeague5.has('pending')).toBe(true);
expect(protestStatusesLeague5.has('under_review') || protestStatusesLeague5.has('upheld')).toBe(true);
// Penalties should include at least one linked to a protest
expect(seed.penalties.some((p) => (p as unknown as { protestId?: string }).protestId)).toBe(true);
// Basic ID sanity (no duplicates per collection)
const assertNoDuplicateIds = (ids: string[]) => {
const unique = new Set(ids);
expect(unique.size).toBe(ids.length);
};
assertNoDuplicateIds(seed.drivers.map((d) => d.id.toString()));
assertNoDuplicateIds(seed.leagues.map((l) => l.id.toString()));
assertNoDuplicateIds(seed.teams.map((t) => t.id.toString()));
assertNoDuplicateIds(seed.sponsors.map((s) => s.id.toString()));
assertNoDuplicateIds(seed.seasons.map((s) => s.id));
assertNoDuplicateIds(seed.seasonSponsorships.map((s) => s.id));
assertNoDuplicateIds(seed.sponsorshipRequests.map((r) => r.id));
assertNoDuplicateIds(seed.protests.map((p) => p.id));
assertNoDuplicateIds(seed.penalties.map((p) => p.id));
assertNoDuplicateIds(seed.leagueWallets.map((w) => w.id.toString()));
assertNoDuplicateIds(seed.leagueWalletTransactions.map((t) => t.id.toString()));
});
});

View File

@@ -1,11 +1,12 @@
import { describe, expect, it, vi } from 'vitest';
import { Result } from '@core/shared/application/Result';
import { DriverService } from './DriverService';
describe('DriverService', () => {
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
it('getDriversLeaderboard executes use case and returns presenter model', async () => {
const getDriversLeaderboardUseCase = { execute: vi.fn(async () => {}) };
const getDriversLeaderboardUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driversLeaderboardPresenter = { getResponseModel: vi.fn(() => ({ items: [] })) };
const service = new DriverService(
@@ -31,7 +32,7 @@ describe('DriverService', () => {
});
it('getTotalDrivers executes use case and returns presenter model', async () => {
const getTotalDriversUseCase = { execute: vi.fn(async () => {}) };
const getTotalDriversUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverStatsPresenter = { getResponseModel: vi.fn(() => ({ totalDrivers: 123 })) };
const service = new DriverService(
@@ -57,7 +58,7 @@ describe('DriverService', () => {
});
it('completeOnboarding passes optional bio only when provided', async () => {
const completeDriverOnboardingUseCase = { execute: vi.fn(async () => {}) };
const completeDriverOnboardingUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -112,7 +113,7 @@ describe('DriverService', () => {
});
it('getDriverRegistrationStatus passes raceId and driverId and returns presenter model', async () => {
const isDriverRegisteredForRaceUseCase = { execute: vi.fn(async () => {}) };
const isDriverRegisteredForRaceUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverRegistrationStatusPresenter = { getResponseModel: vi.fn(() => ({ isRegistered: true })) };
const service = new DriverService(
@@ -142,7 +143,7 @@ describe('DriverService', () => {
it('getCurrentDriver calls repository and returns presenter model', async () => {
const driverRepository = { findById: vi.fn(async () => null) };
const driverPresenter = { getResponseModel: vi.fn(() => null) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => null) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -168,7 +169,7 @@ describe('DriverService', () => {
it('updateDriverProfile builds optional input and returns presenter model', async () => {
const updateDriverProfileUseCase = { execute: vi.fn(async () => {}) };
const driverPresenter = { getResponseModel: vi.fn(() => ({ driver: { id: 'd1' } })) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ driver: { id: 'd1' } })) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -210,7 +211,7 @@ describe('DriverService', () => {
it('getDriver calls repository and returns presenter model', async () => {
const driverRepository = { findById: vi.fn(async () => null) };
const driverPresenter = { getResponseModel: vi.fn(() => ({ driver: null })) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ driver: null })) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -235,7 +236,7 @@ describe('DriverService', () => {
});
it('getDriverProfile executes use case and returns presenter model', async () => {
const getProfileOverviewUseCase = { execute: vi.fn(async () => {}) };
const getProfileOverviewUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverProfilePresenter = { getResponseModel: vi.fn(() => ({ profile: { id: 'd1' } })) };
const service = new DriverService(