integration tests cleanup
This commit is contained in:
@@ -1,408 +0,0 @@
|
||||
/**
|
||||
* Contract Validation Tests for API
|
||||
*
|
||||
* These tests validate that the API DTOs and OpenAPI spec are consistent
|
||||
* and that the generated types will be compatible with the website.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
interface OpenAPISchema {
|
||||
type?: string;
|
||||
format?: string;
|
||||
$ref?: string;
|
||||
items?: OpenAPISchema;
|
||||
properties?: Record<string, OpenAPISchema>;
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
nullable?: boolean;
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
}
|
||||
|
||||
interface OpenAPISpec {
|
||||
openapi: string;
|
||||
info: {
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
};
|
||||
paths: Record<string, any>;
|
||||
components: {
|
||||
schemas: Record<string, OpenAPISchema>;
|
||||
};
|
||||
}
|
||||
|
||||
describe('API Contract Validation', () => {
|
||||
const apiRoot = path.join(__dirname, '../..'); // /Users/marcmintel/Projects/gridpilot
|
||||
const openapiPath = path.join(apiRoot, 'apps/api/openapi.json');
|
||||
const generatedTypesDir = path.join(apiRoot, 'apps/website/lib/types/generated');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
describe('OpenAPI Spec Integrity', () => {
|
||||
it('should have a valid OpenAPI spec file', async () => {
|
||||
const specExists = await fs.access(openapiPath).then(() => true).catch(() => false);
|
||||
expect(specExists).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a valid JSON structure', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should have required OpenAPI fields', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/);
|
||||
expect(spec.info).toBeDefined();
|
||||
expect(spec.info.title).toBeDefined();
|
||||
expect(spec.info.version).toBeDefined();
|
||||
expect(spec.components).toBeDefined();
|
||||
expect(spec.components.schemas).toBeDefined();
|
||||
});
|
||||
|
||||
it('committed openapi.json should match generator output', async () => {
|
||||
const repoRoot = apiRoot; // Already at the repo root
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gridpilot-openapi-'));
|
||||
const generatedOpenapiPath = path.join(tmpDir, 'openapi.json');
|
||||
|
||||
await execFileAsync(
|
||||
'npx',
|
||||
['--no-install', 'tsx', 'scripts/generate-openapi-spec.ts', '--output', generatedOpenapiPath],
|
||||
{ cwd: repoRoot, maxBuffer: 20 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
const committed: OpenAPISpec = JSON.parse(await fs.readFile(openapiPath, 'utf-8'));
|
||||
const generated: OpenAPISpec = JSON.parse(await fs.readFile(generatedOpenapiPath, 'utf-8'));
|
||||
|
||||
expect(generated).toEqual(committed);
|
||||
});
|
||||
|
||||
it('should include real HTTP paths for known routes', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
const pathKeys = Object.keys(spec.paths ?? {});
|
||||
expect(pathKeys.length).toBeGreaterThan(0);
|
||||
|
||||
// A couple of stable routes to detect "empty/stale" specs.
|
||||
expect(spec.paths['/drivers/leaderboard']).toBeDefined();
|
||||
expect(spec.paths['/dashboard/overview']).toBeDefined();
|
||||
|
||||
// Sanity-check the operation objects exist (method keys are lowercase in OpenAPI).
|
||||
expect(spec.paths['/drivers/leaderboard'].get).toBeDefined();
|
||||
expect(spec.paths['/dashboard/overview'].get).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include league schedule publish/unpublish endpoints and published state', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/publish']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/publish'].post).toBeDefined();
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/unpublish']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/unpublish'].post).toBeDefined();
|
||||
|
||||
const scheduleSchema = spec.components.schemas['LeagueScheduleDTO'];
|
||||
if (!scheduleSchema) {
|
||||
throw new Error('Expected LeagueScheduleDTO schema to be present in OpenAPI spec');
|
||||
}
|
||||
|
||||
expect(scheduleSchema.properties?.published).toBeDefined();
|
||||
expect(scheduleSchema.properties?.published?.type).toBe('boolean');
|
||||
expect(scheduleSchema.required ?? []).toContain('published');
|
||||
});
|
||||
|
||||
it('should include league roster admin read endpoints and schemas', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/members']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/members'].get).toBeDefined();
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests'].get).toBeDefined();
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/approve']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/approve'].post).toBeDefined();
|
||||
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/reject']).toBeDefined();
|
||||
expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/reject'].post).toBeDefined();
|
||||
|
||||
const memberSchema = spec.components.schemas['LeagueRosterMemberDTO'];
|
||||
if (!memberSchema) {
|
||||
throw new Error('Expected LeagueRosterMemberDTO schema to be present in OpenAPI spec');
|
||||
}
|
||||
|
||||
expect(memberSchema.properties?.driverId).toBeDefined();
|
||||
expect(memberSchema.properties?.role).toBeDefined();
|
||||
expect(memberSchema.properties?.joinedAt).toBeDefined();
|
||||
expect(memberSchema.required ?? []).toContain('driverId');
|
||||
expect(memberSchema.required ?? []).toContain('role');
|
||||
expect(memberSchema.required ?? []).toContain('joinedAt');
|
||||
expect(memberSchema.required ?? []).toContain('driver');
|
||||
|
||||
const joinRequestSchema = spec.components.schemas['LeagueRosterJoinRequestDTO'];
|
||||
if (!joinRequestSchema) {
|
||||
throw new Error('Expected LeagueRosterJoinRequestDTO schema to be present in OpenAPI spec');
|
||||
}
|
||||
|
||||
expect(joinRequestSchema.properties?.id).toBeDefined();
|
||||
expect(joinRequestSchema.properties?.leagueId).toBeDefined();
|
||||
expect(joinRequestSchema.properties?.driverId).toBeDefined();
|
||||
expect(joinRequestSchema.properties?.requestedAt).toBeDefined();
|
||||
expect(joinRequestSchema.required ?? []).toContain('id');
|
||||
expect(joinRequestSchema.required ?? []).toContain('leagueId');
|
||||
expect(joinRequestSchema.required ?? []).toContain('driverId');
|
||||
expect(joinRequestSchema.required ?? []).toContain('requestedAt');
|
||||
expect(joinRequestSchema.required ?? []).toContain('driver');
|
||||
});
|
||||
|
||||
it('should have no circular references in schemas', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
const schemas = spec.components.schemas;
|
||||
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
function detectCircular(schemaName: string): boolean {
|
||||
if (visiting.has(schemaName)) return true;
|
||||
if (visited.has(schemaName)) return false;
|
||||
|
||||
visiting.add(schemaName);
|
||||
const schema = schemas[schemaName];
|
||||
|
||||
if (!schema) {
|
||||
visiting.delete(schemaName);
|
||||
visited.add(schemaName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check properties for references
|
||||
if (schema.properties) {
|
||||
for (const prop of Object.values(schema.properties)) {
|
||||
if (prop.$ref) {
|
||||
const refName = prop.$ref.split('/').pop();
|
||||
if (refName && detectCircular(refName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (prop.items?.$ref) {
|
||||
const refName = prop.items.$ref.split('/').pop();
|
||||
if (refName && detectCircular(refName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(schemaName);
|
||||
visited.add(schemaName);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const schemaName of Object.keys(schemas)) {
|
||||
expect(detectCircular(schemaName)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DTO Consistency', () => {
|
||||
it('should have generated DTO files for critical schemas', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
const generatedFiles = await fs.readdir(generatedTypesDir);
|
||||
const generatedDTOs = generatedFiles
|
||||
.filter(f => f.endsWith('.ts'))
|
||||
.map(f => f.replace('.ts', ''));
|
||||
|
||||
// We intentionally do NOT require a 1:1 mapping for *all* schemas here.
|
||||
// OpenAPI generation and type generation can be run as separate steps,
|
||||
// and new schemas should not break API contract validation by themselves.
|
||||
const criticalDTOs = [
|
||||
'RequestAvatarGenerationInputDTO',
|
||||
'RequestAvatarGenerationOutputDTO',
|
||||
'UploadMediaInputDTO',
|
||||
'UploadMediaOutputDTO',
|
||||
'RaceDTO',
|
||||
'DriverDTO',
|
||||
];
|
||||
|
||||
for (const dtoName of criticalDTOs) {
|
||||
expect(spec.components.schemas[dtoName]).toBeDefined();
|
||||
expect(generatedDTOs).toContain(dtoName);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have consistent property types between DTOs and schemas', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
const schemas = spec.components.schemas;
|
||||
|
||||
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||
const dtoPath = path.join(generatedTypesDir, `${schemaName}.ts`);
|
||||
const dtoExists = await fs.access(dtoPath).then(() => true).catch(() => false);
|
||||
|
||||
if (!dtoExists) continue;
|
||||
|
||||
const dtoContent = await fs.readFile(dtoPath, 'utf-8');
|
||||
|
||||
// Check that all required properties are present
|
||||
if (schema.required) {
|
||||
for (const requiredProp of schema.required) {
|
||||
expect(dtoContent).toContain(requiredProp);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all properties are present
|
||||
if (schema.properties) {
|
||||
for (const propName of Object.keys(schema.properties)) {
|
||||
expect(dtoContent).toContain(propName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type Generation Integrity', () => {
|
||||
it('should have valid TypeScript syntax in generated files', async () => {
|
||||
const files = await fs.readdir(generatedTypesDir);
|
||||
const dtos = files.filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
||||
|
||||
for (const file of dtos) {
|
||||
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
|
||||
|
||||
// `index.ts` is a generated barrel file (no interfaces).
|
||||
if (file === 'index.ts') {
|
||||
expect(content).toContain('export type {');
|
||||
expect(content).toContain("from './");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic TypeScript syntax checks (DTO interfaces)
|
||||
expect(content).toContain('export interface');
|
||||
expect(content).toContain('{');
|
||||
expect(content).toContain('}');
|
||||
|
||||
// Should not have syntax errors (basic check)
|
||||
expect(content).not.toContain('undefined;');
|
||||
expect(content).not.toContain('any;');
|
||||
}
|
||||
});
|
||||
|
||||
it('should have proper imports for dependencies', async () => {
|
||||
const files = await fs.readdir(generatedTypesDir);
|
||||
const dtos = files.filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
||||
|
||||
for (const file of dtos) {
|
||||
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
|
||||
const importMatches = content.match(/import type \{ (\w+) \} from '\.\/(\w+)';/g) || [];
|
||||
|
||||
for (const importLine of importMatches) {
|
||||
const match = importLine.match(/import type \{ (\w+) \} from '\.\/(\w+)';/);
|
||||
if (match) {
|
||||
const [, importedType, fromFile] = match;
|
||||
expect(importedType).toBe(fromFile);
|
||||
|
||||
// Check that the imported file exists
|
||||
const importedPath = path.join(generatedTypesDir, `${fromFile}.ts`);
|
||||
const exists = await fs.access(importedPath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Contract Compatibility', () => {
|
||||
it('should maintain backward compatibility for existing DTOs', async () => {
|
||||
// This test ensures that when regenerating types, existing properties aren't removed
|
||||
// unless explicitly intended
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
|
||||
// Check critical DTOs that are likely used in production
|
||||
const criticalDTOs = [
|
||||
'RequestAvatarGenerationInputDTO',
|
||||
'RequestAvatarGenerationOutputDTO',
|
||||
'UploadMediaInputDTO',
|
||||
'UploadMediaOutputDTO',
|
||||
'RaceDTO',
|
||||
'DriverDTO'
|
||||
];
|
||||
|
||||
for (const dtoName of criticalDTOs) {
|
||||
if (spec.components.schemas[dtoName]) {
|
||||
const dtoPath = path.join(generatedTypesDir, `${dtoName}.ts`);
|
||||
const exists = await fs.access(dtoPath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle nullable fields correctly', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
const schemas = spec.components.schemas;
|
||||
|
||||
for (const [, schema] of Object.entries(schemas)) {
|
||||
const required = new Set(schema.required ?? []);
|
||||
if (!schema.properties) continue;
|
||||
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||
if (!propSchema.nullable) continue;
|
||||
|
||||
// In OpenAPI 3.0, a `nullable: true` property should not be listed as required,
|
||||
// otherwise downstream generators can't represent it safely.
|
||||
expect(required.has(propName)).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should have no empty string defaults for avatar/logo URLs', async () => {
|
||||
const content = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec: OpenAPISpec = JSON.parse(content);
|
||||
const schemas = spec.components.schemas;
|
||||
|
||||
// Check DTOs that should use URL|null pattern
|
||||
const mediaRelatedDTOs = [
|
||||
'GetAvatarOutputDTO',
|
||||
'UpdateAvatarInputDTO',
|
||||
'DashboardDriverSummaryDTO',
|
||||
'DriverProfileDriverSummaryDTO',
|
||||
'DriverLeaderboardItemDTO',
|
||||
'TeamListItemDTO',
|
||||
'LeagueSummaryDTO',
|
||||
'SponsorDTO',
|
||||
];
|
||||
|
||||
for (const dtoName of mediaRelatedDTOs) {
|
||||
const schema = schemas[dtoName];
|
||||
if (!schema || !schema.properties) continue;
|
||||
|
||||
// Check for avatarUrl, logoUrl properties
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||
if (propName === 'avatarUrl' || propName === 'logoUrl') {
|
||||
// Should be string type, nullable (no empty string defaults)
|
||||
expect(propSchema.type).toBe('string');
|
||||
expect(propSchema.nullable).toBe(true);
|
||||
// Should not have default value of empty string
|
||||
if (propSchema.default !== undefined) {
|
||||
expect(propSchema.default).not.toBe('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,339 +0,0 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||
import { createContainer, ContainerManager } from '../../apps/website/lib/di/container';
|
||||
import {
|
||||
SESSION_SERVICE_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_SERVICE_TOKEN,
|
||||
LEAGUE_SERVICE_TOKEN,
|
||||
AUTH_SERVICE_TOKEN,
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
TEAM_SERVICE_TOKEN,
|
||||
RACE_SERVICE_TOKEN,
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
CONFIG_TOKEN,
|
||||
LEAGUE_API_CLIENT_TOKEN,
|
||||
AUTH_API_CLIENT_TOKEN,
|
||||
DRIVER_API_CLIENT_TOKEN,
|
||||
TEAM_API_CLIENT_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
} from '../../apps/website/lib/di/tokens';
|
||||
|
||||
// Define minimal service interfaces for testing
|
||||
interface MockSessionService {
|
||||
getSession: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface MockAuthService {
|
||||
login: (email: string, password: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface MockLeagueService {
|
||||
getAllLeagues: () => Promise<unknown[]>;
|
||||
}
|
||||
|
||||
interface MockLeagueMembershipService {
|
||||
getLeagueMemberships: (userId: string) => Promise<unknown[]>;
|
||||
}
|
||||
|
||||
interface ConfigFunction {
|
||||
(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration test for website DI container
|
||||
*/
|
||||
describe('Website DI Container Integration', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeAll(() => {
|
||||
originalEnv = { ...process.env };
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Clean up all API URL env vars before each test
|
||||
delete process.env.API_BASE_URL;
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
delete process.env.API_BASE_URL;
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
ContainerManager.getInstance().dispose();
|
||||
});
|
||||
|
||||
describe('Basic Container Functionality', () => {
|
||||
it('creates container successfully', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
expect(container).toBeDefined();
|
||||
expect(container).not.toBeNull();
|
||||
});
|
||||
|
||||
it('resolves core services without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(LOGGER_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(CONFIG_TOKEN)).not.toThrow();
|
||||
|
||||
const logger = container.get(LOGGER_TOKEN);
|
||||
const config = container.get(CONFIG_TOKEN);
|
||||
|
||||
expect(logger).toBeDefined();
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves API clients without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const apiClients = [
|
||||
LEAGUE_API_CLIENT_TOKEN,
|
||||
AUTH_API_CLIENT_TOKEN,
|
||||
DRIVER_API_CLIENT_TOKEN,
|
||||
TEAM_API_CLIENT_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
];
|
||||
|
||||
for (const token of apiClients) {
|
||||
expect(() => container.get(token)).not.toThrow();
|
||||
const client = container.get(token);
|
||||
expect(client).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves auth services including SessionService', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(SESSION_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(AUTH_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const sessionService = container.get<MockSessionService>(SESSION_SERVICE_TOKEN);
|
||||
const authService = container.get<MockAuthService>(AUTH_SERVICE_TOKEN);
|
||||
|
||||
expect(sessionService).toBeDefined();
|
||||
expect(authService).toBeDefined();
|
||||
expect(typeof sessionService.getSession).toBe('function');
|
||||
expect(typeof authService.login).toBe('function');
|
||||
});
|
||||
|
||||
it('resolves league services including LeagueMembershipService', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(LEAGUE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const leagueService = container.get<MockLeagueService>(LEAGUE_SERVICE_TOKEN);
|
||||
const membershipService = container.get<MockLeagueMembershipService>(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
expect(leagueService).toBeDefined();
|
||||
expect(membershipService).toBeDefined();
|
||||
expect(typeof leagueService.getAllLeagues).toBe('function');
|
||||
expect(typeof membershipService.getLeagueMemberships).toBe('function');
|
||||
});
|
||||
|
||||
it('resolves domain services without errors', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
expect(() => container.get(DRIVER_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(TEAM_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(RACE_SERVICE_TOKEN)).not.toThrow();
|
||||
expect(() => container.get(DASHBOARD_SERVICE_TOKEN)).not.toThrow();
|
||||
|
||||
const driverService = container.get(DRIVER_SERVICE_TOKEN);
|
||||
const teamService = container.get(TEAM_SERVICE_TOKEN);
|
||||
const raceService = container.get(RACE_SERVICE_TOKEN);
|
||||
const dashboardService = container.get(DASHBOARD_SERVICE_TOKEN);
|
||||
|
||||
expect(driverService).toBeDefined();
|
||||
expect(teamService).toBeDefined();
|
||||
expect(raceService).toBeDefined();
|
||||
expect(dashboardService).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves all services in a single operation', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const tokens = [
|
||||
SESSION_SERVICE_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_SERVICE_TOKEN,
|
||||
LEAGUE_SERVICE_TOKEN,
|
||||
AUTH_SERVICE_TOKEN,
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
TEAM_SERVICE_TOKEN,
|
||||
RACE_SERVICE_TOKEN,
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
CONFIG_TOKEN,
|
||||
];
|
||||
|
||||
const services = tokens.map(token => {
|
||||
try {
|
||||
return container.get(token);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to resolve token ${token.toString()}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
expect(services.length).toBe(tokens.length);
|
||||
services.forEach((service) => {
|
||||
expect(service).toBeDefined();
|
||||
expect(service).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws clear error for non-existent bindings', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
const fakeToken = Symbol.for('Non.Existent.Service');
|
||||
expect(() => container.get(fakeToken)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR Dynamic Environment Variables', () => {
|
||||
it('config binding is a function (not a string)', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const config = container.get(CONFIG_TOKEN);
|
||||
|
||||
// Config should be a function that can be called
|
||||
expect(typeof config).toBe('function');
|
||||
|
||||
// Should be callable
|
||||
const configFn = config as ConfigFunction;
|
||||
expect(() => configFn()).not.toThrow();
|
||||
});
|
||||
|
||||
it('config function returns current environment value', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const getConfig = container.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
const configValue = getConfig();
|
||||
// Should return some value (could be fallback in test env)
|
||||
expect(typeof configValue).toBe('string');
|
||||
expect(configValue.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('multiple containers share the same config behavior', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container1 = createContainer();
|
||||
const container2 = createContainer();
|
||||
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
|
||||
// Both should return the same type of value
|
||||
expect(typeof config1()).toBe(typeof config2());
|
||||
});
|
||||
|
||||
it('ContainerManager singleton behavior', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const manager = ContainerManager.getInstance();
|
||||
const container1 = manager.getContainer();
|
||||
const container2 = manager.getContainer();
|
||||
|
||||
// Should be same instance
|
||||
expect(container1).toBe(container2);
|
||||
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
});
|
||||
|
||||
it('API clients work with dynamic config', () => {
|
||||
process.env.API_BASE_URL = 'http://api-test:3001';
|
||||
|
||||
const container = createContainer();
|
||||
const leagueClient = container.get(LEAGUE_API_CLIENT_TOKEN);
|
||||
|
||||
expect(leagueClient).toBeDefined();
|
||||
expect(leagueClient).not.toBeNull();
|
||||
expect(typeof leagueClient).toBe('object');
|
||||
});
|
||||
|
||||
it('dispose clears singleton instance', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const manager = ContainerManager.getInstance();
|
||||
const container1 = manager.getContainer();
|
||||
|
||||
manager.dispose();
|
||||
|
||||
const container2 = manager.getContainer();
|
||||
|
||||
// Should be different instances after dispose
|
||||
expect(container1).not.toBe(container2);
|
||||
});
|
||||
|
||||
it('services work after container recreation', () => {
|
||||
process.env.API_BASE_URL = 'http://test:3001';
|
||||
|
||||
const container1 = createContainer();
|
||||
const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
ContainerManager.getInstance().dispose();
|
||||
|
||||
const container2 = createContainer();
|
||||
const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction;
|
||||
|
||||
// Both should be functions
|
||||
expect(typeof config1).toBe('function');
|
||||
expect(typeof config2).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR Boot Safety', () => {
|
||||
it('resolves all tokens required for SSR entry rendering', () => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
const container = createContainer();
|
||||
|
||||
// Tokens typically used in SSR (middleware, layouts, initial page loads)
|
||||
const ssrTokens = [
|
||||
LOGGER_TOKEN,
|
||||
CONFIG_TOKEN,
|
||||
SESSION_SERVICE_TOKEN,
|
||||
AUTH_SERVICE_TOKEN,
|
||||
LEAGUE_SERVICE_TOKEN,
|
||||
DRIVER_SERVICE_TOKEN,
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
// API clients are often resolved by services
|
||||
AUTH_API_CLIENT_TOKEN,
|
||||
LEAGUE_API_CLIENT_TOKEN,
|
||||
];
|
||||
|
||||
for (const token of ssrTokens) {
|
||||
try {
|
||||
const service = container.get(token);
|
||||
expect(service, `Failed to resolve ${token.toString()}`).toBeDefined();
|
||||
} catch (error) {
|
||||
throw new Error(`SSR Boot Safety Failure: Could not resolve ${token.toString()}. Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,829 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
||||
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
import { fetchFeatureFlags, getEnabledFlags, isFeatureEnabled } from '../../shared/website/FeatureFlagHelpers';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
||||
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
|
||||
// Wait for API to be ready with seeded data before running tests
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
|
||||
console.log('[SETUP] Waiting for API to be ready...');
|
||||
|
||||
// Poll the API until it returns data (indicating seeding is complete)
|
||||
const maxAttempts = 60;
|
||||
const interval = 1000; // 1 second
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
// Try to fetch total drivers count - this endpoint should return > 0 after seeding
|
||||
const response = await request.get(`${API_BASE_URL}/drivers/total-drivers`);
|
||||
|
||||
if (response.ok()) {
|
||||
const data = await response.json();
|
||||
|
||||
// Check if we have actual drivers (count > 0)
|
||||
if (data && data.totalDrivers && data.totalDrivers > 0) {
|
||||
console.log(`[SETUP] API is ready with ${data.totalDrivers} drivers`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: API not ready yet (status: ${response.status()})`);
|
||||
} catch (error) {
|
||||
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: ${error.message}`);
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error('[SETUP] API failed to become ready with seeded data within timeout');
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to fetch feature flags from the API
|
||||
* Uses Playwright request context for compatibility across environments
|
||||
*/
|
||||
async function fetchFeatureFlagsWrapper(request: import('@playwright/test').APIRequestContext) {
|
||||
return fetchFeatureFlags(
|
||||
async (url) => {
|
||||
const response = await request.get(url);
|
||||
return {
|
||||
ok: response.ok(),
|
||||
json: () => response.json(),
|
||||
status: response.status()
|
||||
};
|
||||
},
|
||||
API_BASE_URL
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Website Pages - TypeORM Integration', () => {
|
||||
let routeManager: WebsiteRouteManager;
|
||||
|
||||
test.beforeEach(() => {
|
||||
routeManager = new WebsiteRouteManager();
|
||||
});
|
||||
|
||||
test('website loads and connects to API', async ({ page }) => {
|
||||
// Test that the website loads
|
||||
const response = await page.goto(WEBSITE_BASE_URL);
|
||||
expect(response?.ok()).toBe(true);
|
||||
|
||||
// Check that the page renders (body is visible)
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('all routes from RouteConfig are discoverable', async () => {
|
||||
expect(() => routeManager.getWebsiteRouteInventory()).not.toThrow();
|
||||
});
|
||||
|
||||
test('public routes are accessible without authentication', async ({ page }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const publicRoutes = routes.filter(r => r.access === 'public').slice(0, 5);
|
||||
|
||||
for (const route of publicRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
const status = response?.status();
|
||||
const finalUrl = page.url();
|
||||
|
||||
console.log(`[TEST DEBUG] Public route - Path: ${path}, Status: ${status}, Final URL: ${finalUrl}`);
|
||||
if (status === 500) {
|
||||
console.log(`[TEST DEBUG] 500 error on ${path} - Page title: ${await page.title()}`);
|
||||
}
|
||||
|
||||
// The /500 error page intentionally returns 500 status
|
||||
// All other routes should load successfully or show 404
|
||||
if (path === '/500') {
|
||||
expect(response?.status()).toBe(500);
|
||||
} else {
|
||||
expect(response?.ok() || response?.status() === 404).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('protected routes redirect unauthenticated users to login', async ({ page }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const protectedRoutes = routes.filter(r => r.access !== 'public').slice(0, 3);
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(path);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin routes require admin role', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const adminRoutes = routes.filter(r => r.access === 'admin').slice(0, 2);
|
||||
|
||||
for (const route of adminRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
// Regular auth user should be redirected to their home page (dashboard)
|
||||
{
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
try {
|
||||
const response = await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
const finalUrl = auth.page.url();
|
||||
console.log(`[TEST DEBUG] Admin route test - Path: ${path}`);
|
||||
console.log(`[TEST DEBUG] Response status: ${response?.status()}`);
|
||||
console.log(`[TEST DEBUG] Final URL: ${finalUrl}`);
|
||||
console.log(`[TEST DEBUG] Page title: ${await auth.page.title()}`);
|
||||
expect(auth.page.url().includes('dashboard')).toBeTruthy();
|
||||
} finally {
|
||||
try {
|
||||
await auth.context.close();
|
||||
} catch (e) {
|
||||
// Ignore context closing errors in test environment
|
||||
console.log(`[TEST DEBUG] Context close error (ignored): ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Admin user should have access
|
||||
{
|
||||
const admin = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
|
||||
try {
|
||||
await admin.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(admin.page.url().includes(path)).toBeTruthy();
|
||||
} finally {
|
||||
try {
|
||||
await admin.context.close();
|
||||
} catch (e) {
|
||||
// Ignore context closing errors in test environment
|
||||
console.log(`[TEST DEBUG] Context close error (ignored): ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor routes require sponsor role', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const sponsorRoutes = routes.filter(r => r.access === 'sponsor').slice(0, 2);
|
||||
|
||||
for (const route of sponsorRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
// Regular auth user should be redirected to their home page (dashboard)
|
||||
{
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
const finalUrl = auth.page.url();
|
||||
console.log(`[DEBUG] Final URL: ${finalUrl}`);
|
||||
console.log(`[DEBUG] Includes 'dashboard': ${finalUrl.includes('dashboard')}`);
|
||||
expect(finalUrl.includes('dashboard')).toBeTruthy();
|
||||
await auth.context.close();
|
||||
}
|
||||
|
||||
// Sponsor user should have access
|
||||
{
|
||||
const sponsor = await WebsiteAuthManager.createAuthContext(browser, request, 'sponsor');
|
||||
await sponsor.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(sponsor.page.url().includes(path)).toBeTruthy();
|
||||
await sponsor.context.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('auth routes redirect authenticated users away', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const authRoutes = routes.filter(r => r.access === 'auth').slice(0, 2);
|
||||
|
||||
for (const route of authRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
// Should redirect to dashboard or stay on the page
|
||||
const currentUrl = auth.page.url();
|
||||
expect(currentUrl.includes('dashboard') || currentUrl.includes(path)).toBeTruthy();
|
||||
|
||||
await auth.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('parameterized routes handle edge cases', async ({ page }) => {
|
||||
const edgeCases = routeManager.getParamEdgeCases();
|
||||
|
||||
for (const route of edgeCases) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
// Client-side pages return 200 even when data doesn't exist
|
||||
// They show error messages in the UI instead of HTTP 404
|
||||
// This is expected behavior for CSR pages in Next.js
|
||||
if (route.allowNotFound) {
|
||||
const status = response?.status();
|
||||
expect([200, 404, 500].includes(status ?? 0)).toBeTruthy();
|
||||
|
||||
// If it's 200, verify error message is shown in the UI
|
||||
if (status === 200) {
|
||||
const bodyText = await page.textContent('body');
|
||||
const hasErrorMessage = bodyText?.includes('not found') ||
|
||||
bodyText?.includes('doesn\'t exist') ||
|
||||
bodyText?.includes('Error');
|
||||
expect(hasErrorMessage).toBeTruthy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('no console or page errors on critical routes', async ({ page }) => {
|
||||
const faultRoutes = routeManager.getFaultInjectionRoutes();
|
||||
|
||||
for (const route of faultRoutes) {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const errors = capture.getErrors();
|
||||
|
||||
// Filter out known/expected errors
|
||||
const unexpectedErrors = errors.filter(error => {
|
||||
const msg = error.message.toLowerCase();
|
||||
// Filter out hydration warnings and other expected Next.js warnings
|
||||
return !msg.includes('hydration') &&
|
||||
!msg.includes('text content does not match') &&
|
||||
!msg.includes('warning:') &&
|
||||
!msg.includes('download the react devtools') &&
|
||||
!msg.includes('connection refused') &&
|
||||
!msg.includes('failed to load resource') &&
|
||||
!msg.includes('network error') &&
|
||||
!msg.includes('cors') &&
|
||||
!msg.includes('react does not recognize the `%s` prop on a dom element');
|
||||
});
|
||||
|
||||
// Check for critical runtime errors that should never occur
|
||||
const criticalErrors = errors.filter(error => {
|
||||
const msg = error.message.toLowerCase();
|
||||
return msg.includes('no queryclient set') ||
|
||||
msg.includes('use queryclientprovider') ||
|
||||
msg.includes('console.groupcollapsed is not a function') ||
|
||||
msg.includes('console.groupend is not a function');
|
||||
});
|
||||
|
||||
if (unexpectedErrors.length > 0) {
|
||||
console.log(`[TEST DEBUG] Unexpected errors on ${path}:`, unexpectedErrors);
|
||||
}
|
||||
|
||||
if (criticalErrors.length > 0) {
|
||||
console.log(`[TEST DEBUG] CRITICAL errors on ${path}:`, criticalErrors);
|
||||
throw new Error(`Critical runtime errors on ${path}: ${JSON.stringify(criticalErrors)}`);
|
||||
}
|
||||
|
||||
// Fail on any unexpected errors including DI binding failures
|
||||
expect(unexpectedErrors.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('detect DI binding failures and missing metadata on boot', async ({ page }) => {
|
||||
// Test critical routes that would trigger DI container creation
|
||||
const criticalRoutes = [
|
||||
'/leagues',
|
||||
'/dashboard',
|
||||
'/teams',
|
||||
'/drivers',
|
||||
'/races',
|
||||
'/leaderboards'
|
||||
];
|
||||
|
||||
for (const path of criticalRoutes) {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check for 500 errors
|
||||
const status = response?.status();
|
||||
if (status === 500) {
|
||||
console.log(`[TEST DEBUG] 500 error on ${path}`);
|
||||
const bodyText = await page.textContent('body');
|
||||
console.log(`[TEST DEBUG] Body content: ${bodyText?.substring(0, 1000)}`);
|
||||
|
||||
// If it's a 500 error, check if it's a known issue or a real DI failure
|
||||
// For now, we'll just log it and continue to see other routes
|
||||
}
|
||||
|
||||
// Check for DI-related errors in console
|
||||
const errors = capture.getErrors();
|
||||
const diErrors = errors.filter(error => {
|
||||
const msg = error.message.toLowerCase();
|
||||
return msg.includes('binding') ||
|
||||
msg.includes('metadata') ||
|
||||
msg.includes('inversify') ||
|
||||
msg.includes('symbol') ||
|
||||
msg.includes('no binding') ||
|
||||
msg.includes('not bound');
|
||||
});
|
||||
|
||||
// Check for React Query provider errors
|
||||
const queryClientErrors = errors.filter(error => {
|
||||
const msg = error.message.toLowerCase();
|
||||
return msg.includes('no queryclient set') ||
|
||||
msg.includes('use queryclientprovider');
|
||||
});
|
||||
|
||||
if (diErrors.length > 0) {
|
||||
console.log(`[TEST DEBUG] DI errors on ${path}:`, diErrors);
|
||||
}
|
||||
|
||||
if (queryClientErrors.length > 0) {
|
||||
console.log(`[TEST DEBUG] QueryClient errors on ${path}:`, queryClientErrors);
|
||||
throw new Error(`QueryClient provider missing on ${path}: ${JSON.stringify(queryClientErrors)}`);
|
||||
}
|
||||
|
||||
// Fail on DI errors
|
||||
expect(diErrors.length).toBe(0);
|
||||
|
||||
// We'll temporarily allow 500 status here to see if other routes work
|
||||
// and to avoid failing the whole suite if /leagues is broken
|
||||
// expect(status).not.toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
test('TypeORM session persistence across routes', async ({ page }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const testRoutes = routes.filter(r => r.access === 'public').slice(0, 5);
|
||||
|
||||
for (const route of testRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
// The /500 error page intentionally returns 500 status
|
||||
if (path === '/500') {
|
||||
expect(response?.status()).toBe(500);
|
||||
} else {
|
||||
expect(response?.ok() || response?.status() === 404).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('auth drift scenarios', async ({ page }) => {
|
||||
const driftRoutes = routeManager.getAuthDriftRoutes();
|
||||
|
||||
for (const route of driftRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
// Try accessing protected route without auth
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
const currentUrl = page.url();
|
||||
|
||||
expect(currentUrl.includes('login') || currentUrl.includes('auth')).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('handles invalid routes gracefully', async ({ page }) => {
|
||||
const invalidRoutes = [
|
||||
'/invalid-route',
|
||||
'/leagues/invalid-id',
|
||||
'/drivers/invalid-id',
|
||||
];
|
||||
|
||||
for (const route of invalidRoutes) {
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${route}`);
|
||||
|
||||
const status = response?.status();
|
||||
const url = page.url();
|
||||
|
||||
expect([200, 404].includes(status ?? 0) || url.includes('/auth/login')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('leagues pages render meaningful content server-side', async ({ page }) => {
|
||||
// Test the main leagues page
|
||||
const leaguesResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues`);
|
||||
|
||||
// Check for 500 errors and log content for debugging
|
||||
if (leaguesResponse?.status() === 500) {
|
||||
const bodyText = await page.textContent('body');
|
||||
console.log(`[TEST DEBUG] 500 error on /leagues. Body: ${bodyText?.substring(0, 1000)}`);
|
||||
}
|
||||
|
||||
expect(leaguesResponse?.ok()).toBe(true);
|
||||
|
||||
// Check that the page has meaningful content (not just loading states or empty)
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content
|
||||
|
||||
// Check for key elements that indicate the page is working
|
||||
const hasLeaguesContent = bodyText?.includes('Leagues') ||
|
||||
bodyText?.includes('Find Your Grid') ||
|
||||
bodyText?.includes('Create League');
|
||||
expect(hasLeaguesContent).toBeTruthy();
|
||||
|
||||
// Test the league detail page (with a sample league ID)
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1`);
|
||||
// May redirect to login if not authenticated, or show error if league doesn't exist
|
||||
// Just verify the page loads without errors
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
// Test the standings page
|
||||
const standingsResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/standings`);
|
||||
expect(standingsResponse?.ok() || standingsResponse?.status() === 404 || standingsResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
// Test the schedule page
|
||||
const scheduleResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/schedule`);
|
||||
expect(scheduleResponse?.ok() || scheduleResponse?.status() === 404 || scheduleResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
// Test the rulebook page
|
||||
const rulebookResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/rulebook`);
|
||||
expect(rulebookResponse?.ok() || rulebookResponse?.status() === 404 || rulebookResponse?.status() === 302).toBeTruthy();
|
||||
});
|
||||
|
||||
test('leaderboards pages render meaningful content server-side', async ({ page }) => {
|
||||
// Test the main leaderboards page
|
||||
const leaderboardsResponse = await page.goto(`${WEBSITE_BASE_URL}/leaderboards`);
|
||||
|
||||
// In test environment, the page might redirect or show errors due to API issues
|
||||
// Just verify the page loads without crashing
|
||||
const leaderboardsStatus = leaderboardsResponse?.status();
|
||||
expect([200, 302, 404, 500].includes(leaderboardsStatus ?? 0)).toBeTruthy();
|
||||
|
||||
// Check that the page has some content (even if it's an error message)
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
expect(bodyText?.length).toBeGreaterThan(10); // Minimal content check
|
||||
|
||||
// Check for key elements that indicate the page structure is working
|
||||
const hasLeaderboardContent = bodyText?.includes('Leaderboards') ||
|
||||
bodyText?.includes('Driver') ||
|
||||
bodyText?.includes('Team') ||
|
||||
bodyText?.includes('Error') ||
|
||||
bodyText?.includes('Loading') ||
|
||||
bodyText?.includes('Something went wrong');
|
||||
expect(hasLeaderboardContent).toBeTruthy();
|
||||
|
||||
// Test the driver rankings page
|
||||
const driverResponse = await page.goto(`${WEBSITE_BASE_URL}/leaderboards/drivers`);
|
||||
const driverStatus = driverResponse?.status();
|
||||
expect([200, 302, 404, 500].includes(driverStatus ?? 0)).toBeTruthy();
|
||||
|
||||
const driverBodyText = await page.textContent('body');
|
||||
expect(driverBodyText).toBeTruthy();
|
||||
expect(driverBodyText?.length).toBeGreaterThan(10);
|
||||
|
||||
const hasDriverContent = driverBodyText?.includes('Driver') ||
|
||||
driverBodyText?.includes('Ranking') ||
|
||||
driverBodyText?.includes('Leaderboard') ||
|
||||
driverBodyText?.includes('Error') ||
|
||||
driverBodyText?.includes('Loading') ||
|
||||
driverBodyText?.includes('Something went wrong');
|
||||
expect(hasDriverContent).toBeTruthy();
|
||||
|
||||
// Test the team leaderboard page
|
||||
const teamResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/leaderboard`);
|
||||
const teamStatus = teamResponse?.status();
|
||||
expect([200, 302, 404, 500].includes(teamStatus ?? 0)).toBeTruthy();
|
||||
|
||||
const teamBodyText = await page.textContent('body');
|
||||
expect(teamBodyText).toBeTruthy();
|
||||
expect(teamBodyText?.length).toBeGreaterThan(10);
|
||||
|
||||
const hasTeamContent = teamBodyText?.includes('Team') ||
|
||||
teamBodyText?.includes('Leaderboard') ||
|
||||
teamBodyText?.includes('Ranking') ||
|
||||
teamBodyText?.includes('Error') ||
|
||||
teamBodyText?.includes('Loading') ||
|
||||
teamBodyText?.includes('Something went wrong');
|
||||
expect(hasTeamContent).toBeTruthy();
|
||||
});
|
||||
|
||||
test('races pages render meaningful content server-side', async ({ page }) => {
|
||||
// Test the main races calendar page
|
||||
const racesResponse = await page.goto(`${WEBSITE_BASE_URL}/races`);
|
||||
expect(racesResponse?.ok()).toBe(true);
|
||||
|
||||
// Check that the page has meaningful content (not just loading states or empty)
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content
|
||||
|
||||
// Check for key elements that indicate the page is working
|
||||
const hasRacesContent = bodyText?.includes('Races') ||
|
||||
bodyText?.includes('Calendar') ||
|
||||
bodyText?.includes('Schedule') ||
|
||||
bodyText?.includes('Upcoming');
|
||||
expect(hasRacesContent).toBeTruthy();
|
||||
|
||||
// Test the all races page
|
||||
const allRacesResponse = await page.goto(`${WEBSITE_BASE_URL}/races/all`);
|
||||
expect(allRacesResponse?.ok()).toBe(true);
|
||||
|
||||
const allRacesBodyText = await page.textContent('body');
|
||||
expect(allRacesBodyText).toBeTruthy();
|
||||
expect(allRacesBodyText?.length).toBeGreaterThan(50);
|
||||
|
||||
const hasAllRacesContent = allRacesBodyText?.includes('All Races') ||
|
||||
allRacesBodyText?.includes('Races') ||
|
||||
allRacesBodyText?.includes('Pagination');
|
||||
expect(hasAllRacesContent).toBeTruthy();
|
||||
|
||||
// Test the race detail page (with a sample race ID)
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123`);
|
||||
// May redirect to login if not authenticated, or show error if race doesn't exist
|
||||
// Just verify the page loads without errors
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
// Test the race results page
|
||||
const resultsResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123/results`);
|
||||
expect(resultsResponse?.ok() || resultsResponse?.status() === 404 || resultsResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
// Test the race stewarding page
|
||||
const stewardingResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123/stewarding`);
|
||||
expect(stewardingResponse?.ok() || stewardingResponse?.status() === 404 || stewardingResponse?.status() === 302).toBeTruthy();
|
||||
});
|
||||
|
||||
test('races pages are not empty or useless', async ({ page }) => {
|
||||
// Test the main races calendar page
|
||||
const racesResponse = await page.goto(`${WEBSITE_BASE_URL}/races`);
|
||||
expect(racesResponse?.ok()).toBe(true);
|
||||
|
||||
const racesBodyText = await page.textContent('body');
|
||||
expect(racesBodyText).toBeTruthy();
|
||||
|
||||
// Ensure the page has substantial content (not just "Loading..." or empty)
|
||||
expect(racesBodyText?.length).toBeGreaterThan(100);
|
||||
|
||||
// Ensure the page doesn't just show error messages or empty states
|
||||
const isEmptyOrError = racesBodyText?.includes('Loading...') ||
|
||||
racesBodyText?.includes('Error loading') ||
|
||||
racesBodyText?.includes('No races found') ||
|
||||
racesBodyText?.trim().length < 50;
|
||||
expect(isEmptyOrError).toBe(false);
|
||||
|
||||
// Test the all races page
|
||||
const allRacesResponse = await page.goto(`${WEBSITE_BASE_URL}/races/all`);
|
||||
expect(allRacesResponse?.ok()).toBe(true);
|
||||
|
||||
const allRacesBodyText = await page.textContent('body');
|
||||
expect(allRacesBodyText).toBeTruthy();
|
||||
expect(allRacesBodyText?.length).toBeGreaterThan(100);
|
||||
|
||||
const isAllRacesEmptyOrError = allRacesBodyText?.includes('Loading...') ||
|
||||
allRacesBodyText?.includes('Error loading') ||
|
||||
allRacesBodyText?.includes('No races found') ||
|
||||
allRacesBodyText?.trim().length < 50;
|
||||
expect(isAllRacesEmptyOrError).toBe(false);
|
||||
});
|
||||
|
||||
test('drivers pages render meaningful content server-side', async ({ page }) => {
|
||||
// Test the main drivers page
|
||||
const driversResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers`);
|
||||
expect(driversResponse?.ok()).toBe(true);
|
||||
|
||||
// Check that the page has meaningful content (not just loading states or empty)
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content
|
||||
|
||||
// Check for key elements that indicate the page is working
|
||||
const hasDriversContent = bodyText?.includes('Drivers') ||
|
||||
bodyText?.includes('Featured Drivers') ||
|
||||
bodyText?.includes('Top Drivers') ||
|
||||
bodyText?.includes('Skill Distribution');
|
||||
expect(hasDriversContent).toBeTruthy();
|
||||
|
||||
// Test the driver detail page (with a sample driver ID)
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers/driver-123`);
|
||||
// May redirect to login if not authenticated, or show error if driver doesn't exist
|
||||
// Just verify the page loads without errors
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
});
|
||||
|
||||
test('drivers pages are not empty or useless', async ({ page }) => {
|
||||
// Test the main drivers page
|
||||
const driversResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers`);
|
||||
expect(driversResponse?.ok()).toBe(true);
|
||||
|
||||
const driversBodyText = await page.textContent('body');
|
||||
expect(driversBodyText).toBeTruthy();
|
||||
|
||||
// Ensure the page has substantial content (not just "Loading..." or empty)
|
||||
expect(driversBodyText?.length).toBeGreaterThan(100);
|
||||
|
||||
// Ensure the page doesn't just show error messages or empty states
|
||||
const isEmptyOrError = driversBodyText?.includes('Loading...') ||
|
||||
driversBodyText?.includes('Error loading') ||
|
||||
driversBodyText?.includes('No drivers found') ||
|
||||
driversBodyText?.trim().length < 50;
|
||||
expect(isEmptyOrError).toBe(false);
|
||||
|
||||
// Test the driver detail page
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers/driver-123`);
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
const detailBodyText = await page.textContent('body');
|
||||
expect(detailBodyText).toBeTruthy();
|
||||
expect(detailBodyText?.length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
test('teams pages render meaningful content server-side', async ({ page }) => {
|
||||
// Test the main teams page
|
||||
const teamsResponse = await page.goto(`${WEBSITE_BASE_URL}/teams`);
|
||||
expect(teamsResponse?.ok()).toBe(true);
|
||||
|
||||
// Check that the page has meaningful content (not just loading states or empty)
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content
|
||||
|
||||
// Check for key elements that indicate the page is working
|
||||
const hasTeamsContent = bodyText?.includes('Teams') ||
|
||||
bodyText?.includes('Find Your') ||
|
||||
bodyText?.includes('Crew') ||
|
||||
bodyText?.includes('Create Team');
|
||||
expect(hasTeamsContent).toBeTruthy();
|
||||
|
||||
// Test the team detail page (with a sample team ID)
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/team-123`);
|
||||
// May redirect to login if not authenticated, or show error if team doesn't exist
|
||||
// Just verify the page loads without errors
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
});
|
||||
|
||||
test('teams pages are not empty or useless', async ({ page }) => {
|
||||
// Test the main teams page
|
||||
const teamsResponse = await page.goto(`${WEBSITE_BASE_URL}/teams`);
|
||||
expect(teamsResponse?.ok()).toBe(true);
|
||||
|
||||
const teamsBodyText = await page.textContent('body');
|
||||
expect(teamsBodyText).toBeTruthy();
|
||||
|
||||
// Ensure the page has substantial content (not just "Loading..." or empty)
|
||||
expect(teamsBodyText?.length).toBeGreaterThan(100);
|
||||
|
||||
// Ensure the page doesn't just show error messages or empty states
|
||||
const isEmptyOrError = teamsBodyText?.includes('Loading...') ||
|
||||
teamsBodyText?.includes('Error loading') ||
|
||||
teamsBodyText?.includes('No teams found') ||
|
||||
teamsBodyText?.trim().length < 50;
|
||||
expect(isEmptyOrError).toBe(false);
|
||||
|
||||
// Test the team detail page
|
||||
const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/team-123`);
|
||||
expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy();
|
||||
|
||||
const detailBodyText = await page.textContent('body');
|
||||
expect(detailBodyText).toBeTruthy();
|
||||
expect(detailBodyText?.length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
// ==================== FEATURE FLAG TESTS ====================
|
||||
// These tests validate API-driven feature flags
|
||||
|
||||
test('features endpoint returns valid contract and reachable from API', async ({ request }) => {
|
||||
// Contract test: verify /features endpoint returns correct shape
|
||||
const featureData = await fetchFeatureFlagsWrapper(request);
|
||||
|
||||
// Verify contract: { features: object, timestamp: string }
|
||||
expect(featureData).toHaveProperty('features');
|
||||
expect(featureData).toHaveProperty('timestamp');
|
||||
|
||||
// Verify features is an object
|
||||
expect(typeof featureData.features).toBe('object');
|
||||
expect(featureData.features).not.toBeNull();
|
||||
|
||||
// Verify timestamp is a string (ISO format)
|
||||
expect(typeof featureData.timestamp).toBe('string');
|
||||
expect(featureData.timestamp.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify at least one feature exists (basic sanity check)
|
||||
const featureKeys = Object.keys(featureData.features);
|
||||
expect(featureKeys.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all feature values are valid states
|
||||
const validStates = ['enabled', 'disabled', 'coming_soon', 'hidden'];
|
||||
Object.values(featureData.features).forEach(value => {
|
||||
expect(validStates).toContain(value);
|
||||
});
|
||||
|
||||
console.log(`[FEATURE TEST] API features endpoint verified: ${featureKeys.length} flags loaded`);
|
||||
});
|
||||
|
||||
test('conditional UI rendering based on feature flags', async ({ page, request }) => {
|
||||
// Fetch current feature flags from API
|
||||
const featureData = await fetchFeatureFlagsWrapper(request);
|
||||
const enabledFlags = getEnabledFlags(featureData);
|
||||
|
||||
console.log(`[FEATURE TEST] Enabled flags: ${enabledFlags.join(', ')}`);
|
||||
|
||||
// Test 1: Verify beta features are conditionally rendered
|
||||
// Check if beta.newUI feature affects UI
|
||||
const betaNewUIEnabled = isFeatureEnabled(featureData, 'beta.newUI');
|
||||
|
||||
// Navigate to a page that might have beta features
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}/dashboard`);
|
||||
expect(response?.ok()).toBe(true);
|
||||
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toBeTruthy();
|
||||
|
||||
// If beta.newUI is enabled, we should see beta UI elements
|
||||
// If disabled, beta elements should be absent
|
||||
if (betaNewUIEnabled) {
|
||||
console.log('[FEATURE TEST] beta.newUI is enabled - checking for beta UI elements');
|
||||
// Beta UI might have specific markers - check for common beta indicators
|
||||
const hasBetaIndicators = bodyText?.includes('beta') ||
|
||||
bodyText?.includes('Beta') ||
|
||||
bodyText?.includes('NEW') ||
|
||||
bodyText?.includes('experimental');
|
||||
// Beta features may or may not be visible depending on implementation
|
||||
// This test validates the flag is being read correctly
|
||||
// We don't assert on hasBetaIndicators since beta UI may not be implemented yet
|
||||
console.log(`[FEATURE TEST] Beta indicators found: ${hasBetaIndicators}`);
|
||||
} else {
|
||||
console.log('[FEATURE TEST] beta.newUI is disabled - verifying beta UI is absent');
|
||||
// If disabled, ensure no beta indicators are present
|
||||
const hasBetaIndicators = bodyText?.includes('beta') ||
|
||||
bodyText?.includes('Beta') ||
|
||||
bodyText?.includes('experimental');
|
||||
// Beta UI should not be visible when disabled
|
||||
expect(hasBetaIndicators).toBe(false);
|
||||
}
|
||||
|
||||
// Test 2: Verify platform features are enabled
|
||||
const platformFeatures = ['platform.leagues', 'platform.teams', 'platform.drivers'];
|
||||
platformFeatures.forEach(flag => {
|
||||
const isEnabled = isFeatureEnabled(featureData, flag);
|
||||
expect(isEnabled).toBe(true); // Should be enabled in test environment
|
||||
});
|
||||
});
|
||||
|
||||
test('feature flag state drives UI behavior', async ({ page, request }) => {
|
||||
// This test validates that feature flags actually control UI visibility
|
||||
const featureData = await fetchFeatureFlagsWrapper(request);
|
||||
|
||||
// Test sponsor management feature
|
||||
const sponsorManagementEnabled = isFeatureEnabled(featureData, 'sponsors.management');
|
||||
|
||||
// Navigate to sponsor-related area
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}/sponsor/dashboard`);
|
||||
|
||||
// If sponsor management is disabled, we should be redirected or see access denied
|
||||
if (!sponsorManagementEnabled) {
|
||||
// Should redirect away or show access denied
|
||||
const currentUrl = page.url();
|
||||
const isRedirected = !currentUrl.includes('/sponsor/dashboard');
|
||||
|
||||
if (isRedirected) {
|
||||
console.log('[FEATURE TEST] Sponsor management disabled - user redirected as expected');
|
||||
} else {
|
||||
// If not redirected, should show access denied message
|
||||
const bodyText = await page.textContent('body');
|
||||
const hasAccessDenied = bodyText?.includes('disabled') ||
|
||||
bodyText?.includes('unavailable') ||
|
||||
bodyText?.includes('not available');
|
||||
expect(hasAccessDenied).toBe(true);
|
||||
}
|
||||
} else {
|
||||
// Should be able to access sponsor dashboard
|
||||
expect(response?.ok()).toBe(true);
|
||||
console.log('[FEATURE TEST] Sponsor management enabled - dashboard accessible');
|
||||
}
|
||||
});
|
||||
|
||||
test('feature flags are consistent across environments', async ({ request }) => {
|
||||
// This test validates that the same feature endpoint works in both local dev and docker e2e
|
||||
const featureData = await fetchFeatureFlagsWrapper(request);
|
||||
|
||||
// Verify the API base URL is correctly resolved
|
||||
const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
console.log(`[FEATURE TEST] Using API base URL: ${apiBaseUrl}`);
|
||||
|
||||
// Verify we got valid data
|
||||
expect(featureData.features).toBeDefined();
|
||||
expect(Object.keys(featureData.features).length).toBeGreaterThan(0);
|
||||
|
||||
// In test environment, core features should be enabled
|
||||
const requiredFeatures = [
|
||||
'platform.dashboard',
|
||||
'platform.leagues',
|
||||
'platform.teams',
|
||||
'platform.drivers',
|
||||
'platform.races',
|
||||
'platform.leaderboards'
|
||||
];
|
||||
|
||||
requiredFeatures.forEach(flag => {
|
||||
const isEnabled = isFeatureEnabled(featureData, flag);
|
||||
expect(isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
console.log('[FEATURE TEST] All required platform features are enabled');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Website SSR Smoke Tests
|
||||
*
|
||||
* Run with: npx vitest run tests/smoke/website-ssr.test.ts --config vitest.smoke.config.ts
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { getWebsiteRouteContracts, RouteContract } from '../shared/website/RouteContractSpec';
|
||||
import { WebsiteServerHarness } from '../integration/harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../integration/harness/ApiServerHarness';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3000';
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
|
||||
|
||||
describe('Website SSR Contract Suite', () => {
|
||||
const contracts = getWebsiteRouteContracts();
|
||||
let websiteHarness: WebsiteServerHarness | null = null;
|
||||
let apiHarness: ApiServerHarness | null = null;
|
||||
let errorCount500 = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 1. Ensure API is running
|
||||
if (API_BASE_URL.includes('localhost')) {
|
||||
try {
|
||||
await fetch(`${API_BASE_URL}/health`);
|
||||
console.log(`API already running at ${API_BASE_URL}`);
|
||||
} catch (e) {
|
||||
console.log(`Starting API server harness on ${API_BASE_URL}...`);
|
||||
apiHarness = new ApiServerHarness({
|
||||
port: parseInt(new URL(API_BASE_URL).port) || 3001,
|
||||
});
|
||||
await apiHarness.start();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ensure Website is running
|
||||
if (WEBSITE_BASE_URL.includes('localhost')) {
|
||||
try {
|
||||
await fetch(WEBSITE_BASE_URL, { method: 'HEAD' });
|
||||
console.log(`Website server already running at ${WEBSITE_BASE_URL}`);
|
||||
} catch (e) {
|
||||
console.log(`Starting website server harness on ${WEBSITE_BASE_URL}...`);
|
||||
websiteHarness = new WebsiteServerHarness({
|
||||
port: parseInt(new URL(WEBSITE_BASE_URL).port) || 3000,
|
||||
});
|
||||
await websiteHarness.start();
|
||||
}
|
||||
}
|
||||
}, 120000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (websiteHarness) {
|
||||
await websiteHarness.stop();
|
||||
}
|
||||
if (apiHarness) {
|
||||
await apiHarness.stop();
|
||||
}
|
||||
|
||||
// Fail suite on bursts of 500s (e.g. > 3)
|
||||
if (errorCount500 > 3) {
|
||||
throw new Error(`Suite failed due to high error rate: ${errorCount500} routes returned 500`);
|
||||
}
|
||||
|
||||
// Fail on uncaught exceptions in logs
|
||||
if (websiteHarness?.hasErrorPatterns()) {
|
||||
console.error('Server logs contained error patterns:\n' + websiteHarness.getLogTail(50));
|
||||
throw new Error('Suite failed due to error patterns in server logs');
|
||||
}
|
||||
});
|
||||
|
||||
test.each(contracts)('Contract: $path', async (contract: RouteContract) => {
|
||||
const url = `${WEBSITE_BASE_URL}${contract.path}`;
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, { redirect: 'manual' });
|
||||
} catch (e) {
|
||||
const logTail = websiteHarness ? `\nServer Log Tail:\n${websiteHarness.getLogTail(20)}` : '';
|
||||
throw new Error(`Failed to fetch ${url}: ${e.message}${logTail}`);
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
const text = await response.text();
|
||||
const location = response.headers.get('location');
|
||||
|
||||
if (status === 500 && contract.expectedStatus !== 'errorRoute') {
|
||||
errorCount500++;
|
||||
}
|
||||
|
||||
const failureContext = `
|
||||
Route: ${contract.path}
|
||||
Status: ${status}
|
||||
Location: ${location || 'N/A'}
|
||||
HTML (clipped): ${text.slice(0, 500)}...
|
||||
${websiteHarness ? `\nServer Log Tail:\n${websiteHarness.getLogTail(10)}` : ''}
|
||||
`.trim();
|
||||
|
||||
// 1. Status class matches expectedStatus
|
||||
if (contract.expectedStatus === 'ok') {
|
||||
expect(status, failureContext).toBe(200);
|
||||
} else if (contract.expectedStatus === 'redirect') {
|
||||
expect(status, failureContext).toBeGreaterThanOrEqual(300);
|
||||
expect(status, failureContext).toBeLessThan(400);
|
||||
} else if (contract.expectedStatus === 'notFoundAllowed') {
|
||||
expect([200, 404], failureContext).toContain(status);
|
||||
} else if (contract.expectedStatus === 'errorRoute') {
|
||||
expect([200, 404, 500], failureContext).toContain(status);
|
||||
}
|
||||
|
||||
// 2. Redirect location semantics
|
||||
if (contract.expectedStatus === 'redirect' && contract.expectedRedirectTo) {
|
||||
expect(location, failureContext).toContain(contract.expectedRedirectTo);
|
||||
if (contract.accessLevel !== 'public' && contract.expectedRedirectTo.includes('/auth/login')) {
|
||||
expect(location, failureContext).toContain('returnTo=');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. HTML sanity checks
|
||||
if (status === 200 || (status === 404 && contract.expectedStatus === 'notFoundAllowed')) {
|
||||
if (contract.ssrMustContain) {
|
||||
for (const pattern of contract.ssrMustContain) {
|
||||
expect(text, failureContext).toMatch(pattern);
|
||||
}
|
||||
}
|
||||
if (contract.ssrMustNotContain) {
|
||||
for (const pattern of contract.ssrMustNotContain) {
|
||||
expect(text, failureContext).not.toMatch(pattern);
|
||||
}
|
||||
}
|
||||
if (contract.minTextLength) {
|
||||
expect(text.length, failureContext).toBeGreaterThanOrEqual(contract.minTextLength);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,281 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../../..');
|
||||
const packagesRoot = path.join(repoRoot, 'packages');
|
||||
|
||||
type PackageKind =
|
||||
| 'racing-domain'
|
||||
| 'racing-application'
|
||||
| 'racing-infrastructure'
|
||||
| 'racing-demo-infrastructure'
|
||||
| 'other';
|
||||
|
||||
interface TsFile {
|
||||
filePath: string;
|
||||
kind: PackageKind;
|
||||
}
|
||||
|
||||
function classifyFile(filePath: string): PackageKind {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
|
||||
// Bounded-context domain lives under core/racing/domain
|
||||
if (normalized.includes('/core/racing/domain/')) {
|
||||
return 'racing-domain';
|
||||
}
|
||||
if (normalized.includes('/core/racing-application/')) {
|
||||
return 'racing-application';
|
||||
}
|
||||
if (normalized.includes('/core/racing-infrastructure/')) {
|
||||
return 'racing-infrastructure';
|
||||
}
|
||||
if (normalized.includes('/core/racing-demo-infrastructure/')) {
|
||||
return 'racing-demo-infrastructure';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function collectTsFiles(dir: string): TsFile[] {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const files: TsFile[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectTsFiles(fullPath));
|
||||
} else if (entry.isFile()) {
|
||||
if (
|
||||
entry.name.endsWith('.ts') ||
|
||||
entry.name.endsWith('.tsx')
|
||||
) {
|
||||
const kind = classifyFile(fullPath);
|
||||
if (kind !== 'other') {
|
||||
files.push({ filePath: fullPath, kind });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
interface ImportViolation {
|
||||
file: string;
|
||||
line: number;
|
||||
moduleSpecifier: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
function extractImportModule(line: string): string | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('import')) return null;
|
||||
|
||||
// Handle: import ... from 'x';
|
||||
const fromMatch = trimmed.match(/from\s+['"](.*)['"]/);
|
||||
if (fromMatch) {
|
||||
return fromMatch[1] || null;
|
||||
}
|
||||
|
||||
// Handle: import 'x';
|
||||
const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/);
|
||||
if (sideEffectMatch) {
|
||||
return sideEffectMatch[1] || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('Package dependency structure for racing slice', () => {
|
||||
const tsFiles = collectTsFiles(packagesRoot);
|
||||
|
||||
it('enforces import boundaries for racing-domain', () => {
|
||||
const violations: ImportViolation[] = [];
|
||||
|
||||
const forbiddenPrefixes = [
|
||||
'@gridpilot/racing-application',
|
||||
'@gridpilot/racing-infrastructure',
|
||||
'@gridpilot/racing-demo-infrastructure',
|
||||
'apps/',
|
||||
'@/',
|
||||
'react',
|
||||
'next',
|
||||
'electron',
|
||||
];
|
||||
|
||||
for (const { filePath, kind } of tsFiles) {
|
||||
if (kind !== 'racing-domain') continue;
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const moduleSpecifier = extractImportModule(line);
|
||||
if (!moduleSpecifier) return;
|
||||
|
||||
for (const prefix of forbiddenPrefixes) {
|
||||
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
moduleSpecifier,
|
||||
reason: 'racing-domain must not depend on application, infrastructure, apps, or UI frameworks',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
const message =
|
||||
'Found forbidden imports in racing domain layer (core/racing/domain):\n' +
|
||||
violations
|
||||
.map(
|
||||
(v) =>
|
||||
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
|
||||
)
|
||||
.join('\n');
|
||||
expect(message).toBe('');
|
||||
} else {
|
||||
expect(violations).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('enforces import boundaries for racing-application', () => {
|
||||
const violations: ImportViolation[] = [];
|
||||
|
||||
const forbiddenPrefixes = [
|
||||
'@gridpilot/racing-infrastructure',
|
||||
'@gridpilot/racing-demo-infrastructure',
|
||||
'apps/',
|
||||
'@/',
|
||||
];
|
||||
|
||||
const allowedPrefixes = [
|
||||
'@gridpilot/racing',
|
||||
'@gridpilot/shared-result',
|
||||
'@gridpilot/identity',
|
||||
];
|
||||
|
||||
for (const { filePath, kind } of tsFiles) {
|
||||
if (kind !== 'racing-application') continue;
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const moduleSpecifier = extractImportModule(line);
|
||||
if (!moduleSpecifier) return;
|
||||
|
||||
for (const prefix of forbiddenPrefixes) {
|
||||
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
moduleSpecifier,
|
||||
reason: 'racing-application must not depend on infrastructure or apps',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleSpecifier.startsWith('@gridpilot/')) {
|
||||
const isAllowed = allowedPrefixes.some((prefix) =>
|
||||
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
|
||||
);
|
||||
if (!isAllowed) {
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
moduleSpecifier,
|
||||
reason: 'racing-application should only depend on domain, shared-result, or other domain packages',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
const message =
|
||||
'Found forbidden imports in core/racing-application:\n' +
|
||||
violations
|
||||
.map(
|
||||
(v) =>
|
||||
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
|
||||
)
|
||||
.join('\n');
|
||||
expect(message).toBe('');
|
||||
} else {
|
||||
expect(violations).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('enforces import boundaries for racing infrastructure packages', () => {
|
||||
const violations: ImportViolation[] = [];
|
||||
|
||||
const forbiddenPrefixes = ['apps/', '@/'];
|
||||
|
||||
const allowedPrefixes = [
|
||||
'@gridpilot/racing',
|
||||
'@gridpilot/shared-result',
|
||||
'@gridpilot/testing-support',
|
||||
'@gridpilot/social',
|
||||
];
|
||||
|
||||
for (const { filePath, kind } of tsFiles) {
|
||||
if (
|
||||
kind !== 'racing-infrastructure' &&
|
||||
kind !== 'racing-demo-infrastructure'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const moduleSpecifier = extractImportModule(line);
|
||||
if (!moduleSpecifier) return;
|
||||
|
||||
for (const prefix of forbiddenPrefixes) {
|
||||
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
moduleSpecifier,
|
||||
reason: 'racing infrastructure must not depend on apps or @/ aliases',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleSpecifier.startsWith('@gridpilot/')) {
|
||||
const isAllowed = allowedPrefixes.some((prefix) =>
|
||||
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
|
||||
);
|
||||
if (!isAllowed) {
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
moduleSpecifier,
|
||||
reason: 'racing infrastructure should depend only on domain, shared-result, or testing-support',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
const message =
|
||||
'Found forbidden imports in racing infrastructure packages:\n' +
|
||||
violations
|
||||
.map(
|
||||
(v) =>
|
||||
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
|
||||
)
|
||||
.join('\n');
|
||||
expect(message).toBe('');
|
||||
} else {
|
||||
expect(violations).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { BaseApiClient } from '../../../apps/website/lib/api/base/BaseApiClient';
|
||||
import { Logger } from '../../../apps/website/lib/interfaces/Logger';
|
||||
import { ErrorReporter } from '../../../apps/website/lib/interfaces/ErrorReporter';
|
||||
|
||||
describe('BaseApiClient - Invariants', () => {
|
||||
let client: BaseApiClient;
|
||||
let mockLogger: Logger;
|
||||
let mockErrorReporter: ErrorReporter;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLogger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
mockErrorReporter = {
|
||||
report: vi.fn(),
|
||||
};
|
||||
client = new BaseApiClient(
|
||||
'https://api.example.com',
|
||||
mockErrorReporter,
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
describe('classifyError()', () => {
|
||||
it('should classify 5xx as SERVER_ERROR', () => {
|
||||
expect((client as any).classifyError(500)).toBe('SERVER_ERROR');
|
||||
expect((client as any).classifyError(503)).toBe('SERVER_ERROR');
|
||||
});
|
||||
|
||||
it('should classify 429 as RATE_LIMIT_ERROR', () => {
|
||||
expect((client as any).classifyError(429)).toBe('RATE_LIMIT_ERROR');
|
||||
});
|
||||
|
||||
it('should classify 401/403 as AUTH_ERROR', () => {
|
||||
expect((client as any).classifyError(401)).toBe('AUTH_ERROR');
|
||||
expect((client as any).classifyError(403)).toBe('AUTH_ERROR');
|
||||
});
|
||||
|
||||
it('should classify 400 as VALIDATION_ERROR', () => {
|
||||
expect((client as any).classifyError(400)).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should classify 404 as NOT_FOUND', () => {
|
||||
expect((client as any).classifyError(404)).toBe('NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should classify other 4xx as UNKNOWN_ERROR', () => {
|
||||
expect((client as any).classifyError(418)).toBe('UNKNOWN_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRetryableError()', () => {
|
||||
it('should return true for retryable error types', () => {
|
||||
expect((client as any).isRetryableError('NETWORK_ERROR')).toBe(true);
|
||||
expect((client as any).isRetryableError('SERVER_ERROR')).toBe(true);
|
||||
expect((client as any).isRetryableError('RATE_LIMIT_ERROR')).toBe(true);
|
||||
expect((client as any).isRetryableError('TIMEOUT_ERROR')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-retryable error types', () => {
|
||||
expect((client as any).isRetryableError('AUTH_ERROR')).toBe(false);
|
||||
expect((client as any).isRetryableError('VALIDATION_ERROR')).toBe(false);
|
||||
expect((client as any).isRetryableError('NOT_FOUND')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { getEnabledFlags, isFeatureEnabled, fetchFeatureFlags, FeatureFlagData } from '../../shared/website/FeatureFlagHelpers';
|
||||
|
||||
describe('FeatureFlagHelpers', () => {
|
||||
const mockFeatureData: FeatureFlagData = {
|
||||
features: {
|
||||
'feature.a': 'enabled',
|
||||
'feature.b': 'disabled',
|
||||
'feature.c': 'coming_soon',
|
||||
'feature.d': 'enabled',
|
||||
},
|
||||
timestamp: '2026-01-17T16:00:00Z',
|
||||
};
|
||||
|
||||
describe('getEnabledFlags()', () => {
|
||||
it('should return only enabled flags', () => {
|
||||
const enabled = getEnabledFlags(mockFeatureData);
|
||||
expect(enabled).toEqual(['feature.a', 'feature.d']);
|
||||
});
|
||||
|
||||
it('should return empty array if no features', () => {
|
||||
expect(getEnabledFlags({ features: {}, timestamp: '' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null/undefined features', () => {
|
||||
expect(getEnabledFlags({ features: null as unknown as Record<string, string>, timestamp: '' })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFeatureEnabled()', () => {
|
||||
it('should return true for enabled features', () => {
|
||||
expect(isFeatureEnabled(mockFeatureData, 'feature.a')).toBe(true);
|
||||
expect(isFeatureEnabled(mockFeatureData, 'feature.d')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-enabled features', () => {
|
||||
expect(isFeatureEnabled(mockFeatureData, 'feature.b')).toBe(false);
|
||||
expect(isFeatureEnabled(mockFeatureData, 'feature.c')).toBe(false);
|
||||
expect(isFeatureEnabled(mockFeatureData, 'non-existent')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFeatureFlags()', () => {
|
||||
it('should fetch and return feature flags', async () => {
|
||||
const mockFetcher = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockFeatureData,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const result = await fetchFeatureFlags(mockFetcher, 'http://api.test');
|
||||
|
||||
expect(mockFetcher).toHaveBeenCalledWith('http://api.test/features');
|
||||
expect(result).toEqual(mockFeatureData);
|
||||
});
|
||||
|
||||
it('should throw error if fetch fails', async () => {
|
||||
const mockFetcher = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
await expect(fetchFeatureFlags(mockFetcher, 'http://api.test')).rejects.toThrow('Failed to fetch feature flags: 500');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,827 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { LeagueDetailViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
import type { LeagueMembershipsDTO } from '../../../apps/website/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { RaceDTO } from '../../../apps/website/lib/types/generated/RaceDTO';
|
||||
import type { GetDriverOutputDTO } from '../../../apps/website/lib/types/generated/GetDriverOutputDTO';
|
||||
import type { LeagueScoringConfigDTO } from '../../../apps/website/lib/types/generated/LeagueScoringConfigDTO';
|
||||
|
||||
describe('LeagueDetailViewDataBuilder', () => {
|
||||
const mockLeague: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-123',
|
||||
name: 'Test League',
|
||||
description: 'A test league description',
|
||||
ownerId: 'owner-456',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/test',
|
||||
youtubeUrl: 'https://youtube.com/test',
|
||||
websiteUrl: 'https://test.com',
|
||||
},
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
logoUrl: 'https://logo.com/test.png',
|
||||
pendingJoinRequestsCount: 3,
|
||||
pendingProtestsCount: 1,
|
||||
walletBalance: 1000,
|
||||
};
|
||||
|
||||
const mockOwner: GetDriverOutputDTO = {
|
||||
id: 'owner-456',
|
||||
iracingId: '12345',
|
||||
name: 'John Doe',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
avatarUrl: 'https://avatar.com/john.png',
|
||||
rating: 850,
|
||||
};
|
||||
|
||||
const mockScoringConfig: LeagueScoringConfigDTO = {
|
||||
leagueId: 'league-123',
|
||||
seasonId: 'season-1',
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
championships: [],
|
||||
};
|
||||
|
||||
const mockMemberships: LeagueMembershipsDTO = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'owner-456',
|
||||
driver: {
|
||||
id: 'owner-456',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
role: 'owner',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
driverId: 'admin-789',
|
||||
driver: {
|
||||
id: 'admin-789',
|
||||
name: 'Jane Smith',
|
||||
iracingId: '67890',
|
||||
country: 'UK',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
{
|
||||
driverId: 'steward-101',
|
||||
driver: {
|
||||
id: 'steward-101',
|
||||
name: 'Bob Wilson',
|
||||
iracingId: '11111',
|
||||
country: 'CA',
|
||||
joinedAt: '2024-01-03T00:00:00Z',
|
||||
},
|
||||
role: 'steward',
|
||||
joinedAt: '2024-01-03T00:00:00Z',
|
||||
},
|
||||
{
|
||||
driverId: 'member-202',
|
||||
driver: {
|
||||
id: 'member-202',
|
||||
name: 'Alice Brown',
|
||||
iracingId: '22222',
|
||||
country: 'AU',
|
||||
joinedAt: '2024-01-04T00:00:00Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2024-01-04T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockSponsors = [
|
||||
{
|
||||
id: 'sponsor-1',
|
||||
name: 'Test Sponsor',
|
||||
tier: 'main' as const,
|
||||
logoUrl: 'https://sponsor.com/logo.png',
|
||||
websiteUrl: 'https://sponsor.com',
|
||||
tagline: 'Best sponsor ever',
|
||||
},
|
||||
];
|
||||
|
||||
describe('build()', () => {
|
||||
it('should transform all input data correctly', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Race 2',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe('league-123');
|
||||
expect(result.name).toBe('Test League');
|
||||
expect(result.description).toBe('A test league description');
|
||||
expect(result.logoUrl).toBe('https://logo.com/test.png');
|
||||
expect(result.walletBalance).toBe(1000);
|
||||
expect(result.pendingProtestsCount).toBe(1);
|
||||
expect(result.pendingJoinRequestsCount).toBe(3);
|
||||
|
||||
// Check info data
|
||||
expect(result.info.name).toBe('Test League');
|
||||
expect(result.info.description).toBe('A test league description');
|
||||
expect(result.info.membersCount).toBe(4);
|
||||
expect(result.info.racesCount).toBe(2);
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
expect(result.info.structure).toBe('Solo • 32 max');
|
||||
expect(result.info.scoring).toBe('preset-1');
|
||||
expect(result.info.createdAt).toBe('2024-01-01T00:00:00Z');
|
||||
expect(result.info.discordUrl).toBe('https://discord.gg/test');
|
||||
expect(result.info.youtubeUrl).toBe('https://youtube.com/test');
|
||||
expect(result.info.websiteUrl).toBe('https://test.com');
|
||||
|
||||
// Check owner summary
|
||||
expect(result.ownerSummary).not.toBeNull();
|
||||
expect(result.ownerSummary?.driverId).toBe('owner-456');
|
||||
expect(result.ownerSummary?.driverName).toBe('John Doe');
|
||||
expect(result.ownerSummary?.avatarUrl).toBe('https://avatar.com/john.png');
|
||||
expect(result.ownerSummary?.roleBadgeText).toBe('Owner');
|
||||
expect(result.ownerSummary?.profileUrl).toBe('/drivers/owner-456');
|
||||
|
||||
// Check admin summaries
|
||||
expect(result.adminSummaries).toHaveLength(1);
|
||||
expect(result.adminSummaries[0].driverId).toBe('admin-789');
|
||||
expect(result.adminSummaries[0].roleBadgeText).toBe('Admin');
|
||||
|
||||
// Check steward summaries
|
||||
expect(result.stewardSummaries).toHaveLength(1);
|
||||
expect(result.stewardSummaries[0].driverId).toBe('steward-101');
|
||||
expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward');
|
||||
|
||||
// Check member summaries
|
||||
expect(result.memberSummaries).toHaveLength(1);
|
||||
expect(result.memberSummaries[0].driverId).toBe('member-202');
|
||||
expect(result.memberSummaries[0].roleBadgeText).toBe('Member');
|
||||
|
||||
// Check sponsors
|
||||
expect(result.sponsors).toHaveLength(1);
|
||||
expect(result.sponsors[0].id).toBe('sponsor-1');
|
||||
expect(result.sponsors[0].name).toBe('Test Sponsor');
|
||||
expect(result.sponsors[0].tier).toBe('main');
|
||||
|
||||
// Check running races (empty in this case)
|
||||
expect(result.runningRaces).toEqual([]);
|
||||
});
|
||||
|
||||
it('should calculate next race correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
|
||||
const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-past',
|
||||
name: 'Past Race',
|
||||
date: pastDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-future-1',
|
||||
name: 'Future Race 1',
|
||||
date: futureDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-future-2',
|
||||
name: 'Future Race 2',
|
||||
date: new Date(now.getTime() + 172800000).toISOString(), // 2 days from now
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.nextRace).toBeDefined();
|
||||
expect(result.nextRace?.id).toBe('race-future-1');
|
||||
expect(result.nextRace?.name).toBe('Future Race 1');
|
||||
expect(result.nextRace?.date).toBe(futureDate);
|
||||
});
|
||||
|
||||
it('should handle no upcoming races', () => {
|
||||
const pastDate = new Date(Date.now() - 86400000).toISOString();
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-past-1',
|
||||
name: 'Past Race 1',
|
||||
date: pastDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-past-2',
|
||||
name: 'Past Race 2',
|
||||
date: new Date(Date.now() - 172800000).toISOString(),
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.nextRace).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should calculate season progress correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 86400000).toISOString();
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString();
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-past-1',
|
||||
name: 'Past Race 1',
|
||||
date: pastDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-past-2',
|
||||
name: 'Past Race 2',
|
||||
date: new Date(now.getTime() - 172800000).toISOString(),
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-future-1',
|
||||
name: 'Future Race 1',
|
||||
date: futureDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-future-2',
|
||||
name: 'Future Race 2',
|
||||
date: new Date(now.getTime() + 172800000).toISOString(),
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.seasonProgress).toBeDefined();
|
||||
expect(result.seasonProgress?.completedRaces).toBe(2);
|
||||
expect(result.seasonProgress?.totalRaces).toBe(4);
|
||||
expect(result.seasonProgress?.percentage).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle no races for season progress', () => {
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races: [],
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.seasonProgress).toBeDefined();
|
||||
expect(result.seasonProgress?.completedRaces).toBe(0);
|
||||
expect(result.seasonProgress?.totalRaces).toBe(0);
|
||||
expect(result.seasonProgress?.percentage).toBe(0);
|
||||
});
|
||||
|
||||
it('should extract recent results from last completed race', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 86400000).toISOString();
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString();
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-past-1',
|
||||
name: 'Past Race 1',
|
||||
date: pastDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-past-2',
|
||||
name: 'Past Race 2',
|
||||
date: new Date(now.getTime() - 172800000).toISOString(),
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-future-1',
|
||||
name: 'Future Race 1',
|
||||
date: futureDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.recentResults).toBeDefined();
|
||||
expect(result.recentResults?.length).toBe(2);
|
||||
expect(result.recentResults?.[0].raceId).toBe('race-past-1');
|
||||
expect(result.recentResults?.[0].raceName).toBe('Past Race 1');
|
||||
expect(result.recentResults?.[1].raceId).toBe('race-past-2');
|
||||
});
|
||||
|
||||
it('should handle no completed races for recent results', () => {
|
||||
const futureDate = new Date(Date.now() + 86400000).toISOString();
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-future-1',
|
||||
name: 'Future Race 1',
|
||||
date: futureDate,
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.recentResults).toBeDefined();
|
||||
expect(result.recentResults?.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle null owner', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: null,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.ownerSummary).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle null scoring config', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: null,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.scoring).toBe('Standard');
|
||||
});
|
||||
|
||||
it('should handle empty memberships', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: { members: [] },
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.membersCount).toBe(0);
|
||||
expect(result.adminSummaries).toHaveLength(0);
|
||||
expect(result.stewardSummaries).toHaveLength(0);
|
||||
expect(result.memberSummaries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should calculate avgSOF from races with strengthOfField', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Race 2',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
// Add strengthOfField to races
|
||||
(races[0] as any).strengthOfField = 1500;
|
||||
(races[1] as any).strengthOfField = 1800;
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.avgSOF).toBe(1650);
|
||||
});
|
||||
|
||||
it('should ignore races with zero or null strengthOfField', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Race 2',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
// Add strengthOfField to races
|
||||
(races[0] as any).strengthOfField = 0;
|
||||
(races[1] as any).strengthOfField = null;
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty races array', () => {
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races: [],
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.racesCount).toBe(0);
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
expect(result.nextRace).toBeUndefined();
|
||||
expect(result.seasonProgress?.completedRaces).toBe(0);
|
||||
expect(result.seasonProgress?.totalRaces).toBe(0);
|
||||
expect(result.recentResults?.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty sponsors array', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.sponsors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle missing social links', () => {
|
||||
const leagueWithoutSocialLinks: LeagueWithCapacityAndScoringDTO = {
|
||||
...mockLeague,
|
||||
socialLinks: undefined,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: leagueWithoutSocialLinks,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info.discordUrl).toBeUndefined();
|
||||
expect(result.info.youtubeUrl).toBeUndefined();
|
||||
expect(result.info.websiteUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing category', () => {
|
||||
const leagueWithoutCategory: LeagueWithCapacityAndScoringDTO = {
|
||||
...mockLeague,
|
||||
category: undefined,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: leagueWithoutCategory,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.info).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle missing description', () => {
|
||||
const leagueWithoutDescription: LeagueWithCapacityAndScoringDTO = {
|
||||
...mockLeague,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: leagueWithoutDescription,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.description).toBe('');
|
||||
expect(result.info.description).toBe('');
|
||||
});
|
||||
|
||||
it('should handle missing logoUrl', () => {
|
||||
const leagueWithoutLogo: LeagueWithCapacityAndScoringDTO = {
|
||||
...mockLeague,
|
||||
logoUrl: undefined,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: leagueWithoutLogo,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.logoUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing admin fields', () => {
|
||||
const leagueWithoutAdminFields: LeagueWithCapacityAndScoringDTO = {
|
||||
...mockLeague,
|
||||
pendingJoinRequestsCount: undefined,
|
||||
pendingProtestsCount: undefined,
|
||||
walletBalance: undefined,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: leagueWithoutAdminFields,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.walletBalance).toBeUndefined();
|
||||
expect(result.pendingProtestsCount).toBeUndefined();
|
||||
expect(result.pendingJoinRequestsCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract running races correctly', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Running Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Past Race',
|
||||
date: '2024-01-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-3',
|
||||
name: 'Running Race 2',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.runningRaces).toHaveLength(2);
|
||||
expect(result.runningRaces[0].id).toBe('race-1');
|
||||
expect(result.runningRaces[0].name).toBe('Running Race 1');
|
||||
expect(result.runningRaces[0].date).toBe('2024-02-01T18:00:00Z');
|
||||
expect(result.runningRaces[1].id).toBe('race-3');
|
||||
expect(result.runningRaces[1].name).toBe('Running Race 2');
|
||||
});
|
||||
|
||||
it('should handle no running races', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Past Race 1',
|
||||
date: '2024-01-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Past Race 2',
|
||||
date: '2024-01-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.runningRaces).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle races with "Running" in different positions', () => {
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race Running',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Running',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
{
|
||||
id: 'race-3',
|
||||
name: 'Completed Race',
|
||||
date: '2024-02-15T18:00:00Z',
|
||||
leagueName: 'Test League',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league: mockLeague,
|
||||
owner: mockOwner,
|
||||
scoringConfig: mockScoringConfig,
|
||||
memberships: mockMemberships,
|
||||
races,
|
||||
sponsors: mockSponsors,
|
||||
});
|
||||
|
||||
expect(result.runningRaces).toHaveLength(2);
|
||||
expect(result.runningRaces[0].id).toBe('race-1');
|
||||
expect(result.runningRaces[1].id).toBe('race-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,386 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueScheduleViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder';
|
||||
import type { LeagueScheduleApiDto } from '../../../apps/website/lib/types/tbd/LeagueScheduleApiDto';
|
||||
|
||||
describe('LeagueScheduleViewDataBuilder', () => {
|
||||
const mockApiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Qualifying',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Race 2',
|
||||
date: '2024-02-08T18:00:00Z',
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
{
|
||||
id: 'race-3',
|
||||
name: 'Race 3',
|
||||
date: '2024-02-15T18:00:00Z',
|
||||
track: 'Track C',
|
||||
car: 'Car C',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('build()', () => {
|
||||
it('should transform all races correctly', () => {
|
||||
const result = LeagueScheduleViewDataBuilder.build(mockApiDto);
|
||||
|
||||
expect(result.leagueId).toBe('league-123');
|
||||
expect(result.races).toHaveLength(3);
|
||||
|
||||
// Check first race
|
||||
expect(result.races[0].id).toBe('race-1');
|
||||
expect(result.races[0].name).toBe('Race 1');
|
||||
expect(result.races[0].scheduledAt).toBe('2024-02-01T18:00:00Z');
|
||||
expect(result.races[0].track).toBe('Track A');
|
||||
expect(result.races[0].car).toBe('Car A');
|
||||
expect(result.races[0].sessionType).toBe('Qualifying');
|
||||
});
|
||||
|
||||
it('should mark past races correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-past',
|
||||
name: 'Past Race',
|
||||
date: pastDate,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
{
|
||||
id: 'race-future',
|
||||
name: 'Future Race',
|
||||
date: futureDate,
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].isPast).toBe(true);
|
||||
expect(result.races[0].isUpcoming).toBe(false);
|
||||
expect(result.races[0].status).toBe('completed');
|
||||
|
||||
expect(result.races[1].isPast).toBe(false);
|
||||
expect(result.races[1].isUpcoming).toBe(true);
|
||||
expect(result.races[1].status).toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should mark upcoming races correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-future',
|
||||
name: 'Future Race',
|
||||
date: futureDate,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].isPast).toBe(false);
|
||||
expect(result.races[0].isUpcoming).toBe(true);
|
||||
expect(result.races[0].status).toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should handle empty schedule', () => {
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagueId).toBe('league-123');
|
||||
expect(result.races).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle races with missing optional fields', () => {
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
track: undefined,
|
||||
car: undefined,
|
||||
sessionType: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].track).toBeUndefined();
|
||||
expect(result.races[0].car).toBeUndefined();
|
||||
expect(result.races[0].sessionType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle current driver ID parameter', () => {
|
||||
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456');
|
||||
|
||||
expect(result.currentDriverId).toBe('driver-456');
|
||||
});
|
||||
|
||||
it('should handle admin permission parameter', () => {
|
||||
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', true);
|
||||
|
||||
expect(result.isAdmin).toBe(true);
|
||||
expect(result.races[0].canEdit).toBe(true);
|
||||
expect(result.races[0].canReschedule).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle non-admin permission parameter', () => {
|
||||
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', false);
|
||||
|
||||
expect(result.isAdmin).toBe(false);
|
||||
expect(result.races[0].canEdit).toBe(false);
|
||||
expect(result.races[0].canReschedule).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle default admin parameter as false', () => {
|
||||
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456');
|
||||
|
||||
expect(result.isAdmin).toBe(false);
|
||||
expect(result.races[0].canEdit).toBe(false);
|
||||
expect(result.races[0].canReschedule).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle registration status for upcoming races', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 86400000).toISOString();
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-future',
|
||||
name: 'Future Race',
|
||||
date: futureDate,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].isUserRegistered).toBe(false);
|
||||
expect(result.races[0].canRegister).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle registration status for past races', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 86400000).toISOString();
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-past',
|
||||
name: 'Past Race',
|
||||
date: pastDate,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].isUserRegistered).toBe(false);
|
||||
expect(result.races[0].canRegister).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle races exactly at current time', () => {
|
||||
const now = new Date();
|
||||
const exactDate = now.toISOString();
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-exact',
|
||||
name: 'Exact Race',
|
||||
date: exactDate,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
// Race at exact current time is considered upcoming (not past)
|
||||
// because the comparison uses < (strictly less than)
|
||||
expect(result.races[0].isPast).toBe(false);
|
||||
expect(result.races[0].isUpcoming).toBe(true);
|
||||
expect(result.races[0].status).toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should handle races with different session types', () => {
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-qualifying',
|
||||
name: 'Qualifying',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Qualifying',
|
||||
},
|
||||
{
|
||||
id: 'race-practice',
|
||||
name: 'Practice',
|
||||
date: '2024-02-02T18:00:00Z',
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
sessionType: 'Practice',
|
||||
},
|
||||
{
|
||||
id: 'race-race',
|
||||
name: 'Race',
|
||||
date: '2024-02-03T18:00:00Z',
|
||||
track: 'Track C',
|
||||
car: 'Car C',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].sessionType).toBe('Qualifying');
|
||||
expect(result.races[1].sessionType).toBe('Practice');
|
||||
expect(result.races[2].sessionType).toBe('Race');
|
||||
});
|
||||
|
||||
it('should handle races without session type', () => {
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].sessionType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle races with empty track and car', () => {
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-02-01T18:00:00Z',
|
||||
track: '',
|
||||
car: '',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].track).toBe('');
|
||||
expect(result.races[0].car).toBe('');
|
||||
});
|
||||
|
||||
it('should handle multiple races with mixed dates', () => {
|
||||
const now = new Date();
|
||||
const pastDate1 = new Date(now.getTime() - 172800000).toISOString(); // 2 days ago
|
||||
const pastDate2 = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
|
||||
const futureDate1 = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
|
||||
const futureDate2 = new Date(now.getTime() + 172800000).toISOString(); // 2 days from now
|
||||
|
||||
const apiDto: LeagueScheduleApiDto = {
|
||||
leagueId: 'league-123',
|
||||
races: [
|
||||
{
|
||||
id: 'race-past-2',
|
||||
name: 'Past Race 2',
|
||||
date: pastDate1,
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
{
|
||||
id: 'race-past-1',
|
||||
name: 'Past Race 1',
|
||||
date: pastDate2,
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
{
|
||||
id: 'race-future-1',
|
||||
name: 'Future Race 1',
|
||||
date: futureDate1,
|
||||
track: 'Track C',
|
||||
car: 'Car C',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
{
|
||||
id: 'race-future-2',
|
||||
name: 'Future Race 2',
|
||||
date: futureDate2,
|
||||
track: 'Track D',
|
||||
car: 'Car D',
|
||||
sessionType: 'Race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races).toHaveLength(4);
|
||||
expect(result.races[0].isPast).toBe(true);
|
||||
expect(result.races[1].isPast).toBe(true);
|
||||
expect(result.races[2].isPast).toBe(false);
|
||||
expect(result.races[3].isPast).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,541 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueStandingsViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder';
|
||||
import type { LeagueStandingDTO } from '../../../apps/website/lib/types/generated/LeagueStandingDTO';
|
||||
import type { LeagueMemberDTO } from '../../../apps/website/lib/types/generated/LeagueMemberDTO';
|
||||
|
||||
describe('LeagueStandingsViewDataBuilder', () => {
|
||||
const mockStandings: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: ['race-1', 'race-2'],
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
iracingId: '67890',
|
||||
country: 'UK',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
points: 120,
|
||||
position: 2,
|
||||
wins: 2,
|
||||
podiums: 4,
|
||||
races: 10,
|
||||
positionChange: 1,
|
||||
lastRacePoints: 18,
|
||||
droppedRaceIds: ['race-3'],
|
||||
},
|
||||
{
|
||||
driverId: 'driver-3',
|
||||
driver: {
|
||||
id: 'driver-3',
|
||||
name: 'Bob Wilson',
|
||||
iracingId: '11111',
|
||||
country: 'CA',
|
||||
joinedAt: '2024-01-03T00:00:00Z',
|
||||
},
|
||||
points: 90,
|
||||
position: 3,
|
||||
wins: 1,
|
||||
podiums: 3,
|
||||
races: 10,
|
||||
positionChange: -1,
|
||||
lastRacePoints: 12,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockMemberships: LeagueMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
iracingId: '67890',
|
||||
country: 'UK',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
describe('build()', () => {
|
||||
it('should transform standings correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.leagueId).toBe('league-123');
|
||||
expect(result.standings).toHaveLength(3);
|
||||
|
||||
// Check first standing
|
||||
expect(result.standings[0].driverId).toBe('driver-1');
|
||||
expect(result.standings[0].position).toBe(1);
|
||||
expect(result.standings[0].totalPoints).toBe(150);
|
||||
expect(result.standings[0].racesFinished).toBe(10);
|
||||
expect(result.standings[0].racesStarted).toBe(10);
|
||||
expect(result.standings[0].positionChange).toBe(0);
|
||||
expect(result.standings[0].lastRacePoints).toBe(25);
|
||||
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
|
||||
expect(result.standings[0].wins).toBe(3);
|
||||
expect(result.standings[0].podiums).toBe(5);
|
||||
});
|
||||
|
||||
it('should calculate position change correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].positionChange).toBe(0); // No change
|
||||
expect(result.standings[1].positionChange).toBe(1); // Moved up
|
||||
expect(result.standings[2].positionChange).toBe(-1); // Moved down
|
||||
});
|
||||
|
||||
it('should map last race points correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].lastRacePoints).toBe(25);
|
||||
expect(result.standings[1].lastRacePoints).toBe(18);
|
||||
expect(result.standings[2].lastRacePoints).toBe(12);
|
||||
});
|
||||
|
||||
it('should handle dropped race IDs correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
|
||||
expect(result.standings[1].droppedRaceIds).toEqual(['race-3']);
|
||||
expect(result.standings[2].droppedRaceIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should calculate championship stats (wins, podiums)', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].wins).toBe(3);
|
||||
expect(result.standings[0].podiums).toBe(5);
|
||||
expect(result.standings[1].wins).toBe(2);
|
||||
expect(result.standings[1].podiums).toBe(4);
|
||||
expect(result.standings[2].wins).toBe(1);
|
||||
expect(result.standings[2].podiums).toBe(3);
|
||||
});
|
||||
|
||||
it('should extract driver metadata correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.drivers).toHaveLength(3);
|
||||
|
||||
// Check first driver
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('John Doe');
|
||||
expect(result.drivers[0].iracingId).toBe('12345');
|
||||
expect(result.drivers[0].country).toBe('US');
|
||||
});
|
||||
|
||||
it('should convert memberships correctly', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.memberships).toHaveLength(2);
|
||||
|
||||
// Check first membership
|
||||
expect(result.memberships[0].driverId).toBe('driver-1');
|
||||
expect(result.memberships[0].leagueId).toBe('league-123');
|
||||
expect(result.memberships[0].role).toBe('member');
|
||||
expect(result.memberships[0].status).toBe('active');
|
||||
});
|
||||
|
||||
it('should handle empty standings', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: [] },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings).toHaveLength(0);
|
||||
expect(result.drivers).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle empty memberships', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: [] },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle missing driver objects in standings', () => {
|
||||
const standingsWithMissingDriver: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
iracingId: '67890',
|
||||
country: 'UK',
|
||||
joinedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
points: 120,
|
||||
position: 2,
|
||||
wins: 2,
|
||||
podiums: 4,
|
||||
races: 10,
|
||||
positionChange: 1,
|
||||
lastRacePoints: 18,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithMissingDriver },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[1].id).toBe('driver-2');
|
||||
});
|
||||
|
||||
it('should handle standings with missing positionChange', () => {
|
||||
const standingsWithoutPositionChange: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: undefined as any,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithoutPositionChange },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].positionChange).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle standings with missing lastRacePoints', () => {
|
||||
const standingsWithoutLastRacePoints: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: undefined as any,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithoutLastRacePoints },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].lastRacePoints).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle standings with missing droppedRaceIds', () => {
|
||||
const standingsWithoutDroppedRaceIds: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: undefined as any,
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithoutDroppedRaceIds },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].droppedRaceIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle standings with missing wins', () => {
|
||||
const standingsWithoutWins: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: undefined as any,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithoutWins },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].wins).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle standings with missing podiums', () => {
|
||||
const standingsWithoutPodiums: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: undefined as any,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithoutPodiums },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].podiums).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle team championship mode', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123',
|
||||
true
|
||||
);
|
||||
|
||||
expect(result.isTeamChampionship).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle non-team championship mode by default', () => {
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: mockStandings },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.isTeamChampionship).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle standings with zero points', () => {
|
||||
const standingsWithZeroPoints: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 0,
|
||||
position: 1,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
races: 10,
|
||||
positionChange: 0,
|
||||
lastRacePoints: 0,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithZeroPoints },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].totalPoints).toBe(0);
|
||||
expect(result.standings[0].wins).toBe(0);
|
||||
expect(result.standings[0].podiums).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle standings with negative position change', () => {
|
||||
const standingsWithNegativeChange: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: -2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithNegativeChange },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].positionChange).toBe(-2);
|
||||
});
|
||||
|
||||
it('should handle standings with positive position change', () => {
|
||||
const standingsWithPositiveChange: LeagueStandingDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'US',
|
||||
joinedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
points: 150,
|
||||
position: 1,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
races: 10,
|
||||
positionChange: 3,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
{ standings: standingsWithPositiveChange },
|
||||
{ members: mockMemberships },
|
||||
'league-123'
|
||||
);
|
||||
|
||||
expect(result.standings[0].positionChange).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,932 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeaguesViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeaguesViewDataBuilder';
|
||||
import type { AllLeaguesWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
|
||||
describe('LeaguesViewDataBuilder', () => {
|
||||
const mockLeagues: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League 1',
|
||||
description: 'A test league description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/test1',
|
||||
youtubeUrl: 'https://youtube.com/test1',
|
||||
websiteUrl: 'https://test1.com',
|
||||
},
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game 1',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
logoUrl: 'https://logo.com/test1.png',
|
||||
pendingJoinRequestsCount: 3,
|
||||
pendingProtestsCount: 1,
|
||||
walletBalance: 1000,
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Test League 2',
|
||||
description: 'Another test league',
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 16,
|
||||
qualifyingFormat: 'Team',
|
||||
},
|
||||
usedSlots: 8,
|
||||
category: 'Oval',
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/test2',
|
||||
},
|
||||
scoring: {
|
||||
gameId: 'game-2',
|
||||
gameName: 'Test Game 2',
|
||||
primaryChampionshipType: 'Team',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Advanced',
|
||||
dropPolicySummary: 'Drop 1 worst race',
|
||||
scoringPatternSummary: 'Points based on finish position with bonuses',
|
||||
},
|
||||
timingSummary: 'Every Saturday at 7 PM',
|
||||
logoUrl: 'https://logo.com/test2.png',
|
||||
},
|
||||
{
|
||||
id: 'league-3',
|
||||
name: 'Test League 3',
|
||||
description: 'A third test league',
|
||||
ownerId: 'owner-3',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 24,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 24,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-3',
|
||||
gameName: 'Test Game 3',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-3',
|
||||
scoringPresetName: 'Custom',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Fixed points per position',
|
||||
},
|
||||
timingSummary: 'Every Friday at 9 PM',
|
||||
},
|
||||
];
|
||||
|
||||
describe('build()', () => {
|
||||
it('should transform all leagues correctly', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues).toHaveLength(3);
|
||||
|
||||
// Check first league
|
||||
expect(result.leagues[0].id).toBe('league-1');
|
||||
expect(result.leagues[0].name).toBe('Test League 1');
|
||||
expect(result.leagues[0].description).toBe('A test league description');
|
||||
expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png');
|
||||
expect(result.leagues[0].ownerId).toBe('owner-1');
|
||||
expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z');
|
||||
expect(result.leagues[0].maxDrivers).toBe(32);
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(15);
|
||||
expect(result.leagues[0].structureSummary).toBe('Solo');
|
||||
expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM');
|
||||
expect(result.leagues[0].category).toBe('Road');
|
||||
|
||||
// Check scoring
|
||||
expect(result.leagues[0].scoring).toBeDefined();
|
||||
expect(result.leagues[0].scoring?.gameId).toBe('game-1');
|
||||
expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1');
|
||||
expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo');
|
||||
expect(result.leagues[0].scoring?.scoringPresetId).toBe('preset-1');
|
||||
expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard');
|
||||
expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races');
|
||||
expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position');
|
||||
});
|
||||
|
||||
it('should handle leagues with missing description', () => {
|
||||
const leaguesWithoutDescription: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: '',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutDescription,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].description).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle leagues with missing logoUrl', () => {
|
||||
const leaguesWithoutLogo: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutLogo,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].logoUrl).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle leagues with missing category', () => {
|
||||
const leaguesWithoutCategory: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutCategory,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].category).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle leagues with missing scoring', () => {
|
||||
const leaguesWithoutScoring: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutScoring,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle leagues with missing social links', () => {
|
||||
const leaguesWithoutSocialLinks: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutSocialLinks,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle leagues with missing timingSummary', () => {
|
||||
const leaguesWithoutTimingSummary: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutTimingSummary,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].timingSummary).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty leagues array', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle leagues with different categories', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].category).toBe('Road');
|
||||
expect(result.leagues[1].category).toBe('Oval');
|
||||
expect(result.leagues[2].category).toBe('Road');
|
||||
});
|
||||
|
||||
it('should handle leagues with different structures', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].structureSummary).toBe('Solo');
|
||||
expect(result.leagues[1].structureSummary).toBe('Team');
|
||||
expect(result.leagues[2].structureSummary).toBe('Solo');
|
||||
});
|
||||
|
||||
it('should handle leagues with different scoring presets', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard');
|
||||
expect(result.leagues[1].scoring?.scoringPresetName).toBe('Advanced');
|
||||
expect(result.leagues[2].scoring?.scoringPresetName).toBe('Custom');
|
||||
});
|
||||
|
||||
it('should handle leagues with different drop policies', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races');
|
||||
expect(result.leagues[1].scoring?.dropPolicySummary).toBe('Drop 1 worst race');
|
||||
expect(result.leagues[2].scoring?.dropPolicySummary).toBe('No drops');
|
||||
});
|
||||
|
||||
it('should handle leagues with different scoring patterns', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position');
|
||||
expect(result.leagues[1].scoring?.scoringPatternSummary).toBe('Points based on finish position with bonuses');
|
||||
expect(result.leagues[2].scoring?.scoringPatternSummary).toBe('Fixed points per position');
|
||||
});
|
||||
|
||||
it('should handle leagues with different primary championship types', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo');
|
||||
expect(result.leagues[1].scoring?.primaryChampionshipType).toBe('Team');
|
||||
expect(result.leagues[2].scoring?.primaryChampionshipType).toBe('Solo');
|
||||
});
|
||||
|
||||
it('should handle leagues with different game names', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1');
|
||||
expect(result.leagues[1].scoring?.gameName).toBe('Test Game 2');
|
||||
expect(result.leagues[2].scoring?.gameName).toBe('Test Game 3');
|
||||
});
|
||||
|
||||
it('should handle leagues with different game IDs', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].scoring?.gameId).toBe('game-1');
|
||||
expect(result.leagues[1].scoring?.gameId).toBe('game-2');
|
||||
expect(result.leagues[2].scoring?.gameId).toBe('game-3');
|
||||
});
|
||||
|
||||
it('should handle leagues with different max drivers', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].maxDrivers).toBe(32);
|
||||
expect(result.leagues[1].maxDrivers).toBe(16);
|
||||
expect(result.leagues[2].maxDrivers).toBe(24);
|
||||
});
|
||||
|
||||
it('should handle leagues with different used slots', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(15);
|
||||
expect(result.leagues[1].usedDriverSlots).toBe(8);
|
||||
expect(result.leagues[2].usedDriverSlots).toBe(24);
|
||||
});
|
||||
|
||||
it('should handle leagues with different owners', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].ownerId).toBe('owner-1');
|
||||
expect(result.leagues[1].ownerId).toBe('owner-2');
|
||||
expect(result.leagues[2].ownerId).toBe('owner-3');
|
||||
});
|
||||
|
||||
it('should handle leagues with different creation dates', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z');
|
||||
expect(result.leagues[1].createdAt).toBe('2024-01-02T00:00:00Z');
|
||||
expect(result.leagues[2].createdAt).toBe('2024-01-03T00:00:00Z');
|
||||
});
|
||||
|
||||
it('should handle leagues with different timing summaries', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM');
|
||||
expect(result.leagues[1].timingSummary).toBe('Every Saturday at 7 PM');
|
||||
expect(result.leagues[2].timingSummary).toBe('Every Friday at 9 PM');
|
||||
});
|
||||
|
||||
it('should handle leagues with different names', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].name).toBe('Test League 1');
|
||||
expect(result.leagues[1].name).toBe('Test League 2');
|
||||
expect(result.leagues[2].name).toBe('Test League 3');
|
||||
});
|
||||
|
||||
it('should handle leagues with different descriptions', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].description).toBe('A test league description');
|
||||
expect(result.leagues[1].description).toBe('Another test league');
|
||||
expect(result.leagues[2].description).toBe('A third test league');
|
||||
});
|
||||
|
||||
it('should handle leagues with different logo URLs', () => {
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: mockLeagues,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png');
|
||||
expect(result.leagues[1].logoUrl).toBe('https://logo.com/test2.png');
|
||||
expect(result.leagues[2].logoUrl).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle leagues with activeDriversCount', () => {
|
||||
const leaguesWithActiveDrivers: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
// Add activeDriversCount to the league
|
||||
(leaguesWithActiveDrivers[0] as any).activeDriversCount = 12;
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithActiveDrivers,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].activeDriversCount).toBe(12);
|
||||
});
|
||||
|
||||
it('should handle leagues with nextRaceAt', () => {
|
||||
const leaguesWithNextRace: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
// Add nextRaceAt to the league
|
||||
(leaguesWithNextRace[0] as any).nextRaceAt = '2024-02-01T18:00:00Z';
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithNextRace,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].nextRaceAt).toBe('2024-02-01T18:00:00Z');
|
||||
});
|
||||
|
||||
it('should handle leagues without activeDriversCount and nextRaceAt', () => {
|
||||
const leaguesWithoutMetadata: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithoutMetadata,
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagues[0].activeDriversCount).toBeUndefined();
|
||||
expect(result.leagues[0].nextRaceAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle leagues with different usedDriverSlots for featured leagues', () => {
|
||||
const leaguesWithDifferentSlots: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Small League',
|
||||
description: 'A small league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 16,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 8,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Large League',
|
||||
description: 'A large league',
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 25,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-2',
|
||||
gameName: 'Test Game 2',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Advanced',
|
||||
dropPolicySummary: 'Drop 1 worst race',
|
||||
scoringPatternSummary: 'Points based on finish position with bonuses',
|
||||
},
|
||||
timingSummary: 'Every Saturday at 7 PM',
|
||||
},
|
||||
{
|
||||
id: 'league-3',
|
||||
name: 'Medium League',
|
||||
description: 'A medium league',
|
||||
ownerId: 'owner-3',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 24,
|
||||
qualifyingFormat: 'Team',
|
||||
},
|
||||
usedSlots: 20,
|
||||
category: 'Oval',
|
||||
scoring: {
|
||||
gameId: 'game-3',
|
||||
gameName: 'Test Game 3',
|
||||
primaryChampionshipType: 'Team',
|
||||
scoringPresetId: 'preset-3',
|
||||
scoringPresetName: 'Custom',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Fixed points per position',
|
||||
},
|
||||
timingSummary: 'Every Friday at 9 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithDifferentSlots,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
// Verify that usedDriverSlots is correctly mapped
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(8);
|
||||
expect(result.leagues[1].usedDriverSlots).toBe(25);
|
||||
expect(result.leagues[2].usedDriverSlots).toBe(20);
|
||||
|
||||
// Verify that leagues can be filtered for featured leagues (usedDriverSlots > 20)
|
||||
const featuredLeagues = result.leagues.filter(l => (l.usedDriverSlots ?? 0) > 20);
|
||||
expect(featuredLeagues).toHaveLength(1);
|
||||
expect(featuredLeagues[0].id).toBe('league-2');
|
||||
});
|
||||
|
||||
it('should handle leagues with different categories for filtering', () => {
|
||||
const leaguesWithDifferentCategories: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Road League 1',
|
||||
description: 'A road league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Oval League 1',
|
||||
description: 'An oval league',
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 16,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 8,
|
||||
category: 'Oval',
|
||||
scoring: {
|
||||
gameId: 'game-2',
|
||||
gameName: 'Test Game 2',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Advanced',
|
||||
dropPolicySummary: 'Drop 1 worst race',
|
||||
scoringPatternSummary: 'Points based on finish position with bonuses',
|
||||
},
|
||||
timingSummary: 'Every Saturday at 7 PM',
|
||||
},
|
||||
{
|
||||
id: 'league-3',
|
||||
name: 'Road League 2',
|
||||
description: 'Another road league',
|
||||
ownerId: 'owner-3',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 24,
|
||||
qualifyingFormat: 'Team',
|
||||
},
|
||||
usedSlots: 20,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-3',
|
||||
gameName: 'Test Game 3',
|
||||
primaryChampionshipType: 'Team',
|
||||
scoringPresetId: 'preset-3',
|
||||
scoringPresetName: 'Custom',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Fixed points per position',
|
||||
},
|
||||
timingSummary: 'Every Friday at 9 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithDifferentCategories,
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
// Verify that category is correctly mapped
|
||||
expect(result.leagues[0].category).toBe('Road');
|
||||
expect(result.leagues[1].category).toBe('Oval');
|
||||
expect(result.leagues[2].category).toBe('Road');
|
||||
|
||||
// Verify that leagues can be filtered by category
|
||||
const roadLeagues = result.leagues.filter(l => l.category === 'Road');
|
||||
expect(roadLeagues).toHaveLength(2);
|
||||
expect(roadLeagues[0].id).toBe('league-1');
|
||||
expect(roadLeagues[1].id).toBe('league-3');
|
||||
|
||||
const ovalLeagues = result.leagues.filter(l => l.category === 'Oval');
|
||||
expect(ovalLeagues).toHaveLength(1);
|
||||
expect(ovalLeagues[0].id).toBe('league-2');
|
||||
});
|
||||
|
||||
it('should handle leagues with null category for filtering', () => {
|
||||
const leaguesWithNullCategory: LeagueWithCapacityAndScoringDTO[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'League with Category',
|
||||
description: 'A league with category',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 15,
|
||||
category: 'Road',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Every Sunday at 8 PM',
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'League without Category',
|
||||
description: 'A league without category',
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
settings: {
|
||||
maxDrivers: 16,
|
||||
qualifyingFormat: 'Solo',
|
||||
},
|
||||
usedSlots: 8,
|
||||
scoring: {
|
||||
gameId: 'game-2',
|
||||
gameName: 'Test Game 2',
|
||||
primaryChampionshipType: 'Solo',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Advanced',
|
||||
dropPolicySummary: 'Drop 1 worst race',
|
||||
scoringPatternSummary: 'Points based on finish position with bonuses',
|
||||
},
|
||||
timingSummary: 'Every Saturday at 7 PM',
|
||||
},
|
||||
];
|
||||
|
||||
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: leaguesWithNullCategory,
|
||||
totalCount: 2,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(apiDto);
|
||||
|
||||
// Verify that null category is handled correctly
|
||||
expect(result.leagues[0].category).toBe('Road');
|
||||
expect(result.leagues[1].category).toBe(null);
|
||||
|
||||
// Verify that leagues can be filtered by category (null category should be filterable)
|
||||
const roadLeagues = result.leagues.filter(l => l.category === 'Road');
|
||||
expect(roadLeagues).toHaveLength(1);
|
||||
expect(roadLeagues[0].id).toBe('league-1');
|
||||
|
||||
const noCategoryLeagues = result.leagues.filter(l => l.category === null);
|
||||
expect(noCategoryLeagues).toHaveLength(1);
|
||||
expect(noCategoryLeagues[0].id).toBe('league-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { routeMatchers } from '../../../apps/website/lib/routing/RouteConfig';
|
||||
|
||||
describe('RouteConfig - routeMatchers Invariants', () => {
|
||||
describe('isPublic()', () => {
|
||||
it('should return true for exact public matches', () => {
|
||||
expect(routeMatchers.isPublic('/')).toBe(true);
|
||||
expect(routeMatchers.isPublic('/leagues')).toBe(true);
|
||||
expect(routeMatchers.isPublic('/auth/login')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for top-level detail pages (league, race, driver, team)', () => {
|
||||
expect(routeMatchers.isPublic('/leagues/123')).toBe(true);
|
||||
expect(routeMatchers.isPublic('/races/456')).toBe(true);
|
||||
expect(routeMatchers.isPublic('/drivers/789')).toBe(true);
|
||||
expect(routeMatchers.isPublic('/teams/abc')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for "leagues/create" and "teams/create" (protected)', () => {
|
||||
expect(routeMatchers.isPublic('/leagues/create')).toBe(false);
|
||||
expect(routeMatchers.isPublic('/teams/create')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for nested protected routes', () => {
|
||||
expect(routeMatchers.isPublic('/dashboard')).toBe(false);
|
||||
expect(routeMatchers.isPublic('/profile/settings')).toBe(false);
|
||||
expect(routeMatchers.isPublic('/admin/users')).toBe(false);
|
||||
expect(routeMatchers.isPublic('/sponsor/dashboard')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for sponsor signup (public)', () => {
|
||||
expect(routeMatchers.isPublic('/sponsor/signup')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unknown routes', () => {
|
||||
expect(routeMatchers.isPublic('/unknown-route')).toBe(false);
|
||||
expect(routeMatchers.isPublic('/api/something')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requiresRole()', () => {
|
||||
it('should return admin roles for admin routes', () => {
|
||||
const roles = routeMatchers.requiresRole('/admin');
|
||||
expect(roles).toContain('admin');
|
||||
expect(roles).toContain('super-admin');
|
||||
|
||||
const userRoles = routeMatchers.requiresRole('/admin/users');
|
||||
expect(userRoles).toEqual(roles);
|
||||
});
|
||||
|
||||
it('should return sponsor role for sponsor routes', () => {
|
||||
expect(routeMatchers.requiresRole('/sponsor/dashboard')).toEqual(['sponsor']);
|
||||
expect(routeMatchers.requiresRole('/sponsor/billing')).toEqual(['sponsor']);
|
||||
});
|
||||
|
||||
it('should return null for public routes', () => {
|
||||
expect(routeMatchers.requiresRole('/')).toBeNull();
|
||||
expect(routeMatchers.requiresRole('/leagues')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-role protected routes', () => {
|
||||
expect(routeMatchers.requiresRole('/dashboard')).toBeNull();
|
||||
expect(routeMatchers.requiresRole('/profile')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for sponsor signup (public)', () => {
|
||||
expect(routeMatchers.requiresRole('/sponsor/signup')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
import { routes } from '../../../apps/website/lib/routing/RouteConfig';
|
||||
|
||||
describe('WebsiteRouteManager - Route Classification Contract', () => {
|
||||
const routeManager = new WebsiteRouteManager();
|
||||
|
||||
describe('getAccessLevel()', () => {
|
||||
it('should correctly classify public routes', () => {
|
||||
expect(routeManager.getAccessLevel('/')).toBe('public');
|
||||
expect(routeManager.getAccessLevel('/auth/login')).toBe('public');
|
||||
expect(routeManager.getAccessLevel('/leagues')).toBe('public');
|
||||
});
|
||||
|
||||
it('should correctly classify dashboard routes as auth', () => {
|
||||
expect(routeManager.getAccessLevel('/dashboard')).toBe('auth');
|
||||
expect(routeManager.getAccessLevel('/profile')).toBe('auth');
|
||||
});
|
||||
|
||||
it('should correctly classify admin routes', () => {
|
||||
expect(routeManager.getAccessLevel('/admin')).toBe('admin');
|
||||
expect(routeManager.getAccessLevel('/admin/users')).toBe('admin');
|
||||
});
|
||||
|
||||
it('should correctly classify sponsor routes', () => {
|
||||
expect(routeManager.getAccessLevel('/sponsor')).toBe('sponsor');
|
||||
expect(routeManager.getAccessLevel('/sponsor/dashboard')).toBe('sponsor');
|
||||
});
|
||||
|
||||
it('should correctly classify dynamic route patterns', () => {
|
||||
// League detail is public
|
||||
expect(routeManager.getAccessLevel('/leagues/any-id')).toBe('public');
|
||||
expect(routeManager.getAccessLevel('/races/any-id')).toBe('public');
|
||||
|
||||
// Nested protected routes
|
||||
expect(routeManager.getAccessLevel('/leagues/any-id/settings')).toBe('auth');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RouteConfig Contract', () => {
|
||||
it('should fail loudly if RouteConfig paths change unexpectedly', () => {
|
||||
// These assertions act as a contract. If the paths change in RouteConfig,
|
||||
// these tests will fail, forcing a conscious update of the contract.
|
||||
expect(routes.public.home).toBe('/');
|
||||
expect(routes.auth.login).toBe('/auth/login');
|
||||
expect(routes.protected.dashboard).toBe('/dashboard');
|
||||
expect(routes.admin.root).toBe('/admin');
|
||||
expect(routes.sponsor.root).toBe('/sponsor');
|
||||
|
||||
// Dynamic patterns
|
||||
expect(routes.league.detail('test-id')).toBe('/leagues/test-id');
|
||||
expect(routes.league.scheduleAdmin('test-id')).toBe('/leagues/test-id/schedule/admin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Representative Subset Verification', () => {
|
||||
const testCases = [
|
||||
{ path: '/', expected: 'public' },
|
||||
{ path: '/auth/login', expected: 'public' },
|
||||
{ path: '/dashboard', expected: 'auth' },
|
||||
{ path: '/admin', expected: 'admin' },
|
||||
{ path: '/sponsor', expected: 'sponsor' },
|
||||
{ path: '/leagues/123', expected: 'public' },
|
||||
{ path: '/races/456', expected: 'public' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ path, expected }) => {
|
||||
it(`should classify ${path} as ${expected}`, () => {
|
||||
expect(routeManager.getAccessLevel(path)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { getWebsiteApiBaseUrl } from '../../../apps/website/lib/config/apiBaseUrl';
|
||||
|
||||
describe('getWebsiteApiBaseUrl()', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
// Clear relevant env vars
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
delete process.env.API_BASE_URL;
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.CI;
|
||||
delete process.env.DOCKER;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('Browser Context', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('window', {});
|
||||
});
|
||||
|
||||
it('should use NEXT_PUBLIC_API_BASE_URL if provided', () => {
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = 'https://api.example.com/';
|
||||
expect(getWebsiteApiBaseUrl()).toBe('https://api.example.com');
|
||||
});
|
||||
|
||||
it('should throw if missing env in test-like environment (CI)', () => {
|
||||
process.env.CI = 'true';
|
||||
expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing NEXT_PUBLIC_API_BASE_URL/);
|
||||
});
|
||||
|
||||
it('should throw if missing env in test-like environment (DOCKER)', () => {
|
||||
process.env.DOCKER = 'true';
|
||||
expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing NEXT_PUBLIC_API_BASE_URL/);
|
||||
});
|
||||
|
||||
it('should fallback to localhost in development (non-docker)', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
expect(getWebsiteApiBaseUrl()).toBe('http://localhost:3001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server Context', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('window', undefined);
|
||||
});
|
||||
|
||||
it('should prioritize API_BASE_URL over NEXT_PUBLIC_API_BASE_URL', () => {
|
||||
process.env.API_BASE_URL = 'https://internal-api.example.com';
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = 'https://public-api.example.com';
|
||||
expect(getWebsiteApiBaseUrl()).toBe('https://internal-api.example.com');
|
||||
});
|
||||
|
||||
it('should use NEXT_PUBLIC_API_BASE_URL if API_BASE_URL is missing', () => {
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = 'https://public-api.example.com';
|
||||
expect(getWebsiteApiBaseUrl()).toBe('https://public-api.example.com');
|
||||
});
|
||||
|
||||
it('should throw if missing env in test-like environment (CI)', () => {
|
||||
process.env.CI = 'true';
|
||||
expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing API_BASE_URL/);
|
||||
});
|
||||
|
||||
it('should fallback to api:3000 in production (non-test environment)', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
expect(getWebsiteApiBaseUrl()).toBe('http://api:3000');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user