website refactor
This commit is contained in:
253
apps/api/src/domain/league/LeagueConfig.integration.test.ts
Normal file
253
apps/api/src/domain/league/LeagueConfig.integration.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
|
||||
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
|
||||
import type { AuthorizationService } from '../auth/AuthorizationService';
|
||||
import { LeagueModule } from './LeagueModule';
|
||||
|
||||
import { requestContextMiddleware } from '@adapters/http/RequestContext';
|
||||
|
||||
import {
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
} from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
|
||||
describe('League Config API - Integration', () => {
|
||||
let app: import("@nestjs/common").INestApplication;
|
||||
|
||||
const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
|
||||
getCurrentSession: vi.fn(async () => null),
|
||||
};
|
||||
|
||||
const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
|
||||
getRolesForUser: vi.fn(() => []),
|
||||
};
|
||||
|
||||
async function seedLeagueWithConfig(params: {
|
||||
leagueId: string;
|
||||
ownerId: string;
|
||||
seasonId: string;
|
||||
scoringConfigId: string;
|
||||
gameId: string;
|
||||
}): Promise<void> {
|
||||
const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN);
|
||||
const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN);
|
||||
const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN);
|
||||
const seasonRepo = app.get(SEASON_REPOSITORY_TOKEN);
|
||||
const scoringRepo = app.get(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN);
|
||||
|
||||
// Game is seeded by default, no need to create
|
||||
|
||||
// Create league
|
||||
await leagueRepo.create(
|
||||
League.create({
|
||||
id: params.leagueId,
|
||||
name: 'Test League',
|
||||
description: 'Test league',
|
||||
ownerId: params.ownerId,
|
||||
settings: { visibility: 'ranked' },
|
||||
}),
|
||||
);
|
||||
|
||||
// Create owner driver
|
||||
await driverRepo.create(
|
||||
Driver.create({
|
||||
id: params.ownerId,
|
||||
iracingId: '2001',
|
||||
name: 'Owner Driver',
|
||||
country: 'DE',
|
||||
}),
|
||||
);
|
||||
|
||||
// Create membership
|
||||
await membershipRepo.saveMembership(
|
||||
LeagueMembership.create({
|
||||
leagueId: params.leagueId,
|
||||
driverId: params.ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
}),
|
||||
);
|
||||
|
||||
// Create season
|
||||
await seasonRepo.create(
|
||||
Season.create({
|
||||
id: params.seasonId,
|
||||
leagueId: params.leagueId,
|
||||
gameId: params.gameId,
|
||||
name: 'Test Season',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
status: 'active',
|
||||
}),
|
||||
);
|
||||
|
||||
// Create scoring config
|
||||
await scoringRepo.save(
|
||||
LeagueScoringConfig.create({
|
||||
id: params.scoringConfigId,
|
||||
seasonId: params.seasonId,
|
||||
scoringPresetId: 'f1-2024',
|
||||
championships: [
|
||||
{
|
||||
id: 'championship-1',
|
||||
name: 'Main Championship',
|
||||
type: 'driver',
|
||||
sessionTypes: ['main'],
|
||||
pointsTableBySessionType: {
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
sprint: new PointsTable({}),
|
||||
main: new PointsTable({ 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }),
|
||||
timeTrial: new PointsTable({}),
|
||||
},
|
||||
dropScorePolicy: { strategy: 'none' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [LeagueModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
|
||||
// Required for getActorFromRequestContext() used by requireLeagueAdminOrOwner().
|
||||
app.use(requestContextMiddleware as never);
|
||||
|
||||
// Test-only auth injection: emulate an authenticated session by setting request.user.
|
||||
app.use((req: unknown, _res: unknown, next: unknown) => {
|
||||
const r = req as { headers: Record<string, string>, user?: { userId: string } };
|
||||
const userId = r.headers['x-test-user-id'];
|
||||
if (typeof userId === 'string' && userId.length > 0) {
|
||||
r.user = { userId };
|
||||
}
|
||||
(next as () => void)();
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const reflector = new Reflector();
|
||||
app.useGlobalGuards(
|
||||
new AuthenticationGuard(sessionPort as never),
|
||||
new AuthorizationGuard(reflector, authorizationService as never),
|
||||
);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app?.close();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return league config with all dependencies injected', async () => {
|
||||
const leagueId = 'league-1';
|
||||
const ownerId = 'owner-1';
|
||||
const seasonId = 'season-1';
|
||||
const scoringConfigId = 'scoring-1';
|
||||
const gameId = 'game-1';
|
||||
|
||||
await seedLeagueWithConfig({ leagueId, ownerId, seasonId, scoringConfigId, gameId });
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/leagues/${leagueId}/config`)
|
||||
.expect(200);
|
||||
|
||||
// Verify the response contains expected data
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.leagueId).toBeDefined();
|
||||
expect(response.body.leagueId.value).toBe(leagueId);
|
||||
expect(response.body.basics).toBeDefined();
|
||||
expect(response.body.basics.name.value).toBe('Test League');
|
||||
expect(response.body.basics.visibility).toBe('ranked');
|
||||
expect(response.body.scoring).toBeDefined();
|
||||
expect(response.body.scoring.type).toBe('f1-2024');
|
||||
});
|
||||
|
||||
it('should return null for non-existent league', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/leagues/non-existent-league/config')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle league with no season or scoring config', async () => {
|
||||
const leagueId = 'league-2';
|
||||
const ownerId = 'owner-2';
|
||||
|
||||
const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN);
|
||||
const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN);
|
||||
const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN);
|
||||
|
||||
// Create league without season or scoring config
|
||||
await leagueRepo.create(
|
||||
League.create({
|
||||
id: leagueId,
|
||||
name: 'League Without Config',
|
||||
description: 'Test league',
|
||||
ownerId: ownerId,
|
||||
settings: { visibility: 'unranked' },
|
||||
}),
|
||||
);
|
||||
|
||||
await driverRepo.create(
|
||||
Driver.create({
|
||||
id: ownerId,
|
||||
iracingId: '2002',
|
||||
name: 'Owner Driver 2',
|
||||
country: 'US',
|
||||
}),
|
||||
);
|
||||
|
||||
await membershipRepo.saveMembership(
|
||||
LeagueMembership.create({
|
||||
leagueId: leagueId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/leagues/${leagueId}/config`)
|
||||
.expect(200);
|
||||
|
||||
// Should still return basic league info
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.leagueId).toBeDefined();
|
||||
expect(response.body.leagueId.value).toBe(leagueId);
|
||||
expect(response.body.basics).toBeDefined();
|
||||
expect(response.body.basics.name.value).toBe('League Without Config');
|
||||
expect(response.body.basics.visibility).toBe('unranked');
|
||||
});
|
||||
});
|
||||
@@ -192,6 +192,7 @@ export class LeagueController {
|
||||
return this.leagueService.getLeagueOwnerSummary(query);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':leagueId/config')
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO })
|
||||
|
||||
70
apps/api/src/domain/league/LeagueProviders.test.ts
Normal file
70
apps/api/src/domain/league/LeagueProviders.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LeagueModule } from './LeagueModule';
|
||||
import {
|
||||
GET_LEAGUE_FULL_CONFIG_USE_CASE,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
} from './LeagueProviders';
|
||||
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository';
|
||||
import type { SeasonRepository } from '@core/racing/domain/repositories/SeasonRepository';
|
||||
import type { LeagueScoringConfigRepository } from '@core/racing/domain/repositories/LeagueScoringConfigRepository';
|
||||
import type { GameRepository } from '@core/racing/domain/repositories/GameRepository';
|
||||
|
||||
describe('LeagueProviders - Dependency Injection', () => {
|
||||
let module: import('@nestjs/testing').TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [LeagueModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
it('should inject GetLeagueFullConfigUseCase with all required dependencies', () => {
|
||||
// Get the use case instance
|
||||
const useCase = module.get<GetLeagueFullConfigUseCase>(GET_LEAGUE_FULL_CONFIG_USE_CASE);
|
||||
|
||||
// Verify the use case is defined
|
||||
expect(useCase).toBeDefined();
|
||||
expect(useCase).toBeInstanceOf(GetLeagueFullConfigUseCase);
|
||||
|
||||
// Get the repositories to verify they exist
|
||||
const leagueRepo = module.get<LeagueRepository>(LEAGUE_REPOSITORY_TOKEN);
|
||||
const seasonRepo = module.get<SeasonRepository>(SEASON_REPOSITORY_TOKEN);
|
||||
const scoringRepo = module.get<LeagueScoringConfigRepository>(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN);
|
||||
const gameRepo = module.get<GameRepository>(GAME_REPOSITORY_TOKEN);
|
||||
|
||||
// Verify all repositories are defined
|
||||
expect(leagueRepo).toBeDefined();
|
||||
expect(seasonRepo).toBeDefined();
|
||||
expect(scoringRepo).toBeDefined();
|
||||
expect(gameRepo).toBeDefined();
|
||||
|
||||
// Verify the use case has the repositories injected
|
||||
// We can't directly access private properties, but we can verify the use case is functional
|
||||
// by checking that it has the execute method
|
||||
expect(typeof useCase.execute).toBe('function');
|
||||
});
|
||||
|
||||
it('should have the correct token for GetLeagueFullConfigUseCase', () => {
|
||||
expect(GET_LEAGUE_FULL_CONFIG_USE_CASE).toBe('GetLeagueFullConfigUseCase');
|
||||
});
|
||||
|
||||
it('should have all required repository tokens defined', () => {
|
||||
expect(LEAGUE_REPOSITORY_TOKEN).toBe('ILeagueRepository');
|
||||
expect(SEASON_REPOSITORY_TOKEN).toBe('ISeasonRepository');
|
||||
expect(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN).toBe('ILeagueScoringConfigRepository');
|
||||
expect(GAME_REPOSITORY_TOKEN).toBe('IGameRepository');
|
||||
});
|
||||
});
|
||||
@@ -251,7 +251,19 @@ export const LeagueProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: GET_LEAGUE_FULL_CONFIG_USE_CASE,
|
||||
useClass: GetLeagueFullConfigUseCase,
|
||||
useFactory: (
|
||||
leagueRepo: LeagueRepository,
|
||||
seasonRepo: SeasonRepository,
|
||||
scoringRepo: import('@core/racing/domain/repositories/LeagueScoringConfigRepository').LeagueScoringConfigRepository,
|
||||
gameRepo: import('@core/racing/domain/repositories/GameRepository').GameRepository,
|
||||
) =>
|
||||
new GetLeagueFullConfigUseCase(leagueRepo, seasonRepo, scoringRepo, gameRepo),
|
||||
inject: [
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE,
|
||||
|
||||
@@ -11,7 +11,7 @@ export class LeagueConfigPresenter implements UseCaseOutputPort<GetLeagueFullCon
|
||||
|
||||
present(result: GetLeagueFullConfigResult): void {
|
||||
const dto = result.config as unknown as {
|
||||
league: { id: string; name: string; description: string; settings: { pointsSystem: string } };
|
||||
league: { id: string; name: string; description: string; settings: { pointsSystem: string; visibility: string } };
|
||||
activeSeason?: {
|
||||
stewardingConfig?: {
|
||||
decisionMode: string;
|
||||
@@ -39,7 +39,7 @@ export class LeagueConfigPresenter implements UseCaseOutputPort<GetLeagueFullCon
|
||||
const schedule = dto.activeSeason?.schedule;
|
||||
const scoringConfig = dto.scoringConfig;
|
||||
|
||||
const visibility: 'public' | 'private' = 'public';
|
||||
const visibility: 'public' | 'private' = settings.visibility === 'ranked' ? 'public' : 'private';
|
||||
|
||||
const championships = scoringConfig?.championships ?? [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user