Files
gridpilot.gg/tests/contracts/dashboard-contract.test.ts
Marc Mintel 12027793b1
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
contract tests
2026-01-22 17:31:54 +01:00

1074 lines
46 KiB
TypeScript

/**
* 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<string, OpenAPISchema>;
required?: string[];
enum?: string[];
nullable?: boolean;
description?: string;
default?: unknown;
}
interface OpenAPISpec {
openapi: string;
info: {
title: string;
description: string;
version: string;
};
paths: Record<string, any>;
components: {
schemas: Record<string, OpenAPISchema>;
};
}
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<DashboardOverviewDTO>('/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<DashboardOverviewDTO>('/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');
});
});
});