wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -5,9 +5,9 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execSync } from 'child_process';
import { createHash } from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { glob } from 'glob';
describe('Type Generation Script', () => {
const apiRoot = path.join(__dirname, '../../apps/api');
@@ -16,6 +16,11 @@ describe('Type Generation Script', () => {
const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated');
const backupDir = path.join(__dirname, '../../.backup/type-gen-test');
async function sha256OfFile(filePath: string): Promise<string> {
const buffer = await fs.readFile(filePath);
return createHash('sha256').update(buffer).digest('hex');
}
beforeAll(async () => {
// Backup existing generated types
await fs.mkdir(backupDir, { recursive: true });
@@ -50,14 +55,7 @@ describe('Type Generation Script', () => {
});
describe('OpenAPI Spec Generation', () => {
it('should generate valid OpenAPI spec', async () => {
// Run the spec generation
execSync('npm run api:generate-spec', {
cwd: path.join(__dirname, '../..'),
stdio: 'pipe'
});
// Check that spec exists and is valid JSON
it('should have a valid committed OpenAPI spec', async () => {
const specContent = await fs.readFile(openapiPath, 'utf-8');
expect(() => JSON.parse(specContent)).not.toThrow();
@@ -67,6 +65,32 @@ describe('Type Generation Script', () => {
expect(spec.components.schemas).toBeDefined();
});
it('should include league schedule route and schema', async () => {
const specContent = await fs.readFile(openapiPath, 'utf-8');
const spec = JSON.parse(specContent);
// Route should exist (controller route extraction)
expect(spec.paths?.['/leagues/{leagueId}/schedule']).toBeDefined();
// Schema should exist (DTO scanning)
const scheduleSchema = spec.components?.schemas?.['LeagueScheduleDTO'];
expect(scheduleSchema).toBeDefined();
// Contract requirements: season-aware schedule DTO
expect(scheduleSchema.required ?? []).toContain('seasonId');
expect(scheduleSchema.properties?.seasonId).toEqual({ type: 'string' });
// Races must be typed and use RaceDTO items
expect(scheduleSchema.required ?? []).toContain('races');
expect(scheduleSchema.properties?.races?.type).toBe('array');
expect(scheduleSchema.properties?.races?.items).toEqual({ $ref: '#/components/schemas/RaceDTO' });
// RaceDTO.date must be ISO-safe string (OpenAPI generator maps Date->date-time, but DTO uses string)
const raceSchema = spec.components?.schemas?.['RaceDTO'];
expect(raceSchema).toBeDefined();
expect(raceSchema.properties?.date).toEqual({ type: 'string' });
});
it('should not have duplicate schema names with different casing', async () => {
const specContent = await fs.readFile(openapiPath, 'utf-8');
const spec = JSON.parse(specContent);
@@ -100,6 +124,26 @@ describe('Type Generation Script', () => {
});
describe('Type Generation', () => {
it('should stamp generated output with the committed OpenAPI SHA256', async () => {
execSync('npm run api:generate-types', {
cwd: path.join(__dirname, '../..'),
stdio: 'pipe',
});
const expectedHash = await sha256OfFile(openapiPath);
const barrelPath = path.join(generatedTypesDir, 'index.ts');
const barrelContent = await fs.readFile(barrelPath, 'utf-8');
expect(barrelContent).toContain(`Spec SHA256: ${expectedHash}`);
expect(barrelContent).toContain(`export type { RaceDTO } from './RaceDTO';`);
expect(barrelContent).toContain(`export type { DriverDTO } from './DriverDTO';`);
const sampleDtoPath = path.join(generatedTypesDir, 'RaceDTO.ts');
const sampleDtoContent = await fs.readFile(sampleDtoPath, 'utf-8');
expect(sampleDtoContent).toContain(`Spec SHA256: ${expectedHash}`);
});
it('should generate TypeScript files for all schemas', async () => {
// Generate types
execSync('npm run api:generate-types', {
@@ -121,7 +165,7 @@ describe('Type Generation Script', () => {
// Most schemas should have corresponding generated files
// (allowing for some duplicates/conflicts that are intentionally skipped)
const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema));
// Should have at least 95% coverage
const coverage = (schemas.length - missingFiles.length) / schemas.length;
expect(coverage).toBeGreaterThan(0.95);
@@ -129,15 +173,14 @@ describe('Type Generation Script', () => {
it('should generate files with correct interface names', async () => {
const files = await fs.readdir(generatedTypesDir);
const dtos = files.filter(f => f.endsWith('.ts'));
const dtos = files.filter(f => f.endsWith('.ts') && f !== 'index.ts');
for (const file of dtos) {
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
const interfaceName = file.replace('.ts', '');
// File should contain an interface (name might be normalized)
expect(content).toMatch(/export interface \w+\s*{/);
// Should not have duplicate interface names in the same file
const interfaceMatches = content.match(/export interface (\w+)/g);
expect(interfaceMatches?.length).toBe(1);
@@ -146,17 +189,23 @@ describe('Type Generation Script', () => {
it('should generate valid TypeScript syntax', async () => {
const files = await fs.readdir(generatedTypesDir);
const dtos = files.filter(f => f.endsWith('.ts'));
const tsFiles = files.filter(f => f.endsWith('.ts'));
for (const file of dtos) {
for (const file of tsFiles) {
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
if (file === 'index.ts') {
expect(content).toContain('Auto-generated barrel');
expect(content).toContain('export type { RaceDTO } from');
continue;
}
// Basic syntax checks
expect(content).toContain('export interface');
expect(content).toContain('{');
expect(content).toContain('}');
expect(content).toContain('Auto-generated DTO');
// Should not have syntax errors
expect(content).not.toMatch(/interface\s+\w+\s*\{\s*\}/); // Empty interfaces
expect(content).not.toContain('undefined;');