/** * Test suite for type generation script * Validates that the type generation process works correctly */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { execSync } from 'child_process'; 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'); const websiteRoot = path.join(__dirname, '../../apps/website'); const openapiPath = path.join(apiRoot, 'openapi.json'); const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated'); const backupDir = path.join(__dirname, '../../.backup/type-gen-test'); beforeAll(async () => { // Backup existing generated types await fs.mkdir(backupDir, { recursive: true }); try { const files = await fs.readdir(generatedTypesDir); for (const file of files) { if (file.endsWith('.ts')) { const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); await fs.writeFile(path.join(backupDir, file), content); } } } catch (error) { // No existing files to backup } }); afterAll(async () => { // Restore backup try { const backupFiles = await fs.readdir(backupDir); for (const file of backupFiles) { if (file.endsWith('.ts')) { const content = await fs.readFile(path.join(backupDir, file), 'utf-8'); await fs.writeFile(path.join(generatedTypesDir, file), content); } } } catch (error) { // No backup to restore } // Clean up backup await fs.rm(backupDir, { recursive: true, force: true }); }); 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 const specContent = await fs.readFile(openapiPath, 'utf-8'); expect(() => JSON.parse(specContent)).not.toThrow(); const spec = JSON.parse(specContent); expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/); expect(spec.components).toBeDefined(); expect(spec.components.schemas).toBeDefined(); }); it('should not have duplicate schema names with different casing', async () => { const specContent = await fs.readFile(openapiPath, 'utf-8'); const spec = JSON.parse(specContent); const schemas = Object.keys(spec.components.schemas); // Check for duplicates with different casing const lowerCaseMap = new Map(); schemas.forEach(schema => { const lower = schema.toLowerCase(); if (!lowerCaseMap.has(lower)) { lowerCaseMap.set(lower, []); } lowerCaseMap.get(lower)!.push(schema); }); const duplicates = Array.from(lowerCaseMap.entries()) .filter(([_, names]) => names.length > 1); expect(duplicates.length).toBe(0); }); it('should generate spec with consistent naming', async () => { const specContent = await fs.readFile(openapiPath, 'utf-8'); const spec = JSON.parse(specContent); const schemas = Object.keys(spec.components.schemas); // All schemas should follow DTO naming convention const invalidNames = schemas.filter(name => !name.endsWith('DTO') && !name.endsWith('Dto')); expect(invalidNames.length).toBe(0); }); }); describe('Type Generation', () => { it('should generate TypeScript files for all schemas', async () => { // Generate types execSync('npm run api:generate-types', { cwd: path.join(__dirname, '../..'), stdio: 'pipe' }); // Read generated files const generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Read OpenAPI spec const specContent = await fs.readFile(openapiPath, 'utf-8'); const spec = JSON.parse(specContent); const schemas = Object.keys(spec.components.schemas); // 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); }); it('should generate files with correct interface names', async () => { const files = await fs.readdir(generatedTypesDir); const dtos = files.filter(f => f.endsWith('.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); } }); it('should generate valid TypeScript syntax', async () => { const files = await fs.readdir(generatedTypesDir); const dtos = files.filter(f => f.endsWith('.ts')); for (const file of dtos) { const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); // 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;'); expect(content).not.toContain('any;'); } }); it('should handle dependencies correctly', async () => { const files = await fs.readdir(generatedTypesDir); const dtos = files.filter(f => f.endsWith('.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; // Import type should match the file name expect(importedType).toBe(fromFile); // The imported file should exist const importedPath = path.join(generatedTypesDir, `${fromFile}.ts`); const exists = await fs.access(importedPath).then(() => true).catch(() => false); expect(exists).toBe(true); } } } }); it('should maintain consistent naming between OpenAPI and generated files', async () => { const specContent = await fs.readFile(openapiPath, 'utf-8'); const spec = JSON.parse(specContent); const schemas = Object.keys(spec.components.schemas); const generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Check that most schemas have matching files (allowing for some edge cases) const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema)); const coverage = (schemas.length - missingFiles.length) / schemas.length; expect(coverage).toBeGreaterThan(0.95); // Check that most files have matching schemas (allowing for normalization) const extraFiles = generatedDTOs.filter(dto => !schemas.includes(dto)); const extraCoverage = (generatedDTOs.length - extraFiles.length) / generatedDTOs.length; expect(extraCoverage).toBeGreaterThan(0.95); }); }); describe('Integration', () => { it('should generate types that can be imported without errors', async () => { // Generate types first execSync('npm run api:generate-types', { cwd: path.join(__dirname, '../..'), stdio: 'pipe' }); // Try to import a few key DTOs const testDTOs = [ 'RaceDTO', 'DriverDTO', 'RequestAvatarGenerationInputDTO', 'RequestAvatarGenerationOutputDTO' ]; for (const dto of testDTOs) { const filePath = path.join(generatedTypesDir, `${dto}.ts`); const exists = await fs.access(filePath).then(() => true).catch(() => false); if (exists) { const content = await fs.readFile(filePath, 'utf-8'); // Should be valid TypeScript that can be parsed expect(content).toContain(`export interface ${dto}`); } } }); it('should handle the full generation workflow', async () => { // Run complete workflow execSync('npm run api:sync-types', { cwd: path.join(__dirname, '../..'), stdio: 'pipe' }); // Verify both spec and types were generated const specExists = await fs.access(openapiPath).then(() => true).catch(() => false); expect(specExists).toBe(true); const files = await fs.readdir(generatedTypesDir); const tsFiles = files.filter(f => f.endsWith('.ts')); expect(tsFiles.length).toBeGreaterThan(0); }); }); });