/** * API Contract Validation Tests * * Validates that API DTOs are consistent and generate valid OpenAPI specs. * This test suite ensures contract compatibility between API and website. */ import { describe, it, expect, beforeAll } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; // Import DTO classes to validate their structure import { GetAnalyticsMetricsOutputDTO } from '../../domain/analytics/dtos/GetAnalyticsMetricsOutputDTO'; import { GetDashboardDataOutputDTO } from '../../domain/analytics/dtos/GetDashboardDataOutputDTO'; import { RecordEngagementInputDTO } from '../../domain/analytics/dtos/RecordEngagementInputDTO'; import { RecordEngagementOutputDTO } from '../../domain/analytics/dtos/RecordEngagementOutputDTO'; import { RecordPageViewInputDTO } from '../../domain/analytics/dtos/RecordPageViewInputDTO'; import { RecordPageViewOutputDTO } from '../../domain/analytics/dtos/RecordPageViewOutputDTO'; import { RequestAvatarGenerationInputDTO } from '../../domain/media/dtos/RequestAvatarGenerationInputDTO'; import { RequestAvatarGenerationOutputDTO } from '../../domain/media/dtos/RequestAvatarGenerationOutputDTO'; import { UploadMediaInputDTO } from '../../domain/media/dtos/UploadMediaInputDTO'; import { UploadMediaOutputDTO } from '../../domain/media/dtos/UploadMediaOutputDTO'; import { ValidateFaceInputDTO } from '../../domain/media/dtos/ValidateFaceInputDTO'; import { ValidateFaceOutputDTO } from '../../domain/media/dtos/ValidateFaceOutputDTO'; import { RaceDTO } from '../../domain/race/dtos/RaceDTO'; import { RaceDetailDTO } from '../../domain/race/dtos/RaceDetailDTO'; import { RaceResultDTO } from '../../domain/race/dtos/RaceResultDTO'; import { SponsorDTO } from '../../domain/sponsor/dtos/SponsorDTO'; import { SponsorshipDTO } from '../../domain/sponsor/dtos/SponsorshipDTO'; import { TeamDTO } from '../../domain/team/dtos/TeamDto'; const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m', dim: '\x1b[2m' }; describe('API Contract Validation', () => { let openApiSpec: any; let specPath: string; beforeAll(async () => { // Load the OpenAPI spec specPath = path.join(__dirname, '..', '..', '..', 'openapi.json'); const specContent = await fs.readFile(specPath, 'utf-8'); openApiSpec = JSON.parse(specContent); }); describe('OpenAPI Spec Integrity', () => { it('should have valid OpenAPI structure', () => { expect(openApiSpec).toBeDefined(); expect(openApiSpec.openapi).toBeDefined(); expect(openApiSpec.info).toBeDefined(); expect(openApiSpec.paths).toBeDefined(); expect(openApiSpec.components).toBeDefined(); expect(openApiSpec.components.schemas).toBeDefined(); }); it('should have valid OpenAPI version', () => { expect(openApiSpec.openapi).toMatch(/^3\.\d+\.\d+$/); }); it('should have required API metadata', () => { expect(openApiSpec.info.title).toBeDefined(); expect(openApiSpec.info.version).toBeDefined(); expect(openApiSpec.info.description).toBeDefined(); }); it('should have no circular references in schemas', () => { const schemas = openApiSpec.components.schemas as Record; const visited = new Set(); const visiting = new Set(); const checkCircular = (schemaName: string, schema: any): boolean => { if (!schema) return false; if (visiting.has(schemaName)) { return true; // Circular reference detected } if (visited.has(schemaName)) { return false; } visiting.add(schemaName); // Check $ref references if (schema.$ref) { const refName = schema.$ref.split('/').pop(); if (schemas[refName] && checkCircular(refName, schemas[refName])) { return true; } } // Check properties if (schema.properties) { for (const prop of Object.values(schema.properties)) { if ((prop as any).$ref) { const refName = (prop as any).$ref.split('/').pop(); if (schemas[refName] && checkCircular(refName, schemas[refName])) { return true; } } } } // Check array items if (schema.items && schema.items.$ref) { const refName = schema.items.$ref.split('/').pop(); if (schemas[refName] && checkCircular(refName, schemas[refName])) { return true; } } visiting.delete(schemaName); visited.add(schemaName); return false; }; for (const [schemaName, schema] of Object.entries(schemas)) { if (checkCircular(schemaName, schema as any)) { throw new Error(`Circular reference detected in schema: ${schemaName}`); } } }); it('should have all required DTOs in OpenAPI spec', () => { const schemas = openApiSpec.components.schemas as Record; // List of critical DTOs that must exist in the spec const requiredDTOs = [ 'GetAnalyticsMetricsOutputDTO', 'GetDashboardDataOutputDTO', 'RecordEngagementInputDTO', 'RecordEngagementOutputDTO', 'RecordPageViewInputDTO', 'RecordPageViewOutputDTO', 'RequestAvatarGenerationInputDTO', 'RequestAvatarGenerationOutputDTO', 'UploadMediaInputDTO', 'UploadMediaOutputDTO', 'ValidateFaceInputDTO', 'ValidateFaceOutputDTO', 'RaceDTO', 'RaceDetailDTO', 'RaceResultDTO', 'SponsorDTO', 'SponsorshipDTO', 'TeamDTO' ]; for (const dtoName of requiredDTOs) { expect(schemas[dtoName], `DTO ${dtoName} should exist in OpenAPI spec`).toBeDefined(); } }); it('should have valid JSON schema for all DTOs', () => { const schemas = openApiSpec.components.schemas as Record; for (const [schemaName, schema] of Object.entries(schemas)) { expect(schema, `Schema ${schemaName} should be an object`).toBeInstanceOf(Object); expect(schema.type, `Schema ${schemaName} should have a type`).toBeDefined(); if (schema.type === 'object') { expect(schema.properties, `Schema ${schemaName} should have properties`).toBeDefined(); } } }); }); describe('DTO Consistency', () => { it('should have consistent DTO definitions between code and spec', () => { const schemas = openApiSpec.components.schemas as Record; // Test a sample of DTOs to ensure they match the spec const testDTOs = [ { name: 'GetAnalyticsMetricsOutputDTO', expectedProps: ['pageViews', 'uniqueVisitors', 'averageSessionDuration', 'bounceRate'] }, { name: 'RaceDTO', expectedProps: ['id', 'name', 'date'] }, { name: 'SponsorDTO', expectedProps: ['id', 'name'] } ]; for (const { name, expectedProps } of testDTOs) { const schema = schemas[name]; expect(schema, `Schema ${name} should exist`).toBeDefined(); if (schema.properties) { for (const prop of expectedProps) { expect(schema.properties[prop], `Property ${prop} should exist in ${name}`).toBeDefined(); } } } }); it('should have no duplicate DTO names', () => { const schemas = openApiSpec.components.schemas as Record; const schemaNames = Object.keys(schemas); const uniqueNames = new Set(schemaNames); expect(schemaNames.length).toBe(uniqueNames.size); }); it('should have consistent naming conventions', () => { const schemas = openApiSpec.components.schemas as Record; for (const schemaName of Object.keys(schemas)) { // DTO names should end with DTO expect(schemaName).toMatch(/DTO$/); } }); }); describe('Type Generation Integrity', () => { it('should have all DTOs with proper type definitions', () => { const schemas = openApiSpec.components.schemas as Record; for (const [schemaName, schema] of Object.entries(schemas)) { if (schema.type === 'object') { expect(schema.properties, `Schema ${schemaName} should have properties`).toBeDefined(); // Check that all properties have types or are references for (const [propName, propSchema] of Object.entries(schema.properties)) { const prop = propSchema as any; // Properties can have a type directly, or be a $ref to another schema const hasType = prop.type !== undefined; const isRef = prop.$ref !== undefined; expect(hasType || isRef, `Property ${propName} in ${schemaName} should have a type or be a $ref`).toBe(true); } } } }); it('should have required fields properly marked', () => { const schemas = openApiSpec.components.schemas as Record; // Test a few critical DTOs const testDTOs = [ { name: 'GetAnalyticsMetricsOutputDTO', required: ['pageViews', 'uniqueVisitors', 'averageSessionDuration', 'bounceRate'] }, { name: 'RaceDTO', required: ['id', 'name', 'date'] } ]; for (const { name, required } of testDTOs) { const schema = schemas[name]; expect(schema.required, `Schema ${name} should have required fields`).toBeDefined(); for (const field of required) { expect(schema.required).toContain(field); } } }); it('should have nullable fields properly marked', () => { const schemas = openApiSpec.components.schemas as Record; // Check that nullable fields are properly marked for (const [schemaName, schema] of Object.entries(schemas)) { if (schema.properties) { for (const [propName, propSchema] of Object.entries(schema.properties)) { if ((propSchema as any).nullable === true) { // Nullable fields should not be in required array if (schema.required) { expect(schema.required).not.toContain(propName); } } } } } }); }); describe('Contract Compatibility', () => { it('should have backward compatible DTOs', () => { const schemas = openApiSpec.components.schemas as Record; // Critical DTOs that must maintain backward compatibility const criticalDTOs = [ 'RaceDTO', 'SponsorDTO', 'TeamDTO', 'DriverDTO' ]; for (const dtoName of criticalDTOs) { const schema = schemas[dtoName]; expect(schema, `Critical DTO ${dtoName} should exist`).toBeDefined(); // These DTOs should have required fields that cannot be removed if (schema.required) { expect(schema.required.length).toBeGreaterThan(0); } } }); it('should have no breaking changes in required fields', () => { const schemas = openApiSpec.components.schemas as Record; // Check that required fields are not empty for critical DTOs const criticalDTOs = ['RaceDTO', 'SponsorDTO', 'TeamDTO']; for (const dtoName of criticalDTOs) { const schema = schemas[dtoName]; if (schema && schema.required) { expect(schema.required.length).toBeGreaterThan(0); } } }); it('should have consistent field types across versions', () => { const schemas = openApiSpec.components.schemas as Record; // Check that common fields have consistent types const commonFields = { id: 'string', name: 'string', createdAt: 'string', updatedAt: 'string' }; for (const [fieldName, expectedType] of Object.entries(commonFields)) { for (const [schemaName, schema] of Object.entries(schemas)) { if (schema.properties && schema.properties[fieldName]) { expect(schema.properties[fieldName].type).toBe(expectedType); } } } }); }); describe('Contract Validation Summary', () => { it('should pass all contract validation checks', () => { const schemas = openApiSpec.components.schemas as Record; const schemaCount = Object.keys(schemas).length; console.log(`${colors.cyan}📊 Contract Validation Summary${colors.reset}`); console.log(`${colors.dim} Total DTOs in OpenAPI spec: ${schemaCount}${colors.reset}`); console.log(`${colors.dim} Spec file: ${specPath}${colors.reset}`); // Verify critical metrics expect(schemaCount).toBeGreaterThan(0); // Count DTOs by category const analyticsDTOs = Object.keys(schemas).filter(name => name.includes('Analytics') || name.includes('Engagement') || name.includes('PageView')); const mediaDTOs = Object.keys(schemas).filter(name => name.includes('Media') || name.includes('Avatar')); const raceDTOs = Object.keys(schemas).filter(name => name.includes('Race')); const sponsorDTOs = Object.keys(schemas).filter(name => name.includes('Sponsor')); const teamDTOs = Object.keys(schemas).filter(name => name.includes('Team')); console.log(`${colors.dim} Analytics DTOs: ${analyticsDTOs.length}${colors.reset}`); console.log(`${colors.dim} Media DTOs: ${mediaDTOs.length}${colors.reset}`); console.log(`${colors.dim} Race DTOs: ${raceDTOs.length}${colors.reset}`); console.log(`${colors.dim} Sponsor DTOs: ${sponsorDTOs.length}${colors.reset}`); console.log(`${colors.dim} Team DTOs: ${teamDTOs.length}${colors.reset}`); // Verify that we have DTOs in each category expect(analyticsDTOs.length).toBeGreaterThan(0); expect(mediaDTOs.length).toBeGreaterThan(0); expect(raceDTOs.length).toBeGreaterThan(0); expect(sponsorDTOs.length).toBeGreaterThan(0); expect(teamDTOs.length).toBeGreaterThan(0); }); }); });