wip league admin tools
This commit is contained in:
@@ -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;');
|
||||
|
||||
Reference in New Issue
Block a user