contract testing

This commit is contained in:
2025-12-24 00:01:01 +01:00
parent 43a8afe7a9
commit 5e491d9724
52 changed files with 2058 additions and 612 deletions

View File

@@ -0,0 +1,258 @@
/**
* 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);
});
});
});