contract testing

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

View File

@@ -0,0 +1,230 @@
/**
* Contract Consumption Tests for Website
*
* These tests validate that the website can properly consume and use
* the generated API types without type errors.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs/promises';
import * as path from 'path';
import { glob } from 'glob';
// Import all generated DTOs to ensure they compile
import type { RequestAvatarGenerationInputDTO } from './generated/RequestAvatarGenerationInputDTO';
import type { RequestAvatarGenerationOutputDTO } from './generated/RequestAvatarGenerationOutputDTO';
import type { UploadMediaInputDTO } from './generated/UploadMediaInputDTO';
import type { UploadMediaOutputDTO } from './generated/UploadMediaOutputDTO';
import type { GetMediaOutputDTO } from './generated/GetMediaOutputDTO';
import type { DeleteMediaOutputDTO } from './generated/DeleteMediaOutputDTO';
import type { GetAvatarOutputDTO } from './generated/GetAvatarOutputDTO';
import type { UpdateAvatarInputDTO } from './generated/UpdateAvatarInputDTO';
import type { UpdateAvatarOutputDTO } from './generated/UpdateAvatarOutputDTO';
import type { RaceDTO } from './generated/RaceDTO';
import type { DriverDTO } from './generated/DriverDTO';
describe('Website Contract Consumption', () => {
const generatedTypesDir = path.join(__dirname, 'generated');
describe('Generated Types Availability', () => {
it('should have generated types directory', async () => {
const exists = await fs.access(generatedTypesDir).then(() => true).catch(() => false);
expect(exists).toBe(true);
});
it('should have expected DTO files', async () => {
const requiredDTOs = [
'RequestAvatarGenerationInputDTO.ts',
'RequestAvatarGenerationOutputDTO.ts',
'UploadMediaInputDTO.ts',
'UploadMediaOutputDTO.ts',
'GetMediaOutputDTO.ts',
'DeleteMediaOutputDTO.ts',
'GetAvatarOutputDTO.ts',
'UpdateAvatarInputDTO.ts',
'UpdateAvatarOutputDTO.ts',
'RaceDTO.ts',
'DriverDTO.ts'
];
const files = await fs.readdir(generatedTypesDir);
const tsFiles = files.filter(f => f.endsWith('.ts'));
for (const required of requiredDTOs) {
expect(tsFiles).toContain(required);
}
});
it('should have no syntax errors in generated files', async () => {
const files = await fs.readdir(generatedTypesDir);
const dtos = files.filter(f => f.endsWith('.ts'));
for (const file of dtos) {
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
// Basic syntax validation
expect(content).toContain('export interface');
expect(content).toContain('{');
expect(content).toContain('}');
// Should not have common syntax errors
expect(content).not.toMatch(/interface\s+\w+\s*\{\s*\}/); // Empty interfaces
}
});
});
describe('Type Compatibility', () => {
it('should allow creating valid DTO objects', () => {
// Test RequestAvatarGenerationInputDTO
const avatarInput: RequestAvatarGenerationInputDTO = {
userId: 'user-123',
facePhotoData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
suitColor: 'red'
};
expect(avatarInput.userId).toBe('user-123');
// Test RequestAvatarGenerationOutputDTO (success case)
const avatarOutputSuccess: RequestAvatarGenerationOutputDTO = {
success: true,
requestId: 'req-123',
avatarUrls: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png']
};
expect(avatarOutputSuccess.success).toBe(true);
expect(avatarOutputSuccess.avatarUrls).toHaveLength(2);
// Test RequestAvatarGenerationOutputDTO (failure case)
const avatarOutputFailure: RequestAvatarGenerationOutputDTO = {
success: false,
errorMessage: 'Generation failed'
};
expect(avatarOutputFailure.success).toBe(false);
expect(avatarOutputFailure.errorMessage).toBe('Generation failed');
});
it('should handle optional fields correctly', () => {
// Test DTOs with optional fields
const uploadInput: UploadMediaInputDTO = {
type: 'image',
category: 'avatar'
};
expect(uploadInput.type).toBe('image');
// Test with minimal required fields
const minimalUpload: UploadMediaInputDTO = {
type: 'image'
};
expect(minimalUpload.type).toBe('image');
});
it('should support array types', () => {
const avatarOutput: RequestAvatarGenerationOutputDTO = {
success: true,
requestId: 'req-123',
avatarUrls: ['url1', 'url2', 'url3']
};
expect(Array.isArray(avatarOutput.avatarUrls)).toBe(true);
expect(avatarOutput.avatarUrls?.length).toBeGreaterThan(0);
});
it('should support nested object types', () => {
const race: RaceDTO = {
id: 'race-123',
name: 'Test Race',
leagueId: 'league-456',
trackName: 'Test Track',
startTime: new Date().toISOString(),
status: 'scheduled',
maxDrivers: 20,
registeredDrivers: 5
};
expect(race.id).toBe('race-123');
expect(race.name).toBe('Test Race');
});
});
describe('Service Integration', () => {
it('should work with service layer patterns', () => {
// Simulate a service method that uses DTOs
function processAvatarRequest(input: RequestAvatarGenerationInputDTO): RequestAvatarGenerationOutputDTO {
// Validate input
if (!input.userId || !input.facePhotoData) {
return {
success: false,
errorMessage: 'Missing required fields'
};
}
return {
success: true,
requestId: `req-${Date.now()}`,
avatarUrls: [`https://cdn.example.com/avatars/${input.userId}.png`]
};
}
const result = processAvatarRequest({
userId: 'test-user',
facePhotoData: 'base64data',
suitColor: 'blue'
});
expect(result.success).toBe(true);
expect(result.avatarUrls).toBeDefined();
expect(result.avatarUrls?.length).toBe(1);
});
it('should handle API response parsing', () => {
// Simulate API response handling
const mockApiResponse = {
success: true,
requestId: 'req-789',
avatarUrls: ['https://cdn.example.com/avatar.png']
};
// Type assertion to ensure it matches DTO
const parsedResponse: RequestAvatarGenerationOutputDTO = mockApiResponse;
expect(parsedResponse.success).toBe(true);
expect(parsedResponse.avatarUrls).toHaveLength(1);
});
});
describe('Error Handling', () => {
it('should handle missing optional fields', () => {
// Test that optional fields can be omitted
const minimalOutput: RequestAvatarGenerationOutputDTO = {
success: false,
errorMessage: 'Error occurred'
};
expect(minimalOutput.success).toBe(false);
expect(minimalOutput.errorMessage).toBe('Error occurred');
// avatarUrls and requestId are optional in failure case
});
it('should allow type narrowing based on success flag', () => {
function handleAvatarResponse(response: RequestAvatarGenerationOutputDTO) {
if (response.success) {
// Success case - avatarUrls should be available
expect(response.avatarUrls).toBeDefined();
expect(response.avatarUrls!.length).toBeGreaterThan(0);
} else {
// Failure case - errorMessage should be available
expect(response.errorMessage).toBeDefined();
}
}
handleAvatarResponse({
success: true,
requestId: 'req-1',
avatarUrls: ['url1']
});
handleAvatarResponse({
success: false,
errorMessage: 'Failed'
});
});
});
});

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface GetRaceDetailParamsDTODTO {
raceId: string;
driverId: string;
export interface AllRacesLeagueFilterDTO {
id: string;
name: string;
}

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface IracingAuthRedirectResult {
redirectUrl: string;
state: string;
}
export interface AllRacesStatusFilterDTO {
value: string;
label: string;
}

View File

@@ -8,6 +8,6 @@ export interface AvailableLeagueDTO {
id: string;
name: string;
game: string;
drivers: number;
avgViewsPerRace: number;
drivers: string;
avgViewsPerRace: string;
}

View File

@@ -5,10 +5,10 @@
*/
export interface BillingStatsDTO {
totalSpent: number;
pendingAmount: number;
totalSpent: string;
pendingAmount: string;
nextPaymentDate: string;
nextPaymentAmount: number;
activeSponsorships: number;
averageMonthlySpend: number;
nextPaymentAmount: string;
activeSponsorships: string;
averageMonthlySpend: string;
}

View File

@@ -11,6 +11,4 @@ export interface DashboardRaceSummaryDTO {
track: string;
car: string;
scheduledAt: string;
status: string;
isMyLeague: boolean;
}

View File

@@ -5,5 +5,5 @@
*/
export interface DeleteMediaOutputDTO {
success: boolean;
success: string;
}

View File

@@ -4,7 +4,6 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface LoginParams {
email: string;
password: string;
}
export interface DeletePrizeResultDTO {
success: boolean;
}

View File

@@ -6,7 +6,10 @@
export interface DriverDTO {
id: string;
iracingId: string;
name: string;
country: string;
position: string;
races: string;
impressions: string;
team: string;
}

View File

@@ -1,100 +0,0 @@
export interface DriverProfileDriverSummaryDTO {
id: string;
name: string;
country: string;
avatarUrl: string;
iracingId: string | null;
joinedAt: string;
rating: number | null;
globalRank: number | null;
consistency: number | null;
bio: string | null;
totalDrivers: number | null;
}
export interface DriverProfileStatsDTO {
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number | null;
bestFinish: number | null;
worstFinish: number | null;
finishRate: number | null;
winRate: number | null;
podiumRate: number | null;
percentile: number | null;
rating: number | null;
consistency: number | null;
overallRank: number | null;
}
export interface DriverProfileFinishDistributionDTO {
totalRaces: number;
wins: number;
podiums: number;
topTen: number;
dnfs: number;
other: number;
}
export interface DriverProfileTeamMembershipDTO {
teamId: string;
teamName: string;
teamTag: string | null;
role: string;
joinedAt: string;
isCurrent: boolean;
}
export interface DriverProfileSocialFriendSummaryDTO {
id: string;
name: string;
country: string;
avatarUrl: string;
}
export interface DriverProfileSocialSummaryDTO {
friendsCount: number;
friends: DriverProfileSocialFriendSummaryDTO[];
}
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
export interface DriverProfileAchievementDTO {
id: string;
title: string;
description: string;
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
rarity: DriverProfileAchievementRarity;
earnedAt: string;
}
export interface DriverProfileSocialHandleDTO {
platform: DriverProfileSocialPlatform;
handle: string;
url: string;
}
export interface DriverProfileExtendedProfileDTO {
socialHandles: DriverProfileSocialHandleDTO[];
achievements: DriverProfileAchievementDTO[];
racingStyle: string;
favoriteTrack: string;
favoriteCar: string;
timezone: string;
availableHours: string;
lookingForTeam: boolean;
openToRequests: boolean;
}
export interface DriverProfileDTO {
currentDriver: DriverProfileDriverSummaryDTO | null;
stats: DriverProfileStatsDTO | null;
finishDistribution: DriverProfileFinishDistributionDTO | null;
teamMemberships: DriverProfileTeamMembershipDTO[];
socialSummary: DriverProfileSocialSummaryDTO;
extendedProfile: DriverProfileExtendedProfileDTO | null;
}

View File

@@ -1,22 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TeamListItemDTO {
id: string;
name: string;
tag: string;
description: string;
memberCount: number;
leagues: string[];
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
export interface GetAllTeamsOutputDTO {
teams: TeamListItemDTO[];
totalCount: number;
}

View File

@@ -1,31 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TeamDTO {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt?: string;
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
export interface MembershipDTO {
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
}
export interface GetDriverTeamOutputDTO {
team: TeamDTO;
membership: MembershipDTO;
isOwner: boolean;
canManage: boolean;
}

View File

@@ -9,7 +9,6 @@ export interface GetMediaOutputDTO {
url: string;
type: string;
category?: string;
/** Format: date-time */
uploadedAt: string;
size?: number;
}

View File

@@ -1,8 +0,0 @@
export interface GetSponsorOutputDTO {
sponsor: {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
};
}

View File

@@ -1,30 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TeamDTO {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt?: string;
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
export interface MembershipDTO {
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
}
export interface GetTeamDetailsOutputDTO {
team: TeamDTO;
membership: MembershipDTO | null;
canManage: boolean;
}

View File

@@ -1,21 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TeamJoinRequestDTO {
requestId: string;
driverId: string;
driverName: string;
teamId: string;
status: 'pending' | 'approved' | 'rejected';
requestedAt: string;
avatarUrl: string;
}
export interface GetTeamJoinRequestsOutputDTO {
requests: TeamJoinRequestDTO[];
pendingCount: number;
totalCount: number;
}

View File

@@ -1,22 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TeamMemberDTO {
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}
export interface GetTeamMembersOutputDTO {
members: TeamMemberDTO[];
totalCount: number;
ownerCount: number;
managerCount: number;
memberCount: number;
}

View File

@@ -9,7 +9,7 @@ export interface InvoiceDTO {
invoiceNumber: string;
date: string;
dueDate: string;
amount: number;
vatAmount: number;
totalAmount: number;
amount: string;
vatAmount: string;
totalAmount: string;
}

View File

@@ -1,19 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
import { ProtestDTO } from './ProtestDTO';
import { RaceDTO } from './RaceDTO';
export interface DriverSummaryDTO {
id: string;
name: string;
}
export interface LeagueAdminProtestsDTO {
protests: ProtestDTO[];
racesById: { [raceId: string]: RaceDTO };
driversById: { [driverId: string]: DriverSummaryDTO };
}

View File

@@ -7,10 +7,4 @@
export interface LeagueWithCapacityDTO {
id: string;
name: string;
description: string;
ownerId: string;
settings: Record<string, unknown>;
maxDrivers: number;
sessionDuration?: number;
visibility?: string;
}

View File

@@ -1,11 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface LoginWithIracingCallbackParams {
code: string;
state: string;
returnTo?: string;
}

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface MemberPaymentDto {
export interface MemberPaymentDTO {
id: string;
feeId: string;
driverId: string;

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface MembershipFeeDto {
export interface MembershipFeeDTO {
id: string;
leagueId: string;
}

View File

@@ -5,10 +5,10 @@
*/
export interface NotificationSettingsDTO {
emailNewSponsorships: boolean;
emailWeeklyReport: boolean;
emailRaceAlerts: boolean;
emailPaymentAlerts: boolean;
emailNewOpportunities: boolean;
emailContractExpiry: boolean;
emailNewSponsorships: string;
emailWeeklyReport: string;
emailRaceAlerts: string;
emailPaymentAlerts: string;
emailNewOpportunities: string;
emailContractExpiry: string;
}

View File

@@ -5,8 +5,8 @@
*/
export interface PrivacySettingsDTO {
publicProfile: boolean;
showStats: boolean;
showActiveSponsorships: boolean;
allowDirectContact: boolean;
publicProfile: string;
showStats: string;
showActiveSponsorships: string;
allowDirectContact: string;
}

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface PrizeDto {
export interface PrizeDTO {
id: string;
leagueId: string;
seasonId: string;

View File

@@ -8,4 +8,5 @@ export interface RaceDTO {
id: string;
name: string;
date: string;
views: string;
}

View File

@@ -1,12 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
import { RacePenaltyDTO } from './RacePenaltyDTO';
export interface RacePenaltiesDTO {
penalties: RacePenaltyDTO[];
driverMap: Record<string, string>;
}

View File

@@ -1,5 +0,0 @@
export interface RecordEngagementInputDTO {
eventType: string;
userId?: string;
metadata?: Record<string, unknown>;
}

View File

@@ -1,4 +0,0 @@
export interface RecordPageViewInputDTO {
path: string;
userId?: string;
}

View File

@@ -1,14 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface SignupParams {
email: string;
password: string;
displayName: string;
iracingCustomerId?: string;
primaryDriverId?: string;
avatarUrl?: string;
}

View File

@@ -5,7 +5,7 @@
*/
export interface SponsorDashboardInvestmentDTO {
activeSponsorships: number;
totalInvestment: number;
costPerThousandViews: number;
activeSponsorships: string;
totalInvestment: string;
costPerThousandViews: string;
}

View File

@@ -5,12 +5,12 @@
*/
export interface SponsorDashboardMetricsDTO {
impressions: number;
impressionsChange: number;
uniqueViewers: number;
viewersChange: number;
races: number;
drivers: number;
exposure: number;
exposureChange: number;
impressions: string;
impressionsChange: string;
uniqueViewers: string;
viewersChange: string;
races: string;
drivers: string;
exposure: string;
exposureChange: string;
}

View File

@@ -7,6 +7,6 @@
export interface SponsorshipPricingItemDTO {
id: string;
level: string;
price: number;
price: string;
currency: string;
}

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface TransactionDto {
export interface TransactionDTO {
id: string;
walletId: string;
}

View File

@@ -5,5 +5,5 @@
*/
export interface UpdateAvatarOutputDTO {
success: boolean;
success: string;
}

View File

@@ -1,11 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface UpdateTeamInputDTO {
name?: string;
tag?: string;
description?: string;
}

View File

@@ -5,5 +5,5 @@
*/
export interface UploadMediaOutputDTO {
success: boolean;
success: string;
}

View File

@@ -4,7 +4,7 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface WalletDto {
export interface WalletDTO {
id: string;
leagueId: string;
balance: number;