/** * Contract Validation Tests for Dashboard Module * * These tests validate that the dashboard API DTOs and OpenAPI spec are consistent * and that the generated types will be compatible with the website dashboard client. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { describe, expect, it } from 'vitest'; interface OpenAPISchema { type?: string; format?: string; $ref?: string; items?: OpenAPISchema; properties?: Record; required?: string[]; enum?: string[]; nullable?: boolean; description?: string; default?: unknown; } interface OpenAPISpec { openapi: string; info: { title: string; description: string; version: string; }; paths: Record; components: { schemas: Record; }; } describe('Dashboard Module Contract Validation', () => { const apiRoot = path.join(__dirname, '../..'); const openapiPath = path.join(apiRoot, 'apps/api/openapi.json'); const generatedTypesDir = path.join(apiRoot, 'apps/website/lib/types/generated'); const websiteTypesDir = path.join(apiRoot, 'apps/website/lib/types'); describe('OpenAPI Spec Integrity for Dashboard Endpoints', () => { it('should have dashboard endpoints defined in OpenAPI spec', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for dashboard endpoints expect(spec.paths['/dashboard/overview']).toBeDefined(); // Verify GET methods exist expect(spec.paths['/dashboard/overview'].get).toBeDefined(); }); it('should have DashboardOverviewDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardOverviewDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('myUpcomingRaces'); expect(schema.required).toContain('otherUpcomingRaces'); expect(schema.required).toContain('upcomingRaces'); expect(schema.required).toContain('activeLeaguesCount'); expect(schema.required).toContain('recentResults'); expect(schema.required).toContain('leagueStandingsSummaries'); expect(schema.required).toContain('feedSummary'); expect(schema.required).toContain('friends'); // Verify optional fields expect(schema.properties?.currentDriver).toBeDefined(); expect(schema.properties?.nextRace).toBeDefined(); }); it('should have DashboardDriverSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardDriverSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('name'); expect(schema.required).toContain('country'); expect(schema.required).toContain('totalRaces'); expect(schema.required).toContain('wins'); expect(schema.required).toContain('podiums'); // Verify optional fields expect(schema.properties?.avatarUrl).toBeDefined(); expect(schema.properties?.category).toBeDefined(); expect(schema.properties?.rating).toBeDefined(); expect(schema.properties?.globalRank).toBeDefined(); expect(schema.properties?.consistency).toBeDefined(); }); it('should have DashboardRaceSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardRaceSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('track'); expect(schema.required).toContain('car'); expect(schema.required).toContain('scheduledAt'); expect(schema.required).toContain('status'); expect(schema.required).toContain('isMyLeague'); // Verify optional fields expect(schema.properties?.leagueId).toBeDefined(); expect(schema.properties?.leagueName).toBeDefined(); }); it('should have DashboardRecentResultDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardRecentResultDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('raceId'); expect(schema.required).toContain('raceName'); expect(schema.required).toContain('finishedAt'); expect(schema.required).toContain('position'); expect(schema.required).toContain('incidents'); // Verify optional fields expect(schema.properties?.leagueId).toBeDefined(); expect(schema.properties?.leagueName).toBeDefined(); }); it('should have DashboardLeagueStandingSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardLeagueStandingSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('leagueId'); expect(schema.required).toContain('leagueName'); expect(schema.required).toContain('totalDrivers'); // Verify optional fields expect(schema.properties?.position).toBeDefined(); expect(schema.properties?.points).toBeDefined(); }); it('should have DashboardFeedItemSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFeedItemSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('type'); expect(schema.required).toContain('headline'); expect(schema.required).toContain('timestamp'); // Verify optional fields expect(schema.properties?.body).toBeDefined(); expect(schema.properties?.ctaLabel).toBeDefined(); expect(schema.properties?.ctaHref).toBeDefined(); }); it('should have DashboardFeedSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFeedSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('notificationCount'); expect(schema.required).toContain('items'); }); it('should have DashboardFriendSummaryDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFriendSummaryDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('name'); expect(schema.required).toContain('country'); // Verify optional fields expect(schema.properties?.avatarUrl).toBeDefined(); }); it('should have proper request/response structure for dashboard overview endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardPath = spec.paths['/dashboard/overview']?.get; expect(dashboardPath).toBeDefined(); // Verify query parameters expect(dashboardPath.parameters).toBeDefined(); const driverIdParam = dashboardPath.parameters.find((p: any) => p.name === 'driverId'); expect(driverIdParam).toBeDefined(); expect(driverIdParam.in).toBe('query'); // Verify response const response200 = dashboardPath.responses['200']; expect(response200).toBeDefined(); expect(response200.content['application/json'].schema.$ref).toBe('#/components/schemas/DashboardOverviewDTO'); }); }); describe('DTO Consistency', () => { it('should have generated DTO files for dashboard schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Check for dashboard-related DTOs const dashboardDTOs = [ 'DashboardOverviewDTO', 'DashboardDriverSummaryDTO', 'DashboardRaceSummaryDTO', 'DashboardRecentResultDTO', 'DashboardLeagueStandingSummaryDTO', 'DashboardFeedItemSummaryDTO', 'DashboardFeedSummaryDTO', 'DashboardFriendSummaryDTO', ]; for (const dtoName of dashboardDTOs) { expect(spec.components.schemas[dtoName]).toBeDefined(); expect(generatedDTOs).toContain(dtoName); } }); it('should have consistent property types between DTOs and schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schemas = spec.components.schemas; // Test DashboardOverviewDTO const dashboardOverviewSchema = schemas['DashboardOverviewDTO']; const dashboardOverviewDtoPath = path.join(generatedTypesDir, 'DashboardOverviewDTO.ts'); const dashboardOverviewDtoExists = await fs.access(dashboardOverviewDtoPath).then(() => true).catch(() => false); if (dashboardOverviewDtoExists) { const dashboardOverviewDtoContent = await fs.readFile(dashboardOverviewDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardOverviewSchema.required) { for (const requiredProp of dashboardOverviewSchema.required) { expect(dashboardOverviewDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardOverviewSchema.properties) { for (const propName of Object.keys(dashboardOverviewSchema.properties)) { expect(dashboardOverviewDtoContent).toContain(propName); } } } // Test DashboardDriverSummaryDTO const dashboardDriverSummarySchema = schemas['DashboardDriverSummaryDTO']; const dashboardDriverSummaryDtoPath = path.join(generatedTypesDir, 'DashboardDriverSummaryDTO.ts'); const dashboardDriverSummaryDtoExists = await fs.access(dashboardDriverSummaryDtoPath).then(() => true).catch(() => false); if (dashboardDriverSummaryDtoExists) { const dashboardDriverSummaryDtoContent = await fs.readFile(dashboardDriverSummaryDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardDriverSummarySchema.required) { for (const requiredProp of dashboardDriverSummarySchema.required) { expect(dashboardDriverSummaryDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardDriverSummarySchema.properties) { for (const propName of Object.keys(dashboardDriverSummarySchema.properties)) { expect(dashboardDriverSummaryDtoContent).toContain(propName); } } } // Test DashboardRaceSummaryDTO const dashboardRaceSummarySchema = schemas['DashboardRaceSummaryDTO']; const dashboardRaceSummaryDtoPath = path.join(generatedTypesDir, 'DashboardRaceSummaryDTO.ts'); const dashboardRaceSummaryDtoExists = await fs.access(dashboardRaceSummaryDtoPath).then(() => true).catch(() => false); if (dashboardRaceSummaryDtoExists) { const dashboardRaceSummaryDtoContent = await fs.readFile(dashboardRaceSummaryDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardRaceSummarySchema.required) { for (const requiredProp of dashboardRaceSummarySchema.required) { expect(dashboardRaceSummaryDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardRaceSummarySchema.properties) { for (const propName of Object.keys(dashboardRaceSummarySchema.properties)) { expect(dashboardRaceSummaryDtoContent).toContain(propName); } } } // Test DashboardRecentResultDTO const dashboardRecentResultSchema = schemas['DashboardRecentResultDTO']; const dashboardRecentResultDtoPath = path.join(generatedTypesDir, 'DashboardRecentResultDTO.ts'); const dashboardRecentResultDtoExists = await fs.access(dashboardRecentResultDtoPath).then(() => true).catch(() => false); if (dashboardRecentResultDtoExists) { const dashboardRecentResultDtoContent = await fs.readFile(dashboardRecentResultDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardRecentResultSchema.required) { for (const requiredProp of dashboardRecentResultSchema.required) { expect(dashboardRecentResultDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardRecentResultSchema.properties) { for (const propName of Object.keys(dashboardRecentResultSchema.properties)) { expect(dashboardRecentResultDtoContent).toContain(propName); } } } // Test DashboardLeagueStandingSummaryDTO const dashboardLeagueStandingSchema = schemas['DashboardLeagueStandingSummaryDTO']; const dashboardLeagueStandingDtoPath = path.join(generatedTypesDir, 'DashboardLeagueStandingSummaryDTO.ts'); const dashboardLeagueStandingDtoExists = await fs.access(dashboardLeagueStandingDtoPath).then(() => true).catch(() => false); if (dashboardLeagueStandingDtoExists) { const dashboardLeagueStandingDtoContent = await fs.readFile(dashboardLeagueStandingDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardLeagueStandingSchema.required) { for (const requiredProp of dashboardLeagueStandingSchema.required) { expect(dashboardLeagueStandingDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardLeagueStandingSchema.properties) { for (const propName of Object.keys(dashboardLeagueStandingSchema.properties)) { expect(dashboardLeagueStandingDtoContent).toContain(propName); } } } // Test DashboardFeedItemSummaryDTO const dashboardFeedItemSchema = schemas['DashboardFeedItemSummaryDTO']; const dashboardFeedItemDtoPath = path.join(generatedTypesDir, 'DashboardFeedItemSummaryDTO.ts'); const dashboardFeedItemDtoExists = await fs.access(dashboardFeedItemDtoPath).then(() => true).catch(() => false); if (dashboardFeedItemDtoExists) { const dashboardFeedItemDtoContent = await fs.readFile(dashboardFeedItemDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardFeedItemSchema.required) { for (const requiredProp of dashboardFeedItemSchema.required) { expect(dashboardFeedItemDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardFeedItemSchema.properties) { for (const propName of Object.keys(dashboardFeedItemSchema.properties)) { expect(dashboardFeedItemDtoContent).toContain(propName); } } } // Test DashboardFeedSummaryDTO const dashboardFeedSummarySchema = schemas['DashboardFeedSummaryDTO']; const dashboardFeedSummaryDtoPath = path.join(generatedTypesDir, 'DashboardFeedSummaryDTO.ts'); const dashboardFeedSummaryDtoExists = await fs.access(dashboardFeedSummaryDtoPath).then(() => true).catch(() => false); if (dashboardFeedSummaryDtoExists) { const dashboardFeedSummaryDtoContent = await fs.readFile(dashboardFeedSummaryDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardFeedSummarySchema.required) { for (const requiredProp of dashboardFeedSummarySchema.required) { expect(dashboardFeedSummaryDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardFeedSummarySchema.properties) { for (const propName of Object.keys(dashboardFeedSummarySchema.properties)) { expect(dashboardFeedSummaryDtoContent).toContain(propName); } } } // Test DashboardFriendSummaryDTO const dashboardFriendSummarySchema = schemas['DashboardFriendSummaryDTO']; const dashboardFriendSummaryDtoPath = path.join(generatedTypesDir, 'DashboardFriendSummaryDTO.ts'); const dashboardFriendSummaryDtoExists = await fs.access(dashboardFriendSummaryDtoPath).then(() => true).catch(() => false); if (dashboardFriendSummaryDtoExists) { const dashboardFriendSummaryDtoContent = await fs.readFile(dashboardFriendSummaryDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardFriendSummarySchema.required) { for (const requiredProp of dashboardFriendSummarySchema.required) { expect(dashboardFriendSummaryDtoContent).toContain(requiredProp); } } // Check that all properties are present if (dashboardFriendSummarySchema.properties) { for (const propName of Object.keys(dashboardFriendSummarySchema.properties)) { expect(dashboardFriendSummaryDtoContent).toContain(propName); } } } }); it('should have dashboard types defined in tbd folder', async () => { // Check if dashboard types exist in tbd folder (similar to admin types) const tbdDir = path.join(websiteTypesDir, 'tbd'); const tbdFiles = await fs.readdir(tbdDir).catch(() => []); // Dashboard types might be in a separate file or combined with existing types // For now, we'll check if the generated types are properly available const generatedFiles = await fs.readdir(generatedTypesDir); const dashboardGenerated = generatedFiles.filter(f => f.includes('Dashboard') || f.includes('Driver') || f.includes('Race') || f.includes('Feed') || f.includes('Friend') || f.includes('League') ); expect(dashboardGenerated.length).toBeGreaterThanOrEqual(8); }); it('should have dashboard types re-exported from main types file', async () => { // Check if there's a dashboard.ts file or if types are exported elsewhere const dashboardTypesPath = path.join(websiteTypesDir, 'dashboard.ts'); const dashboardTypesExists = await fs.access(dashboardTypesPath).then(() => true).catch(() => false); if (dashboardTypesExists) { const dashboardTypesContent = await fs.readFile(dashboardTypesPath, 'utf-8'); // Verify re-exports expect(dashboardTypesContent).toContain('DashboardOverviewDTO'); expect(dashboardTypesContent).toContain('DashboardDriverSummaryDTO'); expect(dashboardTypesContent).toContain('DashboardRaceSummaryDTO'); expect(dashboardTypesContent).toContain('DashboardRecentResultDTO'); expect(dashboardTypesContent).toContain('DashboardLeagueStandingSummaryDTO'); expect(dashboardTypesContent).toContain('DashboardFeedItemSummaryDTO'); expect(dashboardTypesContent).toContain('DashboardFeedSummaryDTO'); expect(dashboardTypesContent).toContain('DashboardFriendSummaryDTO'); } }); }); describe('Dashboard API Client Contract', () => { it('should have DashboardApiClient defined', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientExists = await fs.access(dashboardApiClientPath).then(() => true).catch(() => false); expect(dashboardApiClientExists).toBe(true); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify class definition expect(dashboardApiClientContent).toContain('export class DashboardApiClient'); expect(dashboardApiClientContent).toContain('extends BaseApiClient'); // Verify methods exist expect(dashboardApiClientContent).toContain('getDashboardOverview'); // Verify method signatures expect(dashboardApiClientContent).toContain('getDashboardOverview()'); }); it('should have proper request construction in getDashboardOverview method', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify GET request expect(dashboardApiClientContent).toContain("return this.get('/dashboard/overview')"); }); }); describe('Request Correctness Tests', () => { it('should validate DashboardOverviewDTO required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardOverviewDTO']; // Verify all required fields are present expect(schema.required).toContain('myUpcomingRaces'); expect(schema.required).toContain('otherUpcomingRaces'); expect(schema.required).toContain('upcomingRaces'); expect(schema.required).toContain('activeLeaguesCount'); expect(schema.required).toContain('recentResults'); expect(schema.required).toContain('leagueStandingsSummaries'); expect(schema.required).toContain('feedSummary'); expect(schema.required).toContain('friends'); // Verify no extra required fields expect(schema.required.length).toBe(8); }); it('should validate DashboardOverviewDTO optional fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardOverviewDTO']; // Verify optional fields are not required expect(schema.required).not.toContain('currentDriver'); expect(schema.required).not.toContain('nextRace'); // Verify optional fields exist expect(schema.properties?.currentDriver).toBeDefined(); expect(schema.properties?.nextRace).toBeDefined(); }); it('should validate DashboardDriverSummaryDTO required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardDriverSummaryDTO']; // Verify all required fields are present expect(schema.required).toContain('id'); expect(schema.required).toContain('name'); expect(schema.required).toContain('country'); expect(schema.required).toContain('totalRaces'); expect(schema.required).toContain('wins'); expect(schema.required).toContain('podiums'); // Verify no extra required fields expect(schema.required.length).toBe(6); }); it('should validate DashboardDriverSummaryDTO optional fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardDriverSummaryDTO']; // Verify optional fields are not required expect(schema.required).not.toContain('avatarUrl'); expect(schema.required).not.toContain('category'); expect(schema.required).not.toContain('rating'); expect(schema.required).not.toContain('globalRank'); expect(schema.required).not.toContain('consistency'); // Verify optional fields exist expect(schema.properties?.avatarUrl).toBeDefined(); expect(schema.properties?.category).toBeDefined(); expect(schema.properties?.rating).toBeDefined(); expect(schema.properties?.globalRank).toBeDefined(); expect(schema.properties?.consistency).toBeDefined(); }); it('should validate DashboardRaceSummaryDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardRaceSummaryDTO']; // Verify all required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('track'); expect(schema.required).toContain('car'); expect(schema.required).toContain('scheduledAt'); expect(schema.required).toContain('status'); expect(schema.required).toContain('isMyLeague'); // Verify field types expect(schema.properties?.id?.type).toBe('string'); expect(schema.properties?.track?.type).toBe('string'); expect(schema.properties?.car?.type).toBe('string'); expect(schema.properties?.scheduledAt?.type).toBe('string'); expect(schema.properties?.status?.type).toBe('string'); expect(schema.properties?.isMyLeague?.type).toBe('boolean'); }); it('should validate DashboardRecentResultDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardRecentResultDTO']; // Verify all required fields expect(schema.required).toContain('raceId'); expect(schema.required).toContain('raceName'); expect(schema.required).toContain('finishedAt'); expect(schema.required).toContain('position'); expect(schema.required).toContain('incidents'); // Verify field types expect(schema.properties?.raceId?.type).toBe('string'); expect(schema.properties?.raceName?.type).toBe('string'); expect(schema.properties?.finishedAt?.type).toBe('string'); expect(schema.properties?.position?.type).toBe('number'); expect(schema.properties?.incidents?.type).toBe('number'); }); it('should validate DashboardLeagueStandingSummaryDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardLeagueStandingSummaryDTO']; // Verify all required fields expect(schema.required).toContain('leagueId'); expect(schema.required).toContain('leagueName'); expect(schema.required).toContain('totalDrivers'); // Verify field types expect(schema.properties?.leagueId?.type).toBe('string'); expect(schema.properties?.leagueName?.type).toBe('string'); expect(schema.properties?.totalDrivers?.type).toBe('number'); }); it('should validate DashboardFeedItemSummaryDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFeedItemSummaryDTO']; // Verify all required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('type'); expect(schema.required).toContain('headline'); expect(schema.required).toContain('timestamp'); // Verify field types expect(schema.properties?.id?.type).toBe('string'); expect(schema.properties?.type?.type).toBe('string'); expect(schema.properties?.headline?.type).toBe('string'); expect(schema.properties?.timestamp?.type).toBe('string'); }); it('should validate DashboardFeedSummaryDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFeedSummaryDTO']; // Verify all required fields expect(schema.required).toContain('notificationCount'); expect(schema.required).toContain('items'); // Verify field types expect(schema.properties?.notificationCount?.type).toBe('number'); expect(schema.properties?.items?.type).toBe('array'); }); it('should validate DashboardFriendSummaryDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardFriendSummaryDTO']; // Verify all required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('name'); expect(schema.required).toContain('country'); // Verify field types expect(schema.properties?.id?.type).toBe('string'); expect(schema.properties?.name?.type).toBe('string'); expect(schema.properties?.country?.type).toBe('string'); }); }); describe('Response Handling Tests', () => { it('should handle successful dashboard overview response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardOverviewSchema = spec.components.schemas['DashboardOverviewDTO']; // Verify response structure expect(dashboardOverviewSchema.properties?.currentDriver).toBeDefined(); expect(dashboardOverviewSchema.properties?.myUpcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.otherUpcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.upcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.activeLeaguesCount).toBeDefined(); expect(dashboardOverviewSchema.properties?.nextRace).toBeDefined(); expect(dashboardOverviewSchema.properties?.recentResults).toBeDefined(); expect(dashboardOverviewSchema.properties?.leagueStandingsSummaries).toBeDefined(); expect(dashboardOverviewSchema.properties?.feedSummary).toBeDefined(); expect(dashboardOverviewSchema.properties?.friends).toBeDefined(); }); it('should handle response with all required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardOverviewSchema = spec.components.schemas['DashboardOverviewDTO']; // Verify all required fields are present for (const field of ['myUpcomingRaces', 'otherUpcomingRaces', 'upcomingRaces', 'activeLeaguesCount', 'recentResults', 'leagueStandingsSummaries', 'feedSummary', 'friends']) { expect(dashboardOverviewSchema.required).toContain(field); expect(dashboardOverviewSchema.properties?.[field]).toBeDefined(); } }); it('should handle optional fields in driver response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const driverSchema = spec.components.schemas['DashboardDriverSummaryDTO']; // Verify optional fields are nullable expect(driverSchema.properties?.avatarUrl?.nullable).toBe(true); expect(driverSchema.properties?.category?.nullable).toBe(true); expect(driverSchema.properties?.rating?.nullable).toBe(true); expect(driverSchema.properties?.globalRank?.nullable).toBe(true); expect(driverSchema.properties?.consistency?.nullable).toBe(true); // Verify optional fields are not in required array expect(driverSchema.required).not.toContain('avatarUrl'); expect(driverSchema.required).not.toContain('category'); expect(driverSchema.required).not.toContain('rating'); expect(driverSchema.required).not.toContain('globalRank'); expect(driverSchema.required).not.toContain('consistency'); }); it('should handle optional fields in race response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const raceSchema = spec.components.schemas['DashboardRaceSummaryDTO']; // Verify optional fields are nullable expect(raceSchema.properties?.leagueId?.nullable).toBe(true); expect(raceSchema.properties?.leagueName?.nullable).toBe(true); // Verify optional fields are not in required array expect(raceSchema.required).not.toContain('leagueId'); expect(raceSchema.required).not.toContain('leagueName'); }); it('should handle optional fields in recent result response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const recentResultSchema = spec.components.schemas['DashboardRecentResultDTO']; // Verify optional fields are nullable expect(recentResultSchema.properties?.leagueId?.nullable).toBe(true); expect(recentResultSchema.properties?.leagueName?.nullable).toBe(true); // Verify optional fields are not in required array expect(recentResultSchema.required).not.toContain('leagueId'); expect(recentResultSchema.required).not.toContain('leagueName'); }); it('should handle optional fields in league standing response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const leagueStandingSchema = spec.components.schemas['DashboardLeagueStandingSummaryDTO']; // Verify optional fields are nullable expect(leagueStandingSchema.properties?.position?.nullable).toBe(true); expect(leagueStandingSchema.properties?.points?.nullable).toBe(true); // Verify optional fields are not in required array expect(leagueStandingSchema.required).not.toContain('position'); expect(leagueStandingSchema.required).not.toContain('points'); }); it('should handle optional fields in feed item response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const feedItemSchema = spec.components.schemas['DashboardFeedItemSummaryDTO']; // Verify optional fields are nullable expect(feedItemSchema.properties?.body?.nullable).toBe(true); expect(feedItemSchema.properties?.ctaLabel?.nullable).toBe(true); expect(feedItemSchema.properties?.ctaHref?.nullable).toBe(true); // Verify optional fields are not in required array expect(feedItemSchema.required).not.toContain('body'); expect(feedItemSchema.required).not.toContain('ctaLabel'); expect(feedItemSchema.required).not.toContain('ctaHref'); }); it('should handle optional fields in friend response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const friendSchema = spec.components.schemas['DashboardFriendSummaryDTO']; // Verify optional fields are nullable expect(friendSchema.properties?.avatarUrl?.nullable).toBe(true); // Verify optional fields are not in required array expect(friendSchema.required).not.toContain('avatarUrl'); }); }); describe('Error Handling Tests', () => { it('should document 401 Unauthorized response for dashboard overview endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardPath = spec.paths['/dashboard/overview']?.get; // Check if 401 response is documented if (dashboardPath.responses['401']) { expect(dashboardPath.responses['401']).toBeDefined(); } }); it('should document 500 Internal Server Error response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardPath = spec.paths['/dashboard/overview']?.get; // Check if 500 response is documented for dashboard endpoint if (dashboardPath.responses['500']) { expect(dashboardPath.responses['500']).toBeDefined(); } }); it('should have proper error handling in DashboardApiClient', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify BaseApiClient is extended (which provides error handling) expect(dashboardApiClientContent).toContain('extends BaseApiClient'); // Verify methods use BaseApiClient methods (which handle errors) expect(dashboardApiClientContent).toContain('this.get<'); }); }); describe('Semantic Guarantee Tests', () => { it('should maintain consistency between request and response schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Verify dashboard overview request/response consistency const dashboardOverviewSchema = spec.components.schemas['DashboardOverviewDTO']; // Output should contain all required fields expect(dashboardOverviewSchema.properties?.myUpcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.otherUpcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.upcomingRaces).toBeDefined(); expect(dashboardOverviewSchema.properties?.activeLeaguesCount).toBeDefined(); expect(dashboardOverviewSchema.properties?.recentResults).toBeDefined(); expect(dashboardOverviewSchema.properties?.leagueStandingsSummaries).toBeDefined(); expect(dashboardOverviewSchema.properties?.feedSummary).toBeDefined(); expect(dashboardOverviewSchema.properties?.friends).toBeDefined(); }); it('should validate semantic consistency in dashboard overview', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardOverviewSchema = spec.components.schemas['DashboardOverviewDTO']; // Verify overview has all required fields expect(dashboardOverviewSchema.required).toContain('myUpcomingRaces'); expect(dashboardOverviewSchema.required).toContain('otherUpcomingRaces'); expect(dashboardOverviewSchema.required).toContain('upcomingRaces'); expect(dashboardOverviewSchema.required).toContain('activeLeaguesCount'); expect(dashboardOverviewSchema.required).toContain('recentResults'); expect(dashboardOverviewSchema.required).toContain('leagueStandingsSummaries'); expect(dashboardOverviewSchema.required).toContain('feedSummary'); expect(dashboardOverviewSchema.required).toContain('friends'); }); it('should validate idempotency for dashboard overview', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check if dashboard overview endpoint exists const dashboardPath = spec.paths['/dashboard/overview']?.get; if (dashboardPath) { // Verify it's a GET request (idempotent) expect(dashboardPath).toBeDefined(); // Verify no request body (GET requests are idempotent) expect(dashboardPath.requestBody).toBeUndefined(); } }); it('should validate uniqueness constraints for driver and race IDs', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const driverSchema = spec.components.schemas['DashboardDriverSummaryDTO']; const raceSchema = spec.components.schemas['DashboardRaceSummaryDTO']; // Verify driver ID is a required field expect(driverSchema.required).toContain('id'); expect(driverSchema.properties?.id?.type).toBe('string'); // Verify race ID is a required field expect(raceSchema.required).toContain('id'); expect(raceSchema.properties?.id?.type).toBe('string'); }); it('should validate consistency between request and response types', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Verify all DTOs have consistent type definitions const dtos = [ 'DashboardOverviewDTO', 'DashboardDriverSummaryDTO', 'DashboardRaceSummaryDTO', 'DashboardRecentResultDTO', 'DashboardLeagueStandingSummaryDTO', 'DashboardFeedItemSummaryDTO', 'DashboardFeedSummaryDTO', 'DashboardFriendSummaryDTO', ]; for (const dtoName of dtos) { const schema = spec.components.schemas[dtoName]; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // All should have properties defined expect(schema.properties).toBeDefined(); // All should have required fields (even if empty array) expect(schema.required).toBeDefined(); } }); it('should validate semantic consistency in dashboard data', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardOverviewSchema = spec.components.schemas['DashboardOverviewDTO']; // Verify upcoming races are arrays expect(dashboardOverviewSchema.properties?.myUpcomingRaces?.type).toBe('array'); expect(dashboardOverviewSchema.properties?.otherUpcomingRaces?.type).toBe('array'); expect(dashboardOverviewSchema.properties?.upcomingRaces?.type).toBe('array'); // Verify recent results are arrays expect(dashboardOverviewSchema.properties?.recentResults?.type).toBe('array'); // Verify league standings are arrays expect(dashboardOverviewSchema.properties?.leagueStandingsSummaries?.type).toBe('array'); // Verify friends are arrays expect(dashboardOverviewSchema.properties?.friends?.type).toBe('array'); // Verify activeLeaguesCount is a number expect(dashboardOverviewSchema.properties?.activeLeaguesCount?.type).toBe('number'); }); it('should validate pagination is not applicable for dashboard overview', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Dashboard overview should not have pagination const dashboardPath = spec.paths['/dashboard/overview']; if (dashboardPath) { // Check if there are any query parameters for pagination const methods = Object.keys(dashboardPath); for (const method of methods) { const operation = dashboardPath[method]; if (operation.parameters) { const paramNames = operation.parameters.map((p: any) => p.name); // Dashboard overview should not have page/limit parameters expect(paramNames).not.toContain('page'); expect(paramNames).not.toContain('limit'); } } } }); }); describe('Dashboard Module Integration Tests', () => { it('should have consistent types between API DTOs and website types', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Check all dashboard DTOs exist in generated types const dashboardDTOs = [ 'DashboardOverviewDTO', 'DashboardDriverSummaryDTO', 'DashboardRaceSummaryDTO', 'DashboardRecentResultDTO', 'DashboardLeagueStandingSummaryDTO', 'DashboardFeedItemSummaryDTO', 'DashboardFeedSummaryDTO', 'DashboardFriendSummaryDTO', ]; for (const dtoName of dashboardDTOs) { expect(spec.components.schemas[dtoName]).toBeDefined(); expect(generatedDTOs).toContain(dtoName); } }); it('should have DashboardApiClient methods matching API endpoints', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify getDashboardOverview method exists and uses correct endpoint expect(dashboardApiClientContent).toContain('async getDashboardOverview'); expect(dashboardApiClientContent).toContain("return this.get('/dashboard/overview')"); }); it('should have proper error handling in DashboardApiClient', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify BaseApiClient is extended (which provides error handling) expect(dashboardApiClientContent).toContain('extends BaseApiClient'); // Verify methods use BaseApiClient methods (which handle errors) expect(dashboardApiClientContent).toContain('this.get<'); }); it('should have consistent type imports in DashboardApiClient', async () => { const dashboardApiClientPath = path.join(apiRoot, 'apps/website/lib/api/dashboard/DashboardApiClient.ts'); const dashboardApiClientContent = await fs.readFile(dashboardApiClientPath, 'utf-8'); // Verify all required types are imported expect(dashboardApiClientContent).toContain('DashboardOverviewDTO'); }); }); });