more seeds
This commit is contained in:
@@ -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',
|
||||
],
|
||||
|
||||
224
apps/api/src/domain/bootstrap/RacingSeed.test.ts
Normal file
224
apps/api/src/domain/bootstrap/RacingSeed.test.ts
Normal 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()));
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user