seed data

This commit is contained in:
2025-12-26 22:22:39 +01:00
parent 1e8d84b31b
commit b4f86abf90
10 changed files with 926 additions and 218 deletions

View File

@@ -40,13 +40,13 @@ export const AuthProviders: Provider[] = [
useFactory: (passwordHashingService: IPasswordHashingService, logger: Logger) => {
// Seed initial users for InMemoryUserRepository
const initialUsers: StoredUser[] = [
// Example user (replace with actual test users as needed)
{
id: 'user-1',
email: 'test@example.com',
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
displayName: 'Test User',
salt: '', // Handled by hashing service
// Match seeded racing driver id so dashboard works in inmemory mode.
id: 'driver-1',
email: 'admin@gridpilot.local',
passwordHash: 'demo_salt_321nimda', // InMemoryPasswordHashingService: "admin123" reversed.
displayName: 'Admin',
salt: '',
createdAt: new Date(),
},
];

View File

@@ -1,5 +1,5 @@
import { Provider } from '@nestjs/common';
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { EnsureInitialData, type InMemorySeedDependencies } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { SignupWithEmailUseCase, type SignupWithEmailResult } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
import {
CreateAchievementUseCase,
@@ -84,12 +84,52 @@ export const BootstrapProviders: Provider[] = [
{
provide: EnsureInitialData,
useFactory: (
signupUseCase: SignupWithEmailUseCase,
createAchievementUseCase: CreateAchievementUseCase,
logger: Logger
signupUseCase: SignupWithEmailUseCase,
createAchievementUseCase: CreateAchievementUseCase,
logger: Logger,
driverRepository: InMemorySeedDependencies['driverRepository'],
leagueRepository: InMemorySeedDependencies['leagueRepository'],
raceRepository: InMemorySeedDependencies['raceRepository'],
resultRepository: InMemorySeedDependencies['resultRepository'],
standingRepository: InMemorySeedDependencies['standingRepository'],
leagueMembershipRepository: InMemorySeedDependencies['leagueMembershipRepository'],
raceRegistrationRepository: InMemorySeedDependencies['raceRegistrationRepository'],
teamRepository: InMemorySeedDependencies['teamRepository'],
teamMembershipRepository: InMemorySeedDependencies['teamMembershipRepository'],
feedRepository: InMemorySeedDependencies['feedRepository'],
socialGraphRepository: InMemorySeedDependencies['socialGraphRepository'],
) => {
return new EnsureInitialData(signupUseCase, createAchievementUseCase, logger);
const deps: InMemorySeedDependencies = {
driverRepository,
leagueRepository,
raceRepository,
resultRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
teamRepository,
teamMembershipRepository,
feedRepository,
socialGraphRepository,
};
return new EnsureInitialData(signupUseCase, createAchievementUseCase, logger, deps);
},
inject: [SIGNUP_USE_CASE_TOKEN, CREATE_ACHIEVEMENT_USE_CASE_TOKEN, 'Logger'],
inject: [
SIGNUP_USE_CASE_TOKEN,
CREATE_ACHIEVEMENT_USE_CASE_TOKEN,
'Logger',
'IDriverRepository',
'ILeagueRepository',
'IRaceRepository',
'IResultRepository',
'IStandingRepository',
'ILeagueMembershipRepository',
'IRaceRegistrationRepository',
'ITeamRepository',
'ITeamMembershipRepository',
'IFeedRepository',
'ISocialGraphRepository',
],
},
];

View File

@@ -0,0 +1,129 @@
import { Global, Module } from '@nestjs/common';
import type { Logger } from '@core/shared/application/Logger';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
import {
InMemoryFeedRepository,
InMemorySocialGraphRepository,
} from '@adapters/social/persistence/inmemory/InMemorySocialAndFeed';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
@Global()
@Module({
providers: [
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IDriverRepository => new InMemoryDriverRepository(logger),
inject: ['Logger'],
},
{
provide: LEAGUE_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ILeagueRepository => new InMemoryLeagueRepository(logger),
inject: ['Logger'],
},
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IRaceRepository => new InMemoryRaceRepository(logger),
inject: ['Logger'],
},
{
provide: RESULT_REPOSITORY_TOKEN,
useFactory: (logger: Logger, raceRepo: IRaceRepository): IResultRepository =>
new InMemoryResultRepository(logger, raceRepo),
inject: ['Logger', RACE_REPOSITORY_TOKEN],
},
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: (
logger: Logger,
resultRepo: IResultRepository,
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
): IStandingRepository => new InMemoryStandingRepository(logger, getPointsSystems(), resultRepo, raceRepo, leagueRepo),
inject: ['Logger', RESULT_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
},
{
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ILeagueMembershipRepository => new InMemoryLeagueMembershipRepository(logger),
inject: ['Logger'],
},
{
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IRaceRegistrationRepository => new InMemoryRaceRegistrationRepository(logger),
inject: ['Logger'],
},
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ITeamRepository => new InMemoryTeamRepository(logger),
inject: ['Logger'],
},
{
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ITeamMembershipRepository => new InMemoryTeamMembershipRepository(logger),
inject: ['Logger'],
},
{
provide: FEED_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IFeedRepository =>
new InMemoryFeedRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
inject: ['Logger'],
},
{
provide: SOCIAL_GRAPH_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ISocialGraphRepository =>
new InMemorySocialGraphRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
inject: ['Logger'],
},
],
exports: [
DRIVER_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
TEAM_REPOSITORY_TOKEN,
TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
],
})
export class InMemoryPersistenceModule {}

View File

@@ -1,59 +0,0 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
vi.mock('@/lib/services/ServiceProvider', () => ({
useServices: () => ({
mediaService: {
getLeagueLogo: () => '/logo.png',
},
}),
}));
vi.mock('@/components/leagues/MembershipStatus', () => ({
__esModule: true,
default: () => <div data-testid="membership-status" />,
}));
vi.mock('next/image', () => ({
__esModule: true,
default: (props: any) => <img {...props} />,
}));
import LeagueHeader from './LeagueHeader';
describe('LeagueHeader', () => {
it('renders league name, description and sponsor', () => {
render(
<LeagueHeader
leagueId="league-1"
leagueName="Test League"
description="A fun test league"
ownerId="owner-1"
ownerName="Owner Name"
mainSponsor={{
name: 'Test Sponsor',
websiteUrl: 'https://example.com',
}}
/>
);
expect(screen.getByText('Test League')).toBeInTheDocument();
expect(screen.getByText('A fun test league')).toBeInTheDocument();
expect(screen.getByText('by')).toBeInTheDocument();
expect(screen.getByText('Test Sponsor')).toBeInTheDocument();
});
it('renders without description or sponsor', () => {
render(
<LeagueHeader
leagueId="league-2"
leagueName="League Without Details"
ownerId="owner-2"
ownerName="Owner 2"
/>
);
expect(screen.getByText('League Without Details')).toBeInTheDocument();
});
});

View File

@@ -1,131 +0,0 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import LeagueMembers from './LeagueMembers';
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
// Stub global driver stats helper used by LeagueMembers sorting/rendering
(globalThis as any).getDriverStats = (driverId: string) => ({
driverId,
rating: driverId === 'driver-1' ? 2500 : 2000,
overallRank: driverId === 'driver-1' ? 1 : 2,
wins: driverId === 'driver-1' ? 10 : 5,
});
// Mock effective driver id so we can assert the "(You)" label
vi.mock('@/hooks/useEffectiveDriverId', () => {
return {
useEffectiveDriverId: () => 'driver-1',
};
});
// Mock services hook to inject stub leagueMembershipService and driverService
const mockFetchLeagueMemberships = vi.fn<(leagueId: string) => Promise<void>>();
const mockGetLeagueMembers = vi.fn<(leagueId: string) => any[]>();
const mockFindByIds = vi.fn<(ids: string[]) => Promise<DriverDTO[]>>();
const mockServices = {
leagueMembershipService: {
fetchLeagueMemberships: mockFetchLeagueMemberships,
getLeagueMembers: mockGetLeagueMembers,
},
driverService: {
findByIds: mockFindByIds,
},
};
vi.mock('@/lib/services/ServiceProvider', () => ({
useServices: () => mockServices,
}));
describe('LeagueMembers', () => {
beforeEach(() => {
mockFetchLeagueMemberships.mockReset();
mockGetLeagueMembers.mockReset();
mockFindByIds.mockReset();
});
it('loads memberships via services and renders driver rows', async () => {
const leagueId = 'league-1';
const memberships = [
{
id: 'm1',
leagueId,
driverId: 'driver-1',
role: 'owner',
status: 'active',
joinedAt: '2024-01-01T00:00:00.000Z',
},
{
id: 'm2',
leagueId,
driverId: 'driver-2',
role: 'member',
status: 'active',
joinedAt: '2024-01-02T00:00:00.000Z',
},
];
const drivers: DriverDTO[] = [
{
id: 'driver-1',
iracingId: 'ir-1',
name: 'Driver One',
country: 'DE',
joinedAt: '2024-01-01T00:00:00.000Z',
},
{
id: 'driver-2',
iracingId: 'ir-2',
name: 'Driver Two',
country: 'US',
joinedAt: '2024-01-01T00:00:00.000Z',
},
];
mockFetchLeagueMemberships.mockResolvedValue(undefined);
mockGetLeagueMembers.mockReturnValue(memberships);
mockFindByIds.mockResolvedValue(drivers);
render(<LeagueMembers leagueId={leagueId} showActions={false} />);
// Loading state first
expect(screen.getByText('Loading members...')).toBeInTheDocument();
// Wait for data to be rendered
await waitFor(() => {
expect(screen.queryByText('Loading members...')).not.toBeInTheDocument();
});
// Services should have been called with expected arguments
expect(mockFetchLeagueMemberships).toHaveBeenCalledWith(leagueId);
expect(mockGetLeagueMembers).toHaveBeenCalledWith(leagueId);
expect(mockFindByIds).toHaveBeenCalledTimes(1);
expect(mockFindByIds).toHaveBeenCalledWith(['driver-1', 'driver-2']);
// Driver rows should be rendered using DTO names
expect(screen.getByText('Driver One')).toBeInTheDocument();
expect(screen.getByText('Driver Two')).toBeInTheDocument();
// Current user marker should appear for effective driver id
expect(screen.getByText('(You)')).toBeInTheDocument();
});
it('handles empty membership list gracefully', async () => {
const leagueId = 'league-empty';
mockFetchLeagueMemberships.mockResolvedValue(undefined);
mockGetLeagueMembers.mockReturnValue([]);
mockFindByIds.mockResolvedValue([]);
render(<LeagueMembers leagueId={leagueId} showActions={false} />);
await waitFor(() => {
expect(screen.queryByText('Loading members...')).not.toBeInTheDocument();
});
expect(screen.getByText('No members found')).toBeInTheDocument();
});
});