258 lines
9.5 KiB
TypeScript
258 lines
9.5 KiB
TypeScript
/**
|
|
* 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<string, string[]>();
|
|
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);
|
|
});
|
|
});
|
|
}); |