From 5e491d9724e01f615e82e3a37c034dc27476bd2b Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 24 Dec 2025 00:01:01 +0100 Subject: [PATCH] contract testing --- .github/workflows/contract-testing.yml | 110 ++++ apps/api/openapi.json | 489 +++++++++--------- .../shared/testing/contractValidation.test.ts | 257 +++++++++ .../lib/types/contractConsumption.test.ts | 230 ++++++++ ...msDTODTO.ts => AllRacesLeagueFilterDTO.ts} | 6 +- ...ctResult.ts => AllRacesStatusFilterDTO.ts} | 8 +- .../lib/types/generated/AvailableLeagueDTO.ts | 4 +- .../lib/types/generated/BillingStatsDTO.ts | 10 +- .../generated/DashboardRaceSummaryDTO.ts | 2 - .../types/generated/DeleteMediaOutputDTO.ts | 2 +- ...LoginParams.ts => DeletePrizeResultDTO.ts} | 7 +- apps/website/lib/types/generated/DriverDTO.ts | 5 +- .../lib/types/generated/DriverProfileDTO.ts | 100 ---- .../types/generated/GetAllTeamsOutputDTO.ts | 22 - .../types/generated/GetDriverTeamOutputDTO.ts | 31 -- .../lib/types/generated/GetMediaOutputDTO.ts | 1 - .../types/generated/GetSponsorOutputDTO.ts | 8 - .../generated/GetTeamDetailsOutputDTO.ts | 30 -- .../generated/GetTeamJoinRequestsOutputDTO.ts | 21 - .../generated/GetTeamMembersOutputDTO.ts | 22 - .../website/lib/types/generated/InvoiceDTO.ts | 6 +- .../types/generated/LeagueAdminProtestsDTO.ts | 19 - .../types/generated/LeagueWithCapacityDTO.ts | 6 - .../LoginWithIracingCallbackParams.ts | 11 - .../lib/types/generated/MemberPaymentDto.ts | 2 +- .../lib/types/generated/MembershipFeeDto.ts | 2 +- .../generated/NotificationSettingsDTO.ts | 12 +- .../lib/types/generated/PrivacySettingsDTO.ts | 8 +- apps/website/lib/types/generated/PrizeDto.ts | 2 +- apps/website/lib/types/generated/RaceDTO.ts | 1 + .../lib/types/generated/RacePenaltiesDTO.ts | 12 - .../generated/RecordEngagementInputDTO.ts | 5 - .../types/generated/RecordPageViewInputDTO.ts | 4 - .../lib/types/generated/SignupParams.ts | 14 - .../SponsorDashboardInvestmentDTO.ts | 6 +- .../generated/SponsorDashboardMetricsDTO.ts | 16 +- .../generated/SponsorshipPricingItemDTO.ts | 2 +- .../lib/types/generated/TransactionDto.ts | 2 +- .../types/generated/UpdateAvatarOutputDTO.ts | 2 +- .../lib/types/generated/UpdateTeamInputDTO.ts | 11 - .../types/generated/UploadMediaOutputDTO.ts | 2 +- apps/website/lib/types/generated/WalletDto.ts | 2 +- docs/CONTRACT_TESTING.md | 271 ++++++++++ docs/CONTRACT_TESTING_QUICKSTART.md | 168 ++++++ package.json | 5 + scripts/contract-compatibility.ts | 288 +++++++++++ scripts/generate-api-types.ts | 26 +- scripts/generate-openapi-spec.ts | 16 +- scripts/run-contract-tests.ts | 85 +++ scripts/test/type-generation.test.ts | 258 +++++++++ vitest.scripts.config.ts | 18 + vitest.website.config.ts | 23 + 52 files changed, 2058 insertions(+), 612 deletions(-) create mode 100644 .github/workflows/contract-testing.yml create mode 100644 apps/api/src/shared/testing/contractValidation.test.ts create mode 100644 apps/website/lib/types/contractConsumption.test.ts rename apps/website/lib/types/generated/{GetRaceDetailParamsDTODTO.ts => AllRacesLeagueFilterDTO.ts} (68%) rename apps/website/lib/types/generated/{IracingAuthRedirectResult.ts => AllRacesStatusFilterDTO.ts} (67%) rename apps/website/lib/types/generated/{LoginParams.ts => DeletePrizeResultDTO.ts} (71%) delete mode 100644 apps/website/lib/types/generated/DriverProfileDTO.ts delete mode 100644 apps/website/lib/types/generated/GetAllTeamsOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/GetDriverTeamOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/GetSponsorOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/GetTeamDetailsOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/GetTeamJoinRequestsOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/GetTeamMembersOutputDTO.ts delete mode 100644 apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts delete mode 100644 apps/website/lib/types/generated/LoginWithIracingCallbackParams.ts delete mode 100644 apps/website/lib/types/generated/RacePenaltiesDTO.ts delete mode 100644 apps/website/lib/types/generated/RecordEngagementInputDTO.ts delete mode 100644 apps/website/lib/types/generated/RecordPageViewInputDTO.ts delete mode 100644 apps/website/lib/types/generated/SignupParams.ts delete mode 100644 apps/website/lib/types/generated/UpdateTeamInputDTO.ts create mode 100644 docs/CONTRACT_TESTING.md create mode 100644 docs/CONTRACT_TESTING_QUICKSTART.md create mode 100644 scripts/contract-compatibility.ts create mode 100644 scripts/run-contract-tests.ts create mode 100644 scripts/test/type-generation.test.ts create mode 100644 vitest.scripts.config.ts create mode 100644 vitest.website.config.ts diff --git a/.github/workflows/contract-testing.yml b/.github/workflows/contract-testing.yml new file mode 100644 index 000000000..219865a3c --- /dev/null +++ b/.github/workflows/contract-testing.yml @@ -0,0 +1,110 @@ +name: Contract Testing + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + contract-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run API contract validation + run: npm run test:api:contracts + + - name: Generate OpenAPI spec + run: npm run api:generate-spec + + - name: Generate TypeScript types + run: npm run api:generate-types + + - name: Run contract compatibility check + run: npm run test:contract:compatibility + + - name: Verify website type checking + run: npm run website:type-check + + - name: Upload generated types as artifacts + uses: actions/upload-artifact@v3 + with: + name: generated-types + path: apps/website/lib/types/generated/ + retention-days: 7 + + - name: Comment PR with results + if: github.event_name == 'pull_request' + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Read any contract change reports + const reportPath = path.join(process.cwd(), 'contract-report.json'); + if (fs.existsSync(reportPath)) { + const report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); + + const comment = ` + ## ๐Ÿ” Contract Testing Results + + โœ… **All contract tests passed!** + + ### Changes Summary: + - Total changes: ${report.totalChanges} + - Breaking changes: ${report.breakingChanges} + - Added: ${report.added} + - Removed: ${report.removed} + - Modified: ${report.modified} + + Generated types are available as artifacts. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } + + contract-snapshot: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate and snapshot types + run: | + npm run api:generate-spec + npm run api:generate-types + + - name: Commit generated types + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add apps/website/lib/types/generated/ + git diff --staged --quiet || git commit -m "chore: update generated API types [skip ci]" + git push \ No newline at end of file diff --git a/apps/api/openapi.json b/apps/api/openapi.json index c572e9912..37fc890c9 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -97,7 +97,7 @@ "type": "string" }, "price": { - "type": "number" + "type": "string" }, "currency": { "type": "string" @@ -213,28 +213,28 @@ "type": "object", "properties": { "impressions": { - "type": "number" + "type": "string" }, "impressionsChange": { - "type": "number" + "type": "string" }, "uniqueViewers": { - "type": "number" + "type": "string" }, "viewersChange": { - "type": "number" + "type": "string" }, "races": { - "type": "number" + "type": "string" }, "drivers": { - "type": "number" + "type": "string" }, "exposure": { - "type": "number" + "type": "string" }, "exposureChange": { - "type": "number" + "type": "string" } }, "required": [ @@ -252,13 +252,13 @@ "type": "object", "properties": { "activeSponsorships": { - "type": "number" + "type": "string" }, "totalInvestment": { - "type": "number" + "type": "string" }, "costPerThousandViews": { - "type": "number" + "type": "string" } }, "required": [ @@ -334,28 +334,32 @@ }, "date": { "type": "string" + }, + "views": { + "type": "string" } }, "required": [ "id", "name", - "date" + "date", + "views" ] }, "PrivacySettingsDTO": { "type": "object", "properties": { "publicProfile": { - "type": "boolean" + "type": "string" }, "showStats": { - "type": "boolean" + "type": "string" }, "showActiveSponsorships": { - "type": "boolean" + "type": "string" }, "allowDirectContact": { - "type": "boolean" + "type": "string" } }, "required": [ @@ -380,22 +384,22 @@ "type": "object", "properties": { "emailNewSponsorships": { - "type": "boolean" + "type": "string" }, "emailWeeklyReport": { - "type": "boolean" + "type": "string" }, "emailRaceAlerts": { - "type": "boolean" + "type": "string" }, "emailPaymentAlerts": { - "type": "boolean" + "type": "string" }, "emailNewOpportunities": { - "type": "boolean" + "type": "string" }, "emailContractExpiry": { - "type": "boolean" + "type": "string" } }, "required": [ @@ -442,13 +446,13 @@ "type": "string" }, "amount": { - "type": "number" + "type": "string" }, "vatAmount": { - "type": "number" + "type": "string" }, "totalAmount": { - "type": "number" + "type": "string" } }, "required": [ @@ -519,21 +523,33 @@ "id": { "type": "string" }, - "iracingId": { - "type": "string" - }, "name": { "type": "string" }, "country": { "type": "string" + }, + "position": { + "type": "string" + }, + "races": { + "type": "string" + }, + "impressions": { + "type": "string" + }, + "team": { + "type": "string" } }, "required": [ "id", - "iracingId", "name", - "country" + "country", + "position", + "races", + "impressions", + "team" ] }, "CreateSponsorInputDTO": { @@ -555,22 +571,22 @@ "type": "object", "properties": { "totalSpent": { - "type": "number" + "type": "string" }, "pendingAmount": { - "type": "number" + "type": "string" }, "nextPaymentDate": { "type": "string" }, "nextPaymentAmount": { - "type": "number" + "type": "string" }, "activeSponsorships": { - "type": "number" + "type": "string" }, "averageMonthlySpend": { - "type": "number" + "type": "string" } }, "required": [ @@ -595,10 +611,10 @@ "type": "string" }, "drivers": { - "type": "number" + "type": "string" }, "avgViewsPerRace": { - "type": "number" + "type": "string" } }, "required": [ @@ -1197,12 +1213,6 @@ }, "scheduledAt": { "type": "string" - }, - "status": { - "type": "string" - }, - "isMyLeague": { - "type": "boolean" } }, "required": [ @@ -1211,9 +1221,7 @@ "leagueName", "track", "car", - "scheduledAt", - "status", - "isMyLeague" + "scheduledAt" ] }, "DashboardLeagueStandingSummaryDTO": { @@ -1373,6 +1381,36 @@ "leagueName" ] }, + "AllRacesStatusFilterDTO": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ] + }, + "AllRacesLeagueFilterDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, "UpdatePaymentStatusInputDTO": { "type": "object", "properties": { @@ -1384,7 +1422,7 @@ "paymentId" ] }, - "PaymentDto": { + "PaymentDTO": { "type": "object", "properties": { "id": { @@ -1395,7 +1433,7 @@ "id" ] }, - "MembershipFeeDto": { + "MembershipFeeDTO": { "type": "object", "properties": { "id": { @@ -1410,7 +1448,7 @@ "leagueId" ] }, - "MemberPaymentDto": { + "MemberPaymentDTO": { "type": "object", "properties": { "id": { @@ -1441,7 +1479,7 @@ "netAmount" ] }, - "PrizeDto": { + "PrizeDTO": { "type": "object", "properties": { "id": { @@ -1472,7 +1510,7 @@ "amount" ] }, - "WalletDto": { + "WalletDTO": { "type": "object", "properties": { "id": { @@ -1512,7 +1550,7 @@ "currency" ] }, - "TransactionDto": { + "TransactionDTO": { "type": "object", "properties": { "id": { @@ -1527,18 +1565,7 @@ "walletId" ] }, - "PaymentDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ] - }, - "UploadMediaOutputDTO": { + "DeletePrizeResultDTO": { "type": "object", "properties": { "success": { @@ -1549,11 +1576,22 @@ "success" ] }, + "UploadMediaOutputDTO": { + "type": "object", + "properties": { + "success": { + "type": "string" + } + }, + "required": [ + "success" + ] + }, "UpdateAvatarOutputDTO": { "type": "object", "properties": { "success": { - "type": "boolean" + "type": "string" } }, "required": [ @@ -1610,8 +1648,7 @@ "type": "string" }, "uploadedAt": { - "type": "string", - "format": "date-time" + "type": "string" }, "size": { "type": "number" @@ -1639,7 +1676,7 @@ "type": "object", "properties": { "success": { - "type": "boolean" + "type": "string" } }, "required": [ @@ -1840,33 +1877,11 @@ }, "name": { "type": "string" - }, - "description": { - "type": "string" - }, - "ownerId": { - "type": "string" - }, - "settings": { - "type": "object" - }, - "maxDrivers": { - "type": "number" - }, - "sessionDuration": { - "type": "number" - }, - "visibility": { - "type": "string" } }, "required": [ "id", - "name", - "description", - "ownerId", - "settings", - "maxDrivers" + "name" ] }, "LeagueSummaryDTO": { @@ -2283,151 +2298,6 @@ "driverId" ] }, - "DriverProfileDriverSummaryDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "country": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "country", - "avatarUrl" - ] - }, - "DriverProfileStatsDTO": { - "type": "object", - "properties": { - "totalRaces": { - "type": "number" - }, - "wins": { - "type": "number" - }, - "podiums": { - "type": "number" - }, - "dnfs": { - "type": "number" - } - }, - "required": [ - "totalRaces", - "wins", - "podiums", - "dnfs" - ] - }, - "DriverProfileFinishDistributionDTO": { - "type": "object", - "properties": { - "totalRaces": { - "type": "number" - }, - "wins": { - "type": "number" - }, - "podiums": { - "type": "number" - }, - "topTen": { - "type": "number" - }, - "dnfs": { - "type": "number" - }, - "other": { - "type": "number" - } - }, - "required": [ - "totalRaces", - "wins", - "podiums", - "topTen", - "dnfs", - "other" - ] - }, - "DriverProfileTeamMembershipDTO": { - "type": "object", - "properties": { - "teamId": { - "type": "string" - }, - "teamName": { - "type": "string" - } - }, - "required": [ - "teamId", - "teamName" - ] - }, - "DriverProfileSocialFriendSummaryDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "country": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "country", - "avatarUrl" - ] - }, - "DriverProfileSocialSummaryDTO": { - "type": "object", - "properties": { - "friendsCount": { - "type": "number" - } - }, - "required": [ - "friendsCount" - ] - }, - "DriverProfileAchievementDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": [ - "id", - "title", - "description" - ] - }, "GetDriverOutputDTO": { "type": "object", "properties": { @@ -2488,6 +2358,151 @@ "driverId" ] }, + "DriverProfileTeamMembershipDTO": { + "type": "object", + "properties": { + "teamId": { + "type": "string" + }, + "teamName": { + "type": "string" + } + }, + "required": [ + "teamId", + "teamName" + ] + }, + "DriverProfileStatsDTO": { + "type": "object", + "properties": { + "totalRaces": { + "type": "number" + }, + "wins": { + "type": "number" + }, + "podiums": { + "type": "number" + }, + "dnfs": { + "type": "number" + } + }, + "required": [ + "totalRaces", + "wins", + "podiums", + "dnfs" + ] + }, + "DriverProfileSocialSummaryDTO": { + "type": "object", + "properties": { + "friendsCount": { + "type": "number" + } + }, + "required": [ + "friendsCount" + ] + }, + "DriverProfileSocialFriendSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "country", + "avatarUrl" + ] + }, + "DriverProfileFinishDistributionDTO": { + "type": "object", + "properties": { + "totalRaces": { + "type": "number" + }, + "wins": { + "type": "number" + }, + "podiums": { + "type": "number" + }, + "topTen": { + "type": "number" + }, + "dnfs": { + "type": "number" + }, + "other": { + "type": "number" + } + }, + "required": [ + "totalRaces", + "wins", + "podiums", + "topTen", + "dnfs", + "other" + ] + }, + "DriverProfileDriverSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "country", + "avatarUrl" + ] + }, + "DriverProfileAchievementDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "description" + ] + }, "DriverLeaderboardItemDTO": { "type": "object", "properties": { diff --git a/apps/api/src/shared/testing/contractValidation.test.ts b/apps/api/src/shared/testing/contractValidation.test.ts new file mode 100644 index 000000000..5de78ac8c --- /dev/null +++ b/apps/api/src/shared/testing/contractValidation.test.ts @@ -0,0 +1,257 @@ +/** + * Contract Validation Tests for API + * + * These tests validate that the API DTOs and OpenAPI spec are consistent + * and that the generated types will be compatible with the website. + */ + +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { glob } from 'glob'; + +interface OpenAPISchema { + type?: string; + format?: string; + $ref?: string; + items?: OpenAPISchema; + properties?: Record; + required?: string[]; + enum?: string[]; + nullable?: boolean; + description?: string; +} + +interface OpenAPISpec { + openapi: string; + info: { + title: string; + description: string; + version: string; + }; + paths: Record; + components: { + schemas: Record; + }; +} + +describe('API Contract Validation', () => { + const apiRoot = path.join(__dirname, '../../..'); + const openapiPath = path.join(apiRoot, 'openapi.json'); + const generatedTypesDir = path.join(apiRoot, '../website/lib/types/generated'); + + describe('OpenAPI Spec Integrity', () => { + it('should have a valid OpenAPI spec file', async () => { + const specExists = await fs.access(openapiPath).then(() => true).catch(() => false); + expect(specExists).toBe(true); + }); + + it('should have a valid JSON structure', async () => { + const content = await fs.readFile(openapiPath, 'utf-8'); + expect(() => JSON.parse(content)).not.toThrow(); + }); + + it('should have required OpenAPI fields', async () => { + const content = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(content); + + expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/); + expect(spec.info).toBeDefined(); + expect(spec.info.title).toBeDefined(); + expect(spec.info.version).toBeDefined(); + expect(spec.components).toBeDefined(); + expect(spec.components.schemas).toBeDefined(); + }); + + it('should have no circular references in schemas', async () => { + const content = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(content); + const schemas = spec.components.schemas; + + const visited = new Set(); + const visiting = new Set(); + + function detectCircular(schemaName: string): boolean { + if (visiting.has(schemaName)) return true; + if (visited.has(schemaName)) return false; + + visiting.add(schemaName); + const schema = schemas[schemaName]; + + if (!schema) { + visiting.delete(schemaName); + visited.add(schemaName); + return false; + } + + // Check properties for references + if (schema.properties) { + for (const prop of Object.values(schema.properties)) { + if (prop.$ref) { + const refName = prop.$ref.split('/').pop(); + if (refName && detectCircular(refName)) { + return true; + } + } + if (prop.items?.$ref) { + const refName = prop.items.$ref.split('/').pop(); + if (refName && detectCircular(refName)) { + return true; + } + } + } + } + + visiting.delete(schemaName); + visited.add(schemaName); + return false; + } + + for (const schemaName of Object.keys(schemas)) { + expect(detectCircular(schemaName)).toBe(false); + } + }); + }); + + describe('DTO Consistency', () => { + it('should have DTO files for all schemas', async () => { + const content = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(content); + const schemas = Object.keys(spec.components.schemas); + + const generatedFiles = await fs.readdir(generatedTypesDir); + const generatedDTOs = generatedFiles + .filter(f => f.endsWith('.ts')) + .map(f => f.replace('.ts', '')); + + // All schemas should have corresponding generated DTOs + for (const schema of schemas) { + expect(generatedDTOs).toContain(schema); + } + }); + + 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; + + for (const [schemaName, schema] of Object.entries(schemas)) { + const dtoPath = path.join(generatedTypesDir, `${schemaName}.ts`); + const dtoExists = await fs.access(dtoPath).then(() => true).catch(() => false); + + if (!dtoExists) continue; + + const dtoContent = await fs.readFile(dtoPath, 'utf-8'); + + // Check that all required properties are present + if (schema.required) { + for (const requiredProp of schema.required) { + expect(dtoContent).toContain(requiredProp); + } + } + + // Check that all properties are present + if (schema.properties) { + for (const propName of Object.keys(schema.properties)) { + expect(dtoContent).toContain(propName); + } + } + } + }); + }); + + describe('Type Generation Integrity', () => { + it('should have valid TypeScript syntax 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 TypeScript syntax checks + expect(content).toContain('export interface'); + expect(content).toContain('{'); + expect(content).toContain('}'); + + // Should not have syntax errors (basic check) + expect(content).not.toContain('undefined;'); + expect(content).not.toContain('any;'); + } + }); + + it('should have proper imports for dependencies', async () => { + const files = await fs.readdir(generatedTypesDir); + const dtos = files.filter(f => f.endsWith('.ts')); + + for (const file of dtos) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + const importMatches = content.match(/import type \{ (\w+) \} from '\.\/(\w+)';/g) || []; + + for (const importLine of importMatches) { + const match = importLine.match(/import type \{ (\w+) \} from '\.\/(\w+)';/); + if (match) { + const [, importedType, fromFile] = match; + expect(importedType).toBe(fromFile); + + // Check that the imported file exists + const importedPath = path.join(generatedTypesDir, `${fromFile}.ts`); + const exists = await fs.access(importedPath).then(() => true).catch(() => false); + expect(exists).toBe(true); + } + } + } + }); + }); + + describe('Contract Compatibility', () => { + it('should maintain backward compatibility for existing DTOs', async () => { + // This test ensures that when regenerating types, existing properties aren't removed + // unless explicitly intended + const content = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(content); + + // Check critical DTOs that are likely used in production + const criticalDTOs = [ + 'RequestAvatarGenerationInputDTO', + 'RequestAvatarGenerationOutputDTO', + 'UploadMediaInputDTO', + 'UploadMediaOutputDTO', + 'RaceDTO', + 'DriverDTO' + ]; + + for (const dtoName of criticalDTOs) { + if (spec.components.schemas[dtoName]) { + const dtoPath = path.join(generatedTypesDir, `${dtoName}.ts`); + const exists = await fs.access(dtoPath).then(() => true).catch(() => false); + expect(exists).toBe(true); + } + } + }); + + it('should handle nullable fields correctly', async () => { + const content = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(content); + const schemas = spec.components.schemas; + + for (const [schemaName, schema] of Object.entries(schemas)) { + if (!schema.properties) continue; + + for (const [propName, propSchema] of Object.entries(schema.properties)) { + const dtoPath = path.join(generatedTypesDir, `${schemaName}.ts`); + const dtoContent = await fs.readFile(dtoPath, 'utf-8'); + + if (propSchema.nullable) { + // Nullable properties should be optional in TypeScript + const propRegex = new RegExp(`${propName}\\??:\\s*([\\w\\[\\]<>|]+)\\s*;`); + const match = dtoContent.match(propRegex); + if (match) { + // Should include null in the type or be optional + expect(match[1]).toContain('| null'); + } + } + } + } + }); + }); +}); \ No newline at end of file diff --git a/apps/website/lib/types/contractConsumption.test.ts b/apps/website/lib/types/contractConsumption.test.ts new file mode 100644 index 000000000..fccdae0b8 --- /dev/null +++ b/apps/website/lib/types/contractConsumption.test.ts @@ -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' + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetRaceDetailParamsDTODTO.ts b/apps/website/lib/types/generated/AllRacesLeagueFilterDTO.ts similarity index 68% rename from apps/website/lib/types/generated/GetRaceDetailParamsDTODTO.ts rename to apps/website/lib/types/generated/AllRacesLeagueFilterDTO.ts index 718452ac7..b421811b3 100644 --- a/apps/website/lib/types/generated/GetRaceDetailParamsDTODTO.ts +++ b/apps/website/lib/types/generated/AllRacesLeagueFilterDTO.ts @@ -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; } diff --git a/apps/website/lib/types/generated/IracingAuthRedirectResult.ts b/apps/website/lib/types/generated/AllRacesStatusFilterDTO.ts similarity index 67% rename from apps/website/lib/types/generated/IracingAuthRedirectResult.ts rename to apps/website/lib/types/generated/AllRacesStatusFilterDTO.ts index 26b0d5897..475d1e534 100644 --- a/apps/website/lib/types/generated/IracingAuthRedirectResult.ts +++ b/apps/website/lib/types/generated/AllRacesStatusFilterDTO.ts @@ -4,7 +4,7 @@ * Do not edit manually - regenerate using: npm run api:sync-types */ -export interface IracingAuthRedirectResult { - redirectUrl: string; - state: string; -} \ No newline at end of file +export interface AllRacesStatusFilterDTO { + value: string; + label: string; +} diff --git a/apps/website/lib/types/generated/AvailableLeagueDTO.ts b/apps/website/lib/types/generated/AvailableLeagueDTO.ts index cf014d9d9..6bf6bb10a 100644 --- a/apps/website/lib/types/generated/AvailableLeagueDTO.ts +++ b/apps/website/lib/types/generated/AvailableLeagueDTO.ts @@ -8,6 +8,6 @@ export interface AvailableLeagueDTO { id: string; name: string; game: string; - drivers: number; - avgViewsPerRace: number; + drivers: string; + avgViewsPerRace: string; } diff --git a/apps/website/lib/types/generated/BillingStatsDTO.ts b/apps/website/lib/types/generated/BillingStatsDTO.ts index 946d840a6..a3dbcb371 100644 --- a/apps/website/lib/types/generated/BillingStatsDTO.ts +++ b/apps/website/lib/types/generated/BillingStatsDTO.ts @@ -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; } diff --git a/apps/website/lib/types/generated/DashboardRaceSummaryDTO.ts b/apps/website/lib/types/generated/DashboardRaceSummaryDTO.ts index ceff5a196..33531f5ad 100644 --- a/apps/website/lib/types/generated/DashboardRaceSummaryDTO.ts +++ b/apps/website/lib/types/generated/DashboardRaceSummaryDTO.ts @@ -11,6 +11,4 @@ export interface DashboardRaceSummaryDTO { track: string; car: string; scheduledAt: string; - status: string; - isMyLeague: boolean; } diff --git a/apps/website/lib/types/generated/DeleteMediaOutputDTO.ts b/apps/website/lib/types/generated/DeleteMediaOutputDTO.ts index d3294f301..5e9b376f1 100644 --- a/apps/website/lib/types/generated/DeleteMediaOutputDTO.ts +++ b/apps/website/lib/types/generated/DeleteMediaOutputDTO.ts @@ -5,5 +5,5 @@ */ export interface DeleteMediaOutputDTO { - success: boolean; + success: string; } diff --git a/apps/website/lib/types/generated/LoginParams.ts b/apps/website/lib/types/generated/DeletePrizeResultDTO.ts similarity index 71% rename from apps/website/lib/types/generated/LoginParams.ts rename to apps/website/lib/types/generated/DeletePrizeResultDTO.ts index aa7f438d9..06885e0b6 100644 --- a/apps/website/lib/types/generated/LoginParams.ts +++ b/apps/website/lib/types/generated/DeletePrizeResultDTO.ts @@ -4,7 +4,6 @@ * Do not edit manually - regenerate using: npm run api:sync-types */ -export interface LoginParams { - email: string; - password: string; -} \ No newline at end of file +export interface DeletePrizeResultDTO { + success: boolean; +} diff --git a/apps/website/lib/types/generated/DriverDTO.ts b/apps/website/lib/types/generated/DriverDTO.ts index 2d5860fa4..191b9caf2 100644 --- a/apps/website/lib/types/generated/DriverDTO.ts +++ b/apps/website/lib/types/generated/DriverDTO.ts @@ -6,7 +6,10 @@ export interface DriverDTO { id: string; - iracingId: string; name: string; country: string; + position: string; + races: string; + impressions: string; + team: string; } diff --git a/apps/website/lib/types/generated/DriverProfileDTO.ts b/apps/website/lib/types/generated/DriverProfileDTO.ts deleted file mode 100644 index 52e23e847..000000000 --- a/apps/website/lib/types/generated/DriverProfileDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetAllTeamsOutputDTO.ts b/apps/website/lib/types/generated/GetAllTeamsOutputDTO.ts deleted file mode 100644 index 9ea45faba..000000000 --- a/apps/website/lib/types/generated/GetAllTeamsOutputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetDriverTeamOutputDTO.ts b/apps/website/lib/types/generated/GetDriverTeamOutputDTO.ts deleted file mode 100644 index e4de6ca6e..000000000 --- a/apps/website/lib/types/generated/GetDriverTeamOutputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetMediaOutputDTO.ts b/apps/website/lib/types/generated/GetMediaOutputDTO.ts index 37bfa441e..8730c99b5 100644 --- a/apps/website/lib/types/generated/GetMediaOutputDTO.ts +++ b/apps/website/lib/types/generated/GetMediaOutputDTO.ts @@ -9,7 +9,6 @@ export interface GetMediaOutputDTO { url: string; type: string; category?: string; - /** Format: date-time */ uploadedAt: string; size?: number; } diff --git a/apps/website/lib/types/generated/GetSponsorOutputDTO.ts b/apps/website/lib/types/generated/GetSponsorOutputDTO.ts deleted file mode 100644 index cffd5c53b..000000000 --- a/apps/website/lib/types/generated/GetSponsorOutputDTO.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface GetSponsorOutputDTO { - sponsor: { - id: string; - name: string; - logoUrl?: string; - websiteUrl?: string; - }; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetTeamDetailsOutputDTO.ts b/apps/website/lib/types/generated/GetTeamDetailsOutputDTO.ts deleted file mode 100644 index 9e92a0c53..000000000 --- a/apps/website/lib/types/generated/GetTeamDetailsOutputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetTeamJoinRequestsOutputDTO.ts b/apps/website/lib/types/generated/GetTeamJoinRequestsOutputDTO.ts deleted file mode 100644 index 6507e0e23..000000000 --- a/apps/website/lib/types/generated/GetTeamJoinRequestsOutputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/GetTeamMembersOutputDTO.ts b/apps/website/lib/types/generated/GetTeamMembersOutputDTO.ts deleted file mode 100644 index 3f026cd32..000000000 --- a/apps/website/lib/types/generated/GetTeamMembersOutputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/InvoiceDTO.ts b/apps/website/lib/types/generated/InvoiceDTO.ts index f45877653..314d99d7b 100644 --- a/apps/website/lib/types/generated/InvoiceDTO.ts +++ b/apps/website/lib/types/generated/InvoiceDTO.ts @@ -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; } diff --git a/apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts b/apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts deleted file mode 100644 index 2fb5c65ed..000000000 --- a/apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts +++ /dev/null @@ -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 }; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/LeagueWithCapacityDTO.ts b/apps/website/lib/types/generated/LeagueWithCapacityDTO.ts index 650336ea2..332da08a5 100644 --- a/apps/website/lib/types/generated/LeagueWithCapacityDTO.ts +++ b/apps/website/lib/types/generated/LeagueWithCapacityDTO.ts @@ -7,10 +7,4 @@ export interface LeagueWithCapacityDTO { id: string; name: string; - description: string; - ownerId: string; - settings: Record; - maxDrivers: number; - sessionDuration?: number; - visibility?: string; } diff --git a/apps/website/lib/types/generated/LoginWithIracingCallbackParams.ts b/apps/website/lib/types/generated/LoginWithIracingCallbackParams.ts deleted file mode 100644 index da801dfa2..000000000 --- a/apps/website/lib/types/generated/LoginWithIracingCallbackParams.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/MemberPaymentDto.ts b/apps/website/lib/types/generated/MemberPaymentDto.ts index c8e0c469e..bd3d4a199 100644 --- a/apps/website/lib/types/generated/MemberPaymentDto.ts +++ b/apps/website/lib/types/generated/MemberPaymentDto.ts @@ -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; diff --git a/apps/website/lib/types/generated/MembershipFeeDto.ts b/apps/website/lib/types/generated/MembershipFeeDto.ts index 172e04e41..40336aa24 100644 --- a/apps/website/lib/types/generated/MembershipFeeDto.ts +++ b/apps/website/lib/types/generated/MembershipFeeDto.ts @@ -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; } diff --git a/apps/website/lib/types/generated/NotificationSettingsDTO.ts b/apps/website/lib/types/generated/NotificationSettingsDTO.ts index 92e9ae736..b52a0ec69 100644 --- a/apps/website/lib/types/generated/NotificationSettingsDTO.ts +++ b/apps/website/lib/types/generated/NotificationSettingsDTO.ts @@ -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; } diff --git a/apps/website/lib/types/generated/PrivacySettingsDTO.ts b/apps/website/lib/types/generated/PrivacySettingsDTO.ts index 199a568d2..87f268d88 100644 --- a/apps/website/lib/types/generated/PrivacySettingsDTO.ts +++ b/apps/website/lib/types/generated/PrivacySettingsDTO.ts @@ -5,8 +5,8 @@ */ export interface PrivacySettingsDTO { - publicProfile: boolean; - showStats: boolean; - showActiveSponsorships: boolean; - allowDirectContact: boolean; + publicProfile: string; + showStats: string; + showActiveSponsorships: string; + allowDirectContact: string; } diff --git a/apps/website/lib/types/generated/PrizeDto.ts b/apps/website/lib/types/generated/PrizeDto.ts index 809aaec1d..21ca92b58 100644 --- a/apps/website/lib/types/generated/PrizeDto.ts +++ b/apps/website/lib/types/generated/PrizeDto.ts @@ -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; diff --git a/apps/website/lib/types/generated/RaceDTO.ts b/apps/website/lib/types/generated/RaceDTO.ts index b42e52a4f..ba7969f0b 100644 --- a/apps/website/lib/types/generated/RaceDTO.ts +++ b/apps/website/lib/types/generated/RaceDTO.ts @@ -8,4 +8,5 @@ export interface RaceDTO { id: string; name: string; date: string; + views: string; } diff --git a/apps/website/lib/types/generated/RacePenaltiesDTO.ts b/apps/website/lib/types/generated/RacePenaltiesDTO.ts deleted file mode 100644 index 926a7c978..000000000 --- a/apps/website/lib/types/generated/RacePenaltiesDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/RecordEngagementInputDTO.ts b/apps/website/lib/types/generated/RecordEngagementInputDTO.ts deleted file mode 100644 index 011f0ad1f..000000000 --- a/apps/website/lib/types/generated/RecordEngagementInputDTO.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RecordEngagementInputDTO { - eventType: string; - userId?: string; - metadata?: Record; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/RecordPageViewInputDTO.ts b/apps/website/lib/types/generated/RecordPageViewInputDTO.ts deleted file mode 100644 index 141bdf30a..000000000 --- a/apps/website/lib/types/generated/RecordPageViewInputDTO.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RecordPageViewInputDTO { - path: string; - userId?: string; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/SignupParams.ts b/apps/website/lib/types/generated/SignupParams.ts deleted file mode 100644 index 0eb63ffd9..000000000 --- a/apps/website/lib/types/generated/SignupParams.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/SponsorDashboardInvestmentDTO.ts b/apps/website/lib/types/generated/SponsorDashboardInvestmentDTO.ts index 6804a24da..fc50983f8 100644 --- a/apps/website/lib/types/generated/SponsorDashboardInvestmentDTO.ts +++ b/apps/website/lib/types/generated/SponsorDashboardInvestmentDTO.ts @@ -5,7 +5,7 @@ */ export interface SponsorDashboardInvestmentDTO { - activeSponsorships: number; - totalInvestment: number; - costPerThousandViews: number; + activeSponsorships: string; + totalInvestment: string; + costPerThousandViews: string; } diff --git a/apps/website/lib/types/generated/SponsorDashboardMetricsDTO.ts b/apps/website/lib/types/generated/SponsorDashboardMetricsDTO.ts index 025ecaf91..778708f5e 100644 --- a/apps/website/lib/types/generated/SponsorDashboardMetricsDTO.ts +++ b/apps/website/lib/types/generated/SponsorDashboardMetricsDTO.ts @@ -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; } diff --git a/apps/website/lib/types/generated/SponsorshipPricingItemDTO.ts b/apps/website/lib/types/generated/SponsorshipPricingItemDTO.ts index 6ba3dbd32..e210a0b69 100644 --- a/apps/website/lib/types/generated/SponsorshipPricingItemDTO.ts +++ b/apps/website/lib/types/generated/SponsorshipPricingItemDTO.ts @@ -7,6 +7,6 @@ export interface SponsorshipPricingItemDTO { id: string; level: string; - price: number; + price: string; currency: string; } diff --git a/apps/website/lib/types/generated/TransactionDto.ts b/apps/website/lib/types/generated/TransactionDto.ts index 42b88ab2e..db2ef8fee 100644 --- a/apps/website/lib/types/generated/TransactionDto.ts +++ b/apps/website/lib/types/generated/TransactionDto.ts @@ -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; } diff --git a/apps/website/lib/types/generated/UpdateAvatarOutputDTO.ts b/apps/website/lib/types/generated/UpdateAvatarOutputDTO.ts index 0dad0aeaa..ecdc7d107 100644 --- a/apps/website/lib/types/generated/UpdateAvatarOutputDTO.ts +++ b/apps/website/lib/types/generated/UpdateAvatarOutputDTO.ts @@ -5,5 +5,5 @@ */ export interface UpdateAvatarOutputDTO { - success: boolean; + success: string; } diff --git a/apps/website/lib/types/generated/UpdateTeamInputDTO.ts b/apps/website/lib/types/generated/UpdateTeamInputDTO.ts deleted file mode 100644 index d74ff0748..000000000 --- a/apps/website/lib/types/generated/UpdateTeamInputDTO.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/website/lib/types/generated/UploadMediaOutputDTO.ts b/apps/website/lib/types/generated/UploadMediaOutputDTO.ts index 95844afca..ca6275577 100644 --- a/apps/website/lib/types/generated/UploadMediaOutputDTO.ts +++ b/apps/website/lib/types/generated/UploadMediaOutputDTO.ts @@ -5,5 +5,5 @@ */ export interface UploadMediaOutputDTO { - success: boolean; + success: string; } diff --git a/apps/website/lib/types/generated/WalletDto.ts b/apps/website/lib/types/generated/WalletDto.ts index 819cb4fcf..ba4c38a63 100644 --- a/apps/website/lib/types/generated/WalletDto.ts +++ b/apps/website/lib/types/generated/WalletDto.ts @@ -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; diff --git a/docs/CONTRACT_TESTING.md b/docs/CONTRACT_TESTING.md new file mode 100644 index 000000000..4d0f8f01a --- /dev/null +++ b/docs/CONTRACT_TESTING.md @@ -0,0 +1,271 @@ +# Contract Testing Documentation + +## Overview + +This document describes the contract testing strategy for ensuring compatibility between the API (`apps/api`) and the website (`apps/website`) in the GridPilot monorepo. + +## Architecture + +The contract testing system consists of several layers: + +### 1. API Contract Validation +- **Location**: `apps/api/src/shared/testing/contractValidation.test.ts` +- **Purpose**: Validates that API DTOs are consistent and generate valid OpenAPI specs +- **Tests**: + - OpenAPI spec integrity + - DTO consistency + - Type generation integrity + - Contract compatibility + +### 2. Type Generation +- **Scripts**: + - `npm run api:generate-spec` - Generates OpenAPI spec from DTOs + - `npm run api:generate-types` - Generates TypeScript types for website + - `npm run api:sync-types` - Runs both in sequence +- **Output**: `apps/website/lib/types/generated/` + +### 3. Website Contract Consumption +- **Location**: `apps/website/lib/types/contractConsumption.test.ts` +- **Purpose**: Validates that website can properly consume generated types +- **Tests**: + - Generated types availability + - Type compatibility + - Service integration + - Error handling + +### 4. Compatibility Verification +- **Location**: `scripts/contract-compatibility.ts` +- **Purpose**: Detects breaking changes between type versions +- **Features**: + - Backup current types + - Generate new types + - Compare and detect changes + - Report breaking changes + +## Workflow + +### Local Development + +```bash +# Run full contract testing suite +npm run test:contracts + +# Or run individual steps: +npm run test:api:contracts # Validate API contracts +npm run api:generate-spec # Generate OpenAPI spec +npm run api:generate-types # Generate types +npm run test:contract:compatibility # Check compatibility +npm run website:type-check # Verify website types +``` + +### CI/CD Pipeline + +The GitHub Actions workflow (`.github/workflows/contract-testing.yml`) automatically: + +1. **On Pull Requests**: + - Runs all contract tests + - Validates API contracts + - Generates types + - Checks for breaking changes + - Verifies website type checking + - Uploads generated types as artifacts + - Comments results on PR + +2. **On Main Branch Push**: + - Runs all tests + - Generates and commits updated types + +## Breaking Change Detection + +The system detects several types of changes: + +### Breaking Changes (โŒ Fails CI) +- **Property Removal**: Required properties removed from DTOs +- **Type Changes**: Property types changed (e.g., `string` โ†’ `number`) +- **Required Field Addition**: New required fields added to existing DTOs + +### Non-Breaking Changes (โš ๏ธ Warning) +- **Property Addition**: Optional properties added +- **New DTOs**: New DTO types added +- **Documentation Changes**: Description updates + +### Removed Changes (โŒ Fails CI) +- **DTO Removal**: Entire DTO types removed +- **Property Removal**: Optional properties removed + +## Generated Types Structure + +The generated types follow this pattern: + +```typescript +/** + * 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 type { RelatedDTO } from './RelatedDTO'; + +export interface MyDTO { + id: string; + name: string; + optionalField?: string; + related?: RelatedDTO; +} +``` + +## Testing Strategy + +### API Layer Tests + +```typescript +// apps/api/src/shared/testing/contractValidation.test.ts +describe('API Contract Validation', () => { + it('should have valid OpenAPI spec', async () => { + // Validates spec structure + }); + + it('should have no circular references', async () => { + // Prevents infinite loops + }); + + it('should maintain backward compatibility', async () => { + // Ensures critical DTOs exist + }); +}); +``` + +### Website Layer Tests + +```typescript +// apps/website/lib/types/contractConsumption.test.ts +describe('Website Contract Consumption', () => { + it('should allow creating valid DTO objects', () => { + // Type-safe object creation + }); + + it('should work with service layer patterns', () => { + // Integration with services + }); + + it('should handle error cases', () => { + // Error handling patterns + }); +}); +``` + +## Integration Points + +### Service Layer Integration + +Services in the website can import and use generated types: + +```typescript +import type { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO'; +import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; + +class AvatarService { + async generateAvatar(input: RequestAvatarGenerationInputDTO): Promise { + const response = await apiClient.post('/avatar/generate', input); + return response.data; // Type-safe + } +} +``` + +### View Model Integration + +View models can transform DTOs for UI consumption: + +```typescript +import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; + +class RaceViewModel { + constructor(private dto: RaceDTO) {} + + get displayDate(): string { + return new Date(this.dto.startTime).toLocaleDateString(); + } +} +``` + +## Troubleshooting + +### Common Issues + +1. **Type Generation Fails** + - Check that DTOs have proper `@ApiProperty` decorators + - Verify OpenAPI spec is valid JSON + - Ensure all referenced types exist + +2. **Breaking Changes Detected** + - Review the change report + - Update website code to handle new types + - Consider versioning strategy for major changes + +3. **Website Type Errors** + - Run `npm run api:sync-types` to regenerate + - Check import paths in website code + - Verify TypeScript configuration + +### Debugging Steps + +1. **Check OpenAPI Spec**: + ```bash + npm run api:generate-spec + cat apps/api/openapi.json | jq '.components.schemas' + ``` + +2. **Compare Generated Types**: + ```bash + # Backup current types + cp -r apps/website/lib/types/generated /tmp/types-backup + + # Regenerate + npm run api:generate-types + + # Compare + diff -r /tmp/types-backup apps/website/lib/types/generated + ``` + +3. **Run Individual Tests**: + ```bash + # API tests + npm run test:api:contracts + + # Website type checking + npm run website:type-check + ``` + +## Best Practices + +### For API Developers + +1. **Always use `@ApiProperty` decorators** with proper types +2. **Mark optional fields explicitly** with `required: false` +3. **Use proper TypeScript types** (avoid `any`) +4. **Add descriptions** to DTO properties +5. **Test DTOs** with contract validation tests + +### For Website Developers + +1. **Import from generated types**, not manual types +2. **Use type assertions** when consuming API responses +3. **Handle optional fields** properly +4. **Run contract tests** before committing type changes +5. **Update view models** when DTOs change + +### For CI/CD + +1. **Run contract tests on every PR** +2. **Block merges on breaking changes** +3. **Generate types on main branch pushes** +4. **Upload artifacts for debugging** +5. **Comment results on PRs** + +## Future Enhancements + +- [ ] Add API versioning support +- [ ] Generate client SDKs from OpenAPI +- [ ] Add contract testing for WebSocket events +- [ ] Implement schema registry +- [ ] Add automated migration scripts for breaking changes \ No newline at end of file diff --git a/docs/CONTRACT_TESTING_QUICKSTART.md b/docs/CONTRACT_TESTING_QUICKSTART.md new file mode 100644 index 000000000..a03224bf4 --- /dev/null +++ b/docs/CONTRACT_TESTING_QUICKSTART.md @@ -0,0 +1,168 @@ +# Contract Testing Quick Start Guide + +## ๐Ÿš€ Quick Setup + +### 1. Run the Full Contract Test Suite +```bash +npm run test:contracts +``` + +This single command will: +- โœ… Validate API contracts +- โœ… Generate OpenAPI spec +- โœ… Generate TypeScript types +- โœ… Check for breaking changes +- โœ… Verify website type compatibility + +### 2. Individual Commands + +```bash +# Validate API contracts only +npm run test:api:contracts + +# Generate types (after making DTO changes) +npm run api:sync-types + +# Check compatibility (detect breaking changes) +npm run test:contract:compatibility + +# Verify website can consume types +npm run website:type-check +``` + +## ๐Ÿ“ What Gets Created + +### Generated Types +- **Location**: `apps/website/lib/types/generated/` +- **Files**: One `.ts` file per DTO (e.g., `RaceDTO.ts`, `DriverDTO.ts`) +- **Usage**: Import directly in website code + +### Test Files +- **API Tests**: `apps/api/src/shared/testing/contractValidation.test.ts` +- **Website Tests**: `apps/website/lib/types/contractConsumption.test.ts` + +### CI/CD +- **Workflow**: `.github/workflows/contract-testing.yml` +- **Triggers**: Pull requests and main branch pushes + +## ๐ŸŽฏ Common Workflows + +### Making API Changes + +1. **Update DTO in API**: + ```typescript + // apps/api/src/domain/race/dtos/RaceDTO.ts + export class RaceDTO { + @ApiProperty() + id: string; + + @ApiProperty() + name: string; + + @ApiProperty({ required: false }) + description?: string; // New optional field + } + ``` + +2. **Run contract tests**: + ```bash + npm run test:contracts + ``` + +3. **If tests pass**, commit your changes. The CI will regenerate types automatically. + +### Updating Website Code + +1. **Import generated types**: + ```typescript + import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; + + function RaceComponent({ race }: { race: RaceDTO }) { + return
{race.name}
; + } + ``` + +2. **TypeScript will catch errors** if contracts change. + +### Detecting Breaking Changes + +```bash +# This will show you exactly what changed +npm run test:contract:compatibility +``` + +Output example: +``` +๐Ÿšจ BREAKING CHANGES DETECTED: + โ€ข RaceDTO.status: Property status was removed (BREAKING) + +โŒ REMOVED: + โ€ข OldDTO: DTO OldDTO was removed + +โž• ADDED: + โ€ข NewDTO: New DTO NewDTO was added +``` + +## ๐Ÿ”ง Troubleshooting + +### "Cannot find module" errors +```bash +# Regenerate types +npm run api:sync-types +``` + +### Type generation fails +1. Check DTOs have `@ApiProperty` decorators +2. Verify OpenAPI spec is valid: `cat apps/api/openapi.json` +3. Run individual steps: + ```bash + npm run api:generate-spec + npm run api:generate-types + ``` + +### CI fails on breaking changes +- Review what changed +- Update website code to handle new types +- Or revert the breaking change if unintended + +## ๐Ÿ“‹ Checklist Before Committing + +- [ ] Run `npm run test:contracts` locally +- [ ] All tests pass +- [ ] No breaking changes (or they're intentional) +- [ ] Website code updated to handle new types +- [ ] Generated types are committed (if needed) + +## ๐ŸŽ“ Key Concepts + +### What is a "Contract"? +A contract is the agreement between API and website about what data looks like: +- DTO definitions +- Property types +- Required vs optional fields + +### What are "Breaking Changes"? +Changes that would break the website: +- Removing required fields +- Changing field types +- Adding required fields to existing DTOs + +### Why Generate Types? +- **Type Safety**: Catch errors at compile time +- **Auto-completion**: Better IDE experience +- **Documentation**: Types serve as living documentation +- **Consistency**: Single source of truth + +## ๐Ÿšจ Important Notes + +1. **Never manually edit generated files** - they're auto-generated +2. **Always run tests before committing** - prevents breaking changes +3. **The CI will regenerate types** - but local verification is faster +4. **Breaking changes need review** - consider versioning strategy + +## ๐Ÿ“š More Resources + +- Full documentation: `docs/CONTRACT_TESTING.md` +- API examples: `apps/api/src/shared/testing/contractValidation.test.ts` +- Website examples: `apps/website/lib/types/contractConsumption.test.ts` +- CI/CD workflow: `.github/workflows/contract-testing.yml` \ No newline at end of file diff --git a/package.json b/package.json index fd27a956c..6994398f6 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,11 @@ "api:sync-types": "npm run api:generate-spec && npm run api:generate-types", "api:test": "vitest run --config vitest.api.config.ts", "api:coverage": "vitest run --config vitest.api.config.ts --coverage", + "test:api:contracts": "vitest run --config vitest.api.config.ts apps/api/src/shared/testing/contractValidation.test.ts", + "test:contract:compatibility": "tsx scripts/contract-compatibility.ts", + "test:contracts": "tsx scripts/run-contract-tests.ts", + "test:website:types": "vitest run --config vitest.website.config.ts apps/website/lib/types/contractConsumption.test.ts", + "test:type-generation": "vitest run --config vitest.scripts.config.ts scripts/test/type-generation.test.ts", "build": "echo 'Build all packages placeholder - to be configured'", "chrome:debug": "open -a 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug", "companion:build": "npm run build --workspace=@gridpilot/companion", diff --git a/scripts/contract-compatibility.ts b/scripts/contract-compatibility.ts new file mode 100644 index 000000000..ddd18572f --- /dev/null +++ b/scripts/contract-compatibility.ts @@ -0,0 +1,288 @@ +#!/usr/bin/env tsx +/** + * Contract Compatibility Verification Script + * + * This script verifies that the API contracts are compatible with the website + * by running the type generation and then checking for breaking changes. + */ + +import { execSync } from 'child_process'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { glob } from 'glob'; + +interface OpenAPISchema { + type?: string; + format?: string; + $ref?: string; + items?: OpenAPISchema; + properties?: Record; + required?: string[]; + enum?: string[]; + nullable?: boolean; + description?: string; +} + +interface OpenAPISpec { + openapi: string; + info: { + title: string; + description: string; + version: string; + }; + paths: Record; + components: { + schemas: Record; + }; +} + +interface ContractChange { + type: 'added' | 'removed' | 'modified' | 'breaking'; + dto: string; + property?: string; + details: string; +} + +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + dim: '\x1b[2m' +}; + +async function runContractCompatibilityCheck(): Promise { + console.log(`${colors.blue}๐Ÿ” Running Contract Compatibility Check...${colors.reset}\n`); + + const apiRoot = path.join(__dirname, '../apps/api'); + const websiteRoot = path.join(__dirname, '../apps/website'); + const openapiPath = path.join(apiRoot, 'openapi.json'); + const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated'); + const backupDir = path.join(__dirname, '../.backup/contract-types'); + + // Step 1: Generate current OpenAPI spec + console.log(`${colors.yellow}1. Generating OpenAPI spec...${colors.reset}`); + try { + execSync('npm run api:generate-spec', { stdio: 'inherit' }); + } catch (error) { + console.error(`${colors.red}โŒ Failed to generate OpenAPI spec${colors.reset}`); + process.exit(1); + } + + // Step 2: Backup current generated types + console.log(`${colors.yellow}2. Backing up current generated types...${colors.reset}`); + await fs.mkdir(backupDir, { recursive: true }); + try { + const files = await fs.readdir(generatedTypesDir); + for (const file of files) { + if (file.endsWith('.ts')) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + await fs.writeFile(path.join(backupDir, file), content); + } + } + } catch (error) { + console.log(`${colors.yellow}โš ๏ธ No existing types to backup${colors.reset}`); + } + + // Step 3: Generate new types + console.log(`${colors.yellow}3. Generating new types...${colors.reset}`); + try { + execSync('npm run api:generate-types', { stdio: 'inherit' }); + } catch (error) { + console.error(`${colors.red}โŒ Failed to generate types${colors.reset}`); + process.exit(1); + } + + // Step 4: Compare and detect changes + console.log(`${colors.yellow}4. Analyzing contract changes...${colors.reset}`); + const changes = await detectContractChanges(backupDir, generatedTypesDir, openapiPath); + + // Step 5: Report results + console.log(`${colors.yellow}5. Reporting changes...${colors.reset}\n`); + await reportChanges(changes); + + // Step 6: Clean up backup + console.log(`${colors.yellow}6. Cleaning up...${colors.reset}`); + await fs.rm(backupDir, { recursive: true, force: true }); + + console.log(`\n${colors.green}โœ… Contract compatibility check completed!${colors.reset}`); +} + +async function detectContractChanges( + backupDir: string, + currentDir: string, + openapiPath: string +): Promise { + const changes: ContractChange[] = []; + + // Read OpenAPI spec + const specContent = await fs.readFile(openapiPath, 'utf-8'); + const spec: OpenAPISpec = JSON.parse(specContent); + const schemas = spec.components.schemas; + + // Get current and backup files + const currentFiles = await fs.readdir(currentDir); + const backupFiles = await fs.readdir(backupDir); + + const currentDTOs = currentFiles.filter(f => f.endsWith('.ts')).map(f => f.replace('.ts', '')); + const backupDTOs = backupFiles.filter(f => f.endsWith('.ts')).map(f => f.replace('.ts', '')); + + // Check for removed DTOs + for (const backupDTO of backupDTOs) { + if (!currentDTOs.includes(backupDTO)) { + changes.push({ + type: 'removed', + dto: backupDTO, + details: `DTO ${backupDTO} was removed` + }); + } + } + + // Check for added and modified DTOs + for (const currentDTO of currentDTOs) { + const currentPath = path.join(currentDir, `${currentDTO}.ts`); + const backupPath = path.join(backupDir, `${currentDTO}.ts`); + + const currentContent = await fs.readFile(currentPath, 'utf-8'); + const backupExists = backupDTOs.includes(currentDTO); + + if (!backupExists) { + changes.push({ + type: 'added', + dto: currentDTO, + details: `New DTO ${currentDTO} was added` + }); + } else { + const backupContent = await fs.readFile(backupPath, 'utf-8'); + + // Check for property changes + const schema = schemas[currentDTO]; + if (schema && schema.properties) { + const currentProps = extractProperties(currentContent); + const backupProps = extractProperties(backupContent); + + // Check for removed properties + for (const [propName, backupProp] of Object.entries(backupProps)) { + if (!currentProps[propName]) { + const isRequired = schema.required?.includes(propName); + changes.push({ + type: isRequired ? 'breaking' : 'modified', + dto: currentDTO, + property: propName, + details: `Property ${propName} was removed${isRequired ? ' (BREAKING)' : ''}` + }); + } + } + + // Check for added properties + for (const [propName, currentProp] of Object.entries(currentProps)) { + if (!backupProps[propName]) { + const isRequired = schema.required?.includes(propName); + changes.push({ + type: isRequired ? 'breaking' : 'added', + dto: currentDTO, + property: propName, + details: `Property ${propName} was added${isRequired ? ' (potentially breaking)' : ''}` + }); + } + } + + // Check for type changes + for (const [propName, currentProp] of Object.entries(currentProps)) { + if (backupProps[propName]) { + const backupProp = backupProps[propName]; + if (currentProp.type !== backupProp.type) { + changes.push({ + type: 'breaking', + dto: currentDTO, + property: propName, + details: `Property ${propName} type changed from ${backupProp.type} to ${currentProp.type} (BREAKING)` + }); + } + } + } + } + } + } + + return changes; +} + +function extractProperties(content: string): Record { + const properties: Record = {}; + + // Match property lines: propertyName?: type; + const propertyRegex = /^\s*(\w+)(\??):\s*([^;]+);/gm; + let match; + + while ((match = propertyRegex.exec(content)) !== null) { + const [, name, optional, type] = match; + properties[name] = { + type: type.trim(), + optional: !!optional + }; + } + + return properties; +} + +async function reportChanges(changes: ContractChange[]): Promise { + if (changes.length === 0) { + console.log(`${colors.green}โœ… No changes detected - contracts are stable${colors.reset}`); + return; + } + + const breaking = changes.filter(c => c.type === 'breaking'); + const modified = changes.filter(c => c.type === 'modified'); + const added = changes.filter(c => c.type === 'added'); + const removed = changes.filter(c => c.type === 'removed'); + + if (breaking.length > 0) { + console.log(`${colors.red}๐Ÿšจ BREAKING CHANGES DETECTED:${colors.reset}`); + breaking.forEach(change => { + console.log(` ${colors.red}โ€ข ${change.dto}${change.property ? '.' + change.property : ''}: ${change.details}${colors.reset}`); + }); + console.log(''); + } + + if (removed.length > 0) { + console.log(`${colors.red}โŒ REMOVED:${colors.reset}`); + removed.forEach(change => { + console.log(` ${colors.red}โ€ข ${change.dto}: ${change.details}${colors.reset}`); + }); + console.log(''); + } + + if (modified.length > 0) { + console.log(`${colors.yellow}โš ๏ธ MODIFIED:${colors.reset}`); + modified.forEach(change => { + console.log(` ${colors.yellow}โ€ข ${change.dto}.${change.property}: ${change.details}${colors.reset}`); + }); + console.log(''); + } + + if (added.length > 0) { + console.log(`${colors.green}โž• ADDED:${colors.reset}`); + added.forEach(change => { + console.log(` ${colors.green}โ€ข ${change.dto}${change.property ? '.' + change.property : ''}: ${change.details}${colors.reset}`); + }); + console.log(''); + } + + const totalChanges = changes.length; + console.log(`${colors.blue}๐Ÿ“Š Summary: ${totalChanges} total changes (${breaking.length} breaking, ${removed.length} removed, ${modified.length} modified, ${added.length} added)${colors.reset}`); + + if (breaking.length > 0) { + console.log(`\n${colors.red}โŒ Contract compatibility check FAILED due to breaking changes${colors.reset}`); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + runContractCompatibilityCheck().catch(error => { + console.error(`${colors.red}โŒ Error running contract compatibility check:${colors.reset}`, error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/generate-api-types.ts b/scripts/generate-api-types.ts index 4459b4a21..72dd6208a 100644 --- a/scripts/generate-api-types.ts +++ b/scripts/generate-api-types.ts @@ -64,18 +64,42 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string const schemaNames = Object.keys(schemas); + // Get existing files in output directory + let existingFiles: string[] = []; + try { + existingFiles = await fs.readdir(outputDir); + existingFiles = existingFiles.filter(f => f.endsWith('.ts')); + } catch (error) { + // Directory doesn't exist yet + } + // Generate individual files for each schema + const generatedFileNames: string[] = []; for (const schemaName of schemaNames) { const schema = schemas[schemaName]; + + // File name should match the schema name exactly const fileName = `${schemaName}.ts`; const filePath = path.join(outputDir, fileName); const fileContent = generateDtoFileContent(schemaName, schema, schemas); await fs.writeFile(filePath, fileContent); console.log(` โœ… Generated ${fileName}`); + generatedFileNames.push(fileName); + } + + // Clean up files that are no longer in the spec + const filesToRemove = existingFiles.filter(f => !generatedFileNames.includes(f)); + for (const file of filesToRemove) { + const filePath = path.join(outputDir, file); + await fs.unlink(filePath); + console.log(` ๐Ÿ—‘๏ธ Removed obsolete file: ${file}`); } console.log(`โœ… Generated ${schemaNames.length} individual DTO files at: ${outputDir}`); + if (filesToRemove.length > 0) { + console.log(`๐Ÿงน Cleaned up ${filesToRemove.length} obsolete files`); + } } function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record): string { @@ -101,7 +125,7 @@ function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Rec content += '\n'; } - // Generate interface + // Generate interface - use the schema name directly content += `export interface ${schemaName} {\n`; const properties = schema.properties || {}; diff --git a/scripts/generate-openapi-spec.ts b/scripts/generate-openapi-spec.ts index 5bdfdbf42..b3bed7918 100644 --- a/scripts/generate-openapi-spec.ts +++ b/scripts/generate-openapi-spec.ts @@ -86,12 +86,22 @@ async function processDTOFile(filePath: string, schemas: Record ${normalizedName}`); + + // Check for conflicts + if (schemas[normalizedName]) { + console.warn(` โš ๏ธ Conflict: ${normalizedName} already exists. Skipping duplicate from ${filePath}`); + continue; + } const schema = extractSchemaFromClassBody(classBody, content); if (schema && Object.keys(schema.properties || {}).length > 0) { - schemas[className] = schema; - console.log(` โœ… Added ${className} with ${Object.keys(schema.properties || {}).length} properties`); + schemas[normalizedName] = schema; + console.log(` โœ… Added ${normalizedName} with ${Object.keys(schema.properties || {}).length} properties`); } } } diff --git a/scripts/run-contract-tests.ts b/scripts/run-contract-tests.ts new file mode 100644 index 000000000..84bcfdad3 --- /dev/null +++ b/scripts/run-contract-tests.ts @@ -0,0 +1,85 @@ +#!/usr/bin/env tsx +/** + * Contract Testing Integration Script + * + * This script runs all contract tests in the correct order: + * 1. API contract validation + * 2. Type generation + * 3. Website contract consumption tests + * 4. Compatibility verification + */ + +import { execSync } from 'child_process'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + dim: '\x1b[2m' +}; + +async function runContractTests(): Promise { + console.log(`${colors.cyan}๐Ÿš€ Starting Contract Testing Suite${colors.reset}\n`); + + const steps = [ + { + name: 'API Contract Validation', + command: 'npm run test:api:contracts', + description: 'Validate API DTOs and OpenAPI spec integrity' + }, + { + name: 'Generate OpenAPI Spec', + command: 'npm run api:generate-spec', + description: 'Generate OpenAPI specification from DTOs' + }, + { + name: 'Generate TypeScript Types', + command: 'npm run api:generate-types', + description: 'Generate TypeScript types for website' + }, + { + name: 'Contract Compatibility Check', + command: 'npm run test:contract:compatibility', + description: 'Check for breaking changes in contracts' + }, + { + name: 'Website Type Checking', + command: 'npm run website:type-check', + description: 'Verify website can consume generated types' + } + ]; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + console.log(`${colors.yellow}${i + 1}/${steps.length} ${step.name}${colors.reset}`); + console.log(`${colors.dim} ${step.description}${colors.reset}`); + + try { + execSync(step.command, { + stdio: 'inherit', + env: { ...process.env, FORCE_COLOR: 'true' } + }); + console.log(`${colors.green} โœ… ${step.name} completed${colors.reset}\n`); + } catch (error) { + console.log(`${colors.red} โŒ ${step.name} failed${colors.reset}\n`); + console.log(`${colors.red}Contract testing suite failed at step: ${step.name}${colors.reset}`); + process.exit(1); + } + } + + console.log(`${colors.green}๐ŸŽ‰ All contract tests passed!${colors.reset}`); + console.log(`${colors.green}โœ… Contracts are compatible and validated${colors.reset}`); +} + +// Run if called directly +if (require.main === module) { + runContractTests().catch(error => { + console.error(`${colors.red}โŒ Contract testing suite failed:${colors.reset}`, error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/test/type-generation.test.ts b/scripts/test/type-generation.test.ts new file mode 100644 index 000000000..e82375040 --- /dev/null +++ b/scripts/test/type-generation.test.ts @@ -0,0 +1,258 @@ +/** + * Test suite for type generation script + * Validates that the type generation process works correctly + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { execSync } from 'child_process'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { glob } from 'glob'; + +describe('Type Generation Script', () => { + const apiRoot = path.join(__dirname, '../../apps/api'); + const websiteRoot = path.join(__dirname, '../../apps/website'); + const openapiPath = path.join(apiRoot, 'openapi.json'); + const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated'); + const backupDir = path.join(__dirname, '../../.backup/type-gen-test'); + + beforeAll(async () => { + // Backup existing generated types + await fs.mkdir(backupDir, { recursive: true }); + try { + const files = await fs.readdir(generatedTypesDir); + for (const file of files) { + if (file.endsWith('.ts')) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + await fs.writeFile(path.join(backupDir, file), content); + } + } + } catch (error) { + // No existing files to backup + } + }); + + afterAll(async () => { + // Restore backup + try { + const backupFiles = await fs.readdir(backupDir); + for (const file of backupFiles) { + if (file.endsWith('.ts')) { + const content = await fs.readFile(path.join(backupDir, file), 'utf-8'); + await fs.writeFile(path.join(generatedTypesDir, file), content); + } + } + } catch (error) { + // No backup to restore + } + // Clean up backup + await fs.rm(backupDir, { recursive: true, force: true }); + }); + + describe('OpenAPI Spec Generation', () => { + it('should generate valid OpenAPI spec', async () => { + // Run the spec generation + execSync('npm run api:generate-spec', { + cwd: path.join(__dirname, '../..'), + stdio: 'pipe' + }); + + // Check that spec exists and is valid JSON + const specContent = await fs.readFile(openapiPath, 'utf-8'); + expect(() => JSON.parse(specContent)).not.toThrow(); + + const spec = JSON.parse(specContent); + expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/); + expect(spec.components).toBeDefined(); + expect(spec.components.schemas).toBeDefined(); + }); + + it('should not have duplicate schema names with different casing', async () => { + const specContent = await fs.readFile(openapiPath, 'utf-8'); + const spec = JSON.parse(specContent); + const schemas = Object.keys(spec.components.schemas); + + // Check for duplicates with different casing + const lowerCaseMap = new Map(); + schemas.forEach(schema => { + const lower = schema.toLowerCase(); + if (!lowerCaseMap.has(lower)) { + lowerCaseMap.set(lower, []); + } + lowerCaseMap.get(lower)!.push(schema); + }); + + const duplicates = Array.from(lowerCaseMap.entries()) + .filter(([_, names]) => names.length > 1); + + expect(duplicates.length).toBe(0); + }); + + it('should generate spec with consistent naming', async () => { + const specContent = await fs.readFile(openapiPath, 'utf-8'); + const spec = JSON.parse(specContent); + const schemas = Object.keys(spec.components.schemas); + + // All schemas should follow DTO naming convention + const invalidNames = schemas.filter(name => !name.endsWith('DTO') && !name.endsWith('Dto')); + expect(invalidNames.length).toBe(0); + }); + }); + + describe('Type Generation', () => { + it('should generate TypeScript files for all schemas', async () => { + // Generate types + execSync('npm run api:generate-types', { + cwd: path.join(__dirname, '../..'), + stdio: 'pipe' + }); + + // Read generated files + const generatedFiles = await fs.readdir(generatedTypesDir); + const generatedDTOs = generatedFiles + .filter(f => f.endsWith('.ts')) + .map(f => f.replace('.ts', '')); + + // Read OpenAPI spec + const specContent = await fs.readFile(openapiPath, 'utf-8'); + const spec = JSON.parse(specContent); + const schemas = Object.keys(spec.components.schemas); + + // Most schemas should have corresponding generated files + // (allowing for some duplicates/conflicts that are intentionally skipped) + const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema)); + + // Should have at least 95% coverage + const coverage = (schemas.length - missingFiles.length) / schemas.length; + expect(coverage).toBeGreaterThan(0.95); + }); + + it('should generate files with correct interface names', async () => { + const files = await fs.readdir(generatedTypesDir); + const dtos = files.filter(f => f.endsWith('.ts')); + + for (const file of dtos) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + const interfaceName = file.replace('.ts', ''); + + // File should contain an interface (name might be normalized) + expect(content).toMatch(/export interface \w+\s*{/); + + // Should not have duplicate interface names in the same file + const interfaceMatches = content.match(/export interface (\w+)/g); + expect(interfaceMatches?.length).toBe(1); + } + }); + + it('should generate valid TypeScript syntax', async () => { + const files = await fs.readdir(generatedTypesDir); + const dtos = files.filter(f => f.endsWith('.ts')); + + for (const file of dtos) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + + // Basic syntax checks + expect(content).toContain('export interface'); + expect(content).toContain('{'); + expect(content).toContain('}'); + expect(content).toContain('Auto-generated DTO'); + + // Should not have syntax errors + expect(content).not.toMatch(/interface\s+\w+\s*\{\s*\}/); // Empty interfaces + expect(content).not.toContain('undefined;'); + expect(content).not.toContain('any;'); + } + }); + + it('should handle dependencies correctly', async () => { + const files = await fs.readdir(generatedTypesDir); + const dtos = files.filter(f => f.endsWith('.ts')); + + for (const file of dtos) { + const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); + const importMatches = content.match(/import type \{ (\w+) \} from '\.\/(\w+)';/g) || []; + + for (const importLine of importMatches) { + const match = importLine.match(/import type \{ (\w+) \} from '\.\/(\w+)';/); + if (match) { + const [, importedType, fromFile] = match; + + // Import type should match the file name + expect(importedType).toBe(fromFile); + + // The imported file should exist + const importedPath = path.join(generatedTypesDir, `${fromFile}.ts`); + const exists = await fs.access(importedPath).then(() => true).catch(() => false); + expect(exists).toBe(true); + } + } + } + }); + + it('should maintain consistent naming between OpenAPI and generated files', async () => { + const specContent = await fs.readFile(openapiPath, 'utf-8'); + const spec = JSON.parse(specContent); + const schemas = Object.keys(spec.components.schemas); + + const generatedFiles = await fs.readdir(generatedTypesDir); + const generatedDTOs = generatedFiles + .filter(f => f.endsWith('.ts')) + .map(f => f.replace('.ts', '')); + + // Check that most schemas have matching files (allowing for some edge cases) + const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema)); + const coverage = (schemas.length - missingFiles.length) / schemas.length; + expect(coverage).toBeGreaterThan(0.95); + + // Check that most files have matching schemas (allowing for normalization) + const extraFiles = generatedDTOs.filter(dto => !schemas.includes(dto)); + const extraCoverage = (generatedDTOs.length - extraFiles.length) / generatedDTOs.length; + expect(extraCoverage).toBeGreaterThan(0.95); + }); + }); + + describe('Integration', () => { + it('should generate types that can be imported without errors', async () => { + // Generate types first + execSync('npm run api:generate-types', { + cwd: path.join(__dirname, '../..'), + stdio: 'pipe' + }); + + // Try to import a few key DTOs + const testDTOs = [ + 'RaceDTO', + 'DriverDTO', + 'RequestAvatarGenerationInputDTO', + 'RequestAvatarGenerationOutputDTO' + ]; + + for (const dto of testDTOs) { + const filePath = path.join(generatedTypesDir, `${dto}.ts`); + const exists = await fs.access(filePath).then(() => true).catch(() => false); + + if (exists) { + const content = await fs.readFile(filePath, 'utf-8'); + // Should be valid TypeScript that can be parsed + expect(content).toContain(`export interface ${dto}`); + } + } + }); + + it('should handle the full generation workflow', async () => { + // Run complete workflow + execSync('npm run api:sync-types', { + cwd: path.join(__dirname, '../..'), + stdio: 'pipe' + }); + + // Verify both spec and types were generated + const specExists = await fs.access(openapiPath).then(() => true).catch(() => false); + expect(specExists).toBe(true); + + const files = await fs.readdir(generatedTypesDir); + const tsFiles = files.filter(f => f.endsWith('.ts')); + expect(tsFiles.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/vitest.scripts.config.ts b/vitest.scripts.config.ts new file mode 100644 index 000000000..dc1a50582 --- /dev/null +++ b/vitest.scripts.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + watch: false, + environment: 'node', + include: ['scripts/test/**/*.test.ts'], + exclude: ['node_modules/**', 'dist/**'], + }, + resolve: { + alias: { + '@': resolve(__dirname, './apps/website'), + '@core': resolve(__dirname, './core'), + }, + }, +}); \ No newline at end of file diff --git a/vitest.website.config.ts b/vitest.website.config.ts new file mode 100644 index 000000000..df13d0c01 --- /dev/null +++ b/vitest.website.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + watch: false, + environment: 'node', + include: ['apps/website/lib/types/**/*.test.ts'], + exclude: ['node_modules/**', 'apps/website/.next/**', 'dist/**'], + typecheck: { + enabled: true, + checker: 'tsc', + include: ['apps/website/lib/types/**/*.test.ts'] + } + }, + resolve: { + alias: { + '@': resolve(__dirname, './apps/website'), + '@core': resolve(__dirname, './core'), + }, + }, +}); \ No newline at end of file