From 34eae531844988f17e62b6316c71195e4ceffe6e Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 23 Jan 2026 13:00:00 +0100 Subject: [PATCH] integration tests cleanup --- tests/contracts/api-website-contract.test.ts | 408 -------- .../integration/website-di-container.test.ts | 339 ------- .../nightly/website/website-pages.e2e.test.ts | 829 ---------------- tests/smoke/website-ssr.test.ts | 134 --- tests/structure/PackageDependencies.test.ts | 281 ------ tests/unit/website/BaseApiClient.test.ts | 70 -- tests/unit/website/FeatureFlagHelpers.test.ts | 66 -- .../LeagueDetailViewDataBuilder.test.ts | 827 ---------------- .../LeagueScheduleViewDataBuilder.test.ts | 386 -------- .../LeagueStandingsViewDataBuilder.test.ts | 541 ---------- .../website/LeaguesViewDataBuilder.test.ts | 932 ------------------ tests/unit/website/RouteConfig.test.ts | 70 -- .../unit/website/WebsiteRouteManager.test.ts | 73 -- tests/unit/website/apiBaseUrl.test.ts | 75 -- 14 files changed, 5031 deletions(-) delete mode 100644 tests/contracts/api-website-contract.test.ts delete mode 100644 tests/integration/website-di-container.test.ts delete mode 100644 tests/nightly/website/website-pages.e2e.test.ts delete mode 100644 tests/smoke/website-ssr.test.ts delete mode 100644 tests/structure/PackageDependencies.test.ts delete mode 100644 tests/unit/website/BaseApiClient.test.ts delete mode 100644 tests/unit/website/FeatureFlagHelpers.test.ts delete mode 100644 tests/unit/website/LeagueDetailViewDataBuilder.test.ts delete mode 100644 tests/unit/website/LeagueScheduleViewDataBuilder.test.ts delete mode 100644 tests/unit/website/LeagueStandingsViewDataBuilder.test.ts delete mode 100644 tests/unit/website/LeaguesViewDataBuilder.test.ts delete mode 100644 tests/unit/website/RouteConfig.test.ts delete mode 100644 tests/unit/website/WebsiteRouteManager.test.ts delete mode 100644 tests/unit/website/apiBaseUrl.test.ts diff --git a/tests/contracts/api-website-contract.test.ts b/tests/contracts/api-website-contract.test.ts deleted file mode 100644 index ff907d2ba..000000000 --- a/tests/contracts/api-website-contract.test.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * 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 * as os from 'os'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; - -interface OpenAPISchema { - type?: string; - format?: string; - $ref?: string; - items?: OpenAPISchema; - properties?: Record; - required?: string[]; - enum?: string[]; - nullable?: boolean; - description?: string; - default?: unknown; -} - -interface OpenAPISpec { - openapi: string; - info: { - title: string; - description: string; - version: string; - }; - paths: Record; - components: { - schemas: Record; - }; -} - -describe('API Contract Validation', () => { - const apiRoot = path.join(__dirname, '../..'); // /Users/marcmintel/Projects/gridpilot - const openapiPath = path.join(apiRoot, 'apps/api/openapi.json'); - const generatedTypesDir = path.join(apiRoot, 'apps/website/lib/types/generated'); - const execFileAsync = promisify(execFile); - - 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('committed openapi.json should match generator output', async () => { - const repoRoot = apiRoot; // Already at the repo root - - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gridpilot-openapi-')); - const generatedOpenapiPath = path.join(tmpDir, 'openapi.json'); - - await execFileAsync( - 'npx', - ['--no-install', 'tsx', 'scripts/generate-openapi-spec.ts', '--output', generatedOpenapiPath], - { cwd: repoRoot, maxBuffer: 20 * 1024 * 1024 }, - ); - - const committed: OpenAPISpec = JSON.parse(await fs.readFile(openapiPath, 'utf-8')); - const generated: OpenAPISpec = JSON.parse(await fs.readFile(generatedOpenapiPath, 'utf-8')); - - expect(generated).toEqual(committed); - }); - - it('should include real HTTP paths for known routes', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - - const pathKeys = Object.keys(spec.paths ?? {}); - expect(pathKeys.length).toBeGreaterThan(0); - - // A couple of stable routes to detect "empty/stale" specs. - expect(spec.paths['/drivers/leaderboard']).toBeDefined(); - expect(spec.paths['/dashboard/overview']).toBeDefined(); - - // Sanity-check the operation objects exist (method keys are lowercase in OpenAPI). - expect(spec.paths['/drivers/leaderboard'].get).toBeDefined(); - expect(spec.paths['/dashboard/overview'].get).toBeDefined(); - }); - - it('should include league schedule publish/unpublish endpoints and published state', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - - expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/publish']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/publish'].post).toBeDefined(); - - expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/unpublish']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/seasons/{seasonId}/schedule/unpublish'].post).toBeDefined(); - - const scheduleSchema = spec.components.schemas['LeagueScheduleDTO']; - if (!scheduleSchema) { - throw new Error('Expected LeagueScheduleDTO schema to be present in OpenAPI spec'); - } - - expect(scheduleSchema.properties?.published).toBeDefined(); - expect(scheduleSchema.properties?.published?.type).toBe('boolean'); - expect(scheduleSchema.required ?? []).toContain('published'); - }); - - it('should include league roster admin read endpoints and schemas', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - - expect(spec.paths['/leagues/{leagueId}/admin/roster/members']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/admin/roster/members'].get).toBeDefined(); - - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests'].get).toBeDefined(); - - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/approve']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/approve'].post).toBeDefined(); - - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/reject']).toBeDefined(); - expect(spec.paths['/leagues/{leagueId}/admin/roster/join-requests/{joinRequestId}/reject'].post).toBeDefined(); - - const memberSchema = spec.components.schemas['LeagueRosterMemberDTO']; - if (!memberSchema) { - throw new Error('Expected LeagueRosterMemberDTO schema to be present in OpenAPI spec'); - } - - expect(memberSchema.properties?.driverId).toBeDefined(); - expect(memberSchema.properties?.role).toBeDefined(); - expect(memberSchema.properties?.joinedAt).toBeDefined(); - expect(memberSchema.required ?? []).toContain('driverId'); - expect(memberSchema.required ?? []).toContain('role'); - expect(memberSchema.required ?? []).toContain('joinedAt'); - expect(memberSchema.required ?? []).toContain('driver'); - - const joinRequestSchema = spec.components.schemas['LeagueRosterJoinRequestDTO']; - if (!joinRequestSchema) { - throw new Error('Expected LeagueRosterJoinRequestDTO schema to be present in OpenAPI spec'); - } - - expect(joinRequestSchema.properties?.id).toBeDefined(); - expect(joinRequestSchema.properties?.leagueId).toBeDefined(); - expect(joinRequestSchema.properties?.driverId).toBeDefined(); - expect(joinRequestSchema.properties?.requestedAt).toBeDefined(); - expect(joinRequestSchema.required ?? []).toContain('id'); - expect(joinRequestSchema.required ?? []).toContain('leagueId'); - expect(joinRequestSchema.required ?? []).toContain('driverId'); - expect(joinRequestSchema.required ?? []).toContain('requestedAt'); - expect(joinRequestSchema.required ?? []).toContain('driver'); - }); - - 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 generated DTO files for critical schemas', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - - const generatedFiles = await fs.readdir(generatedTypesDir); - const generatedDTOs = generatedFiles - .filter(f => f.endsWith('.ts')) - .map(f => f.replace('.ts', '')); - - // We intentionally do NOT require a 1:1 mapping for *all* schemas here. - // OpenAPI generation and type generation can be run as separate steps, - // and new schemas should not break API contract validation by themselves. - const criticalDTOs = [ - 'RequestAvatarGenerationInputDTO', - 'RequestAvatarGenerationOutputDTO', - 'UploadMediaInputDTO', - 'UploadMediaOutputDTO', - 'RaceDTO', - 'DriverDTO', - ]; - - for (const dtoName of criticalDTOs) { - expect(spec.components.schemas[dtoName]).toBeDefined(); - expect(generatedDTOs).toContain(dtoName); - } - }); - - it('should have consistent property types between DTOs and schemas', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - const schemas = spec.components.schemas; - - 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') && !f.endsWith('.test.ts')); - - for (const file of dtos) { - const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8'); - - // `index.ts` is a generated barrel file (no interfaces). - if (file === 'index.ts') { - expect(content).toContain('export type {'); - expect(content).toContain("from './"); - continue; - } - - // Basic TypeScript syntax checks (DTO interfaces) - 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') && !f.endsWith('.test.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 [, schema] of Object.entries(schemas)) { - const required = new Set(schema.required ?? []); - if (!schema.properties) continue; - - for (const [propName, propSchema] of Object.entries(schema.properties)) { - if (!propSchema.nullable) continue; - - // In OpenAPI 3.0, a `nullable: true` property should not be listed as required, - // otherwise downstream generators can't represent it safely. - expect(required.has(propName)).toBe(false); - } - } - }); - - it('should have no empty string defaults for avatar/logo URLs', async () => { - const content = await fs.readFile(openapiPath, 'utf-8'); - const spec: OpenAPISpec = JSON.parse(content); - const schemas = spec.components.schemas; - - // Check DTOs that should use URL|null pattern - const mediaRelatedDTOs = [ - 'GetAvatarOutputDTO', - 'UpdateAvatarInputDTO', - 'DashboardDriverSummaryDTO', - 'DriverProfileDriverSummaryDTO', - 'DriverLeaderboardItemDTO', - 'TeamListItemDTO', - 'LeagueSummaryDTO', - 'SponsorDTO', - ]; - - for (const dtoName of mediaRelatedDTOs) { - const schema = schemas[dtoName]; - if (!schema || !schema.properties) continue; - - // Check for avatarUrl, logoUrl properties - for (const [propName, propSchema] of Object.entries(schema.properties)) { - if (propName === 'avatarUrl' || propName === 'logoUrl') { - // Should be string type, nullable (no empty string defaults) - expect(propSchema.type).toBe('string'); - expect(propSchema.nullable).toBe(true); - // Should not have default value of empty string - if (propSchema.default !== undefined) { - expect(propSchema.default).not.toBe(''); - } - } - } - } - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/website-di-container.test.ts b/tests/integration/website-di-container.test.ts deleted file mode 100644 index 3b5350260..000000000 --- a/tests/integration/website-di-container.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; -import { createContainer, ContainerManager } from '../../apps/website/lib/di/container'; -import { - SESSION_SERVICE_TOKEN, - LEAGUE_MEMBERSHIP_SERVICE_TOKEN, - LEAGUE_SERVICE_TOKEN, - AUTH_SERVICE_TOKEN, - DRIVER_SERVICE_TOKEN, - TEAM_SERVICE_TOKEN, - RACE_SERVICE_TOKEN, - DASHBOARD_SERVICE_TOKEN, - LOGGER_TOKEN, - CONFIG_TOKEN, - LEAGUE_API_CLIENT_TOKEN, - AUTH_API_CLIENT_TOKEN, - DRIVER_API_CLIENT_TOKEN, - TEAM_API_CLIENT_TOKEN, - RACE_API_CLIENT_TOKEN, -} from '../../apps/website/lib/di/tokens'; - -// Define minimal service interfaces for testing -interface MockSessionService { - getSession: () => Promise; -} - -interface MockAuthService { - login: (email: string, password: string) => Promise; -} - -interface MockLeagueService { - getAllLeagues: () => Promise; -} - -interface MockLeagueMembershipService { - getLeagueMemberships: (userId: string) => Promise; -} - -interface ConfigFunction { - (): string; -} - -/** - * Integration test for website DI container - */ -describe('Website DI Container Integration', () => { - let originalEnv: NodeJS.ProcessEnv; - - beforeAll(() => { - originalEnv = { ...process.env }; - process.env.NODE_ENV = 'test'; - }); - - afterAll(() => { - process.env = originalEnv; - ContainerManager.getInstance().dispose(); - }); - - beforeEach(() => { - // Clean up all API URL env vars before each test - delete process.env.API_BASE_URL; - delete process.env.NEXT_PUBLIC_API_BASE_URL; - ContainerManager.getInstance().dispose(); - }); - - afterEach(() => { - // Clean up after each test - delete process.env.API_BASE_URL; - delete process.env.NEXT_PUBLIC_API_BASE_URL; - ContainerManager.getInstance().dispose(); - }); - - describe('Basic Container Functionality', () => { - it('creates container successfully', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - expect(container).toBeDefined(); - expect(container).not.toBeNull(); - }); - - it('resolves core services without errors', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - expect(() => container.get(LOGGER_TOKEN)).not.toThrow(); - expect(() => container.get(CONFIG_TOKEN)).not.toThrow(); - - const logger = container.get(LOGGER_TOKEN); - const config = container.get(CONFIG_TOKEN); - - expect(logger).toBeDefined(); - expect(config).toBeDefined(); - }); - - it('resolves API clients without errors', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - const apiClients = [ - LEAGUE_API_CLIENT_TOKEN, - AUTH_API_CLIENT_TOKEN, - DRIVER_API_CLIENT_TOKEN, - TEAM_API_CLIENT_TOKEN, - RACE_API_CLIENT_TOKEN, - ]; - - for (const token of apiClients) { - expect(() => container.get(token)).not.toThrow(); - const client = container.get(token); - expect(client).toBeDefined(); - } - }); - - it('resolves auth services including SessionService', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - expect(() => container.get(SESSION_SERVICE_TOKEN)).not.toThrow(); - expect(() => container.get(AUTH_SERVICE_TOKEN)).not.toThrow(); - - const sessionService = container.get(SESSION_SERVICE_TOKEN); - const authService = container.get(AUTH_SERVICE_TOKEN); - - expect(sessionService).toBeDefined(); - expect(authService).toBeDefined(); - expect(typeof sessionService.getSession).toBe('function'); - expect(typeof authService.login).toBe('function'); - }); - - it('resolves league services including LeagueMembershipService', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - expect(() => container.get(LEAGUE_SERVICE_TOKEN)).not.toThrow(); - expect(() => container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)).not.toThrow(); - - const leagueService = container.get(LEAGUE_SERVICE_TOKEN); - const membershipService = container.get(LEAGUE_MEMBERSHIP_SERVICE_TOKEN); - - expect(leagueService).toBeDefined(); - expect(membershipService).toBeDefined(); - expect(typeof leagueService.getAllLeagues).toBe('function'); - expect(typeof membershipService.getLeagueMemberships).toBe('function'); - }); - - it('resolves domain services without errors', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - expect(() => container.get(DRIVER_SERVICE_TOKEN)).not.toThrow(); - expect(() => container.get(TEAM_SERVICE_TOKEN)).not.toThrow(); - expect(() => container.get(RACE_SERVICE_TOKEN)).not.toThrow(); - expect(() => container.get(DASHBOARD_SERVICE_TOKEN)).not.toThrow(); - - const driverService = container.get(DRIVER_SERVICE_TOKEN); - const teamService = container.get(TEAM_SERVICE_TOKEN); - const raceService = container.get(RACE_SERVICE_TOKEN); - const dashboardService = container.get(DASHBOARD_SERVICE_TOKEN); - - expect(driverService).toBeDefined(); - expect(teamService).toBeDefined(); - expect(raceService).toBeDefined(); - expect(dashboardService).toBeDefined(); - }); - - it('resolves all services in a single operation', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - const tokens = [ - SESSION_SERVICE_TOKEN, - LEAGUE_MEMBERSHIP_SERVICE_TOKEN, - LEAGUE_SERVICE_TOKEN, - AUTH_SERVICE_TOKEN, - DRIVER_SERVICE_TOKEN, - TEAM_SERVICE_TOKEN, - RACE_SERVICE_TOKEN, - DASHBOARD_SERVICE_TOKEN, - LOGGER_TOKEN, - CONFIG_TOKEN, - ]; - - const services = tokens.map(token => { - try { - return container.get(token); - } catch (error) { - throw new Error(`Failed to resolve token ${token.toString()}: ${error.message}`); - } - }); - - expect(services.length).toBe(tokens.length); - services.forEach((service) => { - expect(service).toBeDefined(); - expect(service).not.toBeNull(); - }); - }); - - it('throws clear error for non-existent bindings', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - const fakeToken = Symbol.for('Non.Existent.Service'); - expect(() => container.get(fakeToken)).toThrow(); - }); - }); - - describe('SSR Dynamic Environment Variables', () => { - it('config binding is a function (not a string)', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const container = createContainer(); - const config = container.get(CONFIG_TOKEN); - - // Config should be a function that can be called - expect(typeof config).toBe('function'); - - // Should be callable - const configFn = config as ConfigFunction; - expect(() => configFn()).not.toThrow(); - }); - - it('config function returns current environment value', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const container = createContainer(); - const getConfig = container.get(CONFIG_TOKEN) as ConfigFunction; - - const configValue = getConfig(); - // Should return some value (could be fallback in test env) - expect(typeof configValue).toBe('string'); - expect(configValue.length).toBeGreaterThan(0); - }); - - it('multiple containers share the same config behavior', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const container1 = createContainer(); - const container2 = createContainer(); - - const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction; - const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction; - - // Both should be functions - expect(typeof config1).toBe('function'); - expect(typeof config2).toBe('function'); - - // Both should return the same type of value - expect(typeof config1()).toBe(typeof config2()); - }); - - it('ContainerManager singleton behavior', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const manager = ContainerManager.getInstance(); - const container1 = manager.getContainer(); - const container2 = manager.getContainer(); - - // Should be same instance - expect(container1).toBe(container2); - - const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction; - const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction; - - // Both should be functions - expect(typeof config1).toBe('function'); - expect(typeof config2).toBe('function'); - }); - - it('API clients work with dynamic config', () => { - process.env.API_BASE_URL = 'http://api-test:3001'; - - const container = createContainer(); - const leagueClient = container.get(LEAGUE_API_CLIENT_TOKEN); - - expect(leagueClient).toBeDefined(); - expect(leagueClient).not.toBeNull(); - expect(typeof leagueClient).toBe('object'); - }); - - it('dispose clears singleton instance', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const manager = ContainerManager.getInstance(); - const container1 = manager.getContainer(); - - manager.dispose(); - - const container2 = manager.getContainer(); - - // Should be different instances after dispose - expect(container1).not.toBe(container2); - }); - - it('services work after container recreation', () => { - process.env.API_BASE_URL = 'http://test:3001'; - - const container1 = createContainer(); - const config1 = container1.get(CONFIG_TOKEN) as ConfigFunction; - - ContainerManager.getInstance().dispose(); - - const container2 = createContainer(); - const config2 = container2.get(CONFIG_TOKEN) as ConfigFunction; - - // Both should be functions - expect(typeof config1).toBe('function'); - expect(typeof config2).toBe('function'); - }); - }); - - describe('SSR Boot Safety', () => { - it('resolves all tokens required for SSR entry rendering', () => { - process.env.API_BASE_URL = 'http://localhost:3001'; - const container = createContainer(); - - // Tokens typically used in SSR (middleware, layouts, initial page loads) - const ssrTokens = [ - LOGGER_TOKEN, - CONFIG_TOKEN, - SESSION_SERVICE_TOKEN, - AUTH_SERVICE_TOKEN, - LEAGUE_SERVICE_TOKEN, - DRIVER_SERVICE_TOKEN, - DASHBOARD_SERVICE_TOKEN, - // API clients are often resolved by services - AUTH_API_CLIENT_TOKEN, - LEAGUE_API_CLIENT_TOKEN, - ]; - - for (const token of ssrTokens) { - try { - const service = container.get(token); - expect(service, `Failed to resolve ${token.toString()}`).toBeDefined(); - } catch (error) { - throw new Error(`SSR Boot Safety Failure: Could not resolve ${token.toString()}. Error: ${error.message}`); - } - } - }); - }); -}); diff --git a/tests/nightly/website/website-pages.e2e.test.ts b/tests/nightly/website/website-pages.e2e.test.ts deleted file mode 100644 index 2e13a9dfe..000000000 --- a/tests/nightly/website/website-pages.e2e.test.ts +++ /dev/null @@ -1,829 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture'; -import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager'; -import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager'; -import { fetchFeatureFlags, getEnabledFlags, isFeatureEnabled } from '../../shared/website/FeatureFlagHelpers'; - -const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; -const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101'; - -// Wait for API to be ready with seeded data before running tests -test.beforeAll(async ({ request }) => { - const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101'; - - console.log('[SETUP] Waiting for API to be ready...'); - - // Poll the API until it returns data (indicating seeding is complete) - const maxAttempts = 60; - const interval = 1000; // 1 second - - for (let i = 0; i < maxAttempts; i++) { - try { - // Try to fetch total drivers count - this endpoint should return > 0 after seeding - const response = await request.get(`${API_BASE_URL}/drivers/total-drivers`); - - if (response.ok()) { - const data = await response.json(); - - // Check if we have actual drivers (count > 0) - if (data && data.totalDrivers && data.totalDrivers > 0) { - console.log(`[SETUP] API is ready with ${data.totalDrivers} drivers`); - return; - } - } - - console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: API not ready yet (status: ${response.status()})`); - } catch (error) { - console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: ${error.message}`); - } - - // Wait before next attempt - await new Promise(resolve => setTimeout(resolve, interval)); - } - - throw new Error('[SETUP] API failed to become ready with seeded data within timeout'); -}); - -/** - * Helper to fetch feature flags from the API - * Uses Playwright request context for compatibility across environments - */ -async function fetchFeatureFlagsWrapper(request: import('@playwright/test').APIRequestContext) { - return fetchFeatureFlags( - async (url) => { - const response = await request.get(url); - return { - ok: response.ok(), - json: () => response.json(), - status: response.status() - }; - }, - API_BASE_URL - ); -} - -test.describe('Website Pages - TypeORM Integration', () => { - let routeManager: WebsiteRouteManager; - - test.beforeEach(() => { - routeManager = new WebsiteRouteManager(); - }); - - test('website loads and connects to API', async ({ page }) => { - // Test that the website loads - const response = await page.goto(WEBSITE_BASE_URL); - expect(response?.ok()).toBe(true); - - // Check that the page renders (body is visible) - await expect(page.locator('body')).toBeVisible(); - }); - - test('all routes from RouteConfig are discoverable', async () => { - expect(() => routeManager.getWebsiteRouteInventory()).not.toThrow(); - }); - - test('public routes are accessible without authentication', async ({ page }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const publicRoutes = routes.filter(r => r.access === 'public').slice(0, 5); - - for (const route of publicRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - const response = await page.goto(`${WEBSITE_BASE_URL}${path}`); - const status = response?.status(); - const finalUrl = page.url(); - - console.log(`[TEST DEBUG] Public route - Path: ${path}, Status: ${status}, Final URL: ${finalUrl}`); - if (status === 500) { - console.log(`[TEST DEBUG] 500 error on ${path} - Page title: ${await page.title()}`); - } - - // The /500 error page intentionally returns 500 status - // All other routes should load successfully or show 404 - if (path === '/500') { - expect(response?.status()).toBe(500); - } else { - expect(response?.ok() || response?.status() === 404).toBeTruthy(); - } - } - }); - - test('protected routes redirect unauthenticated users to login', async ({ page }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const protectedRoutes = routes.filter(r => r.access !== 'public').slice(0, 3); - - for (const route of protectedRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - await page.goto(`${WEBSITE_BASE_URL}${path}`); - - const currentUrl = new URL(page.url()); - expect(currentUrl.pathname).toBe('/auth/login'); - expect(currentUrl.searchParams.get('returnTo')).toBe(path); - } - }); - - test('admin routes require admin role', async ({ browser, request }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const adminRoutes = routes.filter(r => r.access === 'admin').slice(0, 2); - - for (const route of adminRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - - // Regular auth user should be redirected to their home page (dashboard) - { - const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); - try { - const response = await auth.page.goto(`${WEBSITE_BASE_URL}${path}`); - const finalUrl = auth.page.url(); - console.log(`[TEST DEBUG] Admin route test - Path: ${path}`); - console.log(`[TEST DEBUG] Response status: ${response?.status()}`); - console.log(`[TEST DEBUG] Final URL: ${finalUrl}`); - console.log(`[TEST DEBUG] Page title: ${await auth.page.title()}`); - expect(auth.page.url().includes('dashboard')).toBeTruthy(); - } finally { - try { - await auth.context.close(); - } catch (e) { - // Ignore context closing errors in test environment - console.log(`[TEST DEBUG] Context close error (ignored): ${e.message}`); - } - } - } - - // Admin user should have access - { - const admin = await WebsiteAuthManager.createAuthContext(browser, request, 'admin'); - try { - await admin.page.goto(`${WEBSITE_BASE_URL}${path}`); - expect(admin.page.url().includes(path)).toBeTruthy(); - } finally { - try { - await admin.context.close(); - } catch (e) { - // Ignore context closing errors in test environment - console.log(`[TEST DEBUG] Context close error (ignored): ${e.message}`); - } - } - } - } - }); - - test('sponsor routes require sponsor role', async ({ browser, request }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const sponsorRoutes = routes.filter(r => r.access === 'sponsor').slice(0, 2); - - for (const route of sponsorRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - - // Regular auth user should be redirected to their home page (dashboard) - { - const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); - await auth.page.goto(`${WEBSITE_BASE_URL}${path}`); - const finalUrl = auth.page.url(); - console.log(`[DEBUG] Final URL: ${finalUrl}`); - console.log(`[DEBUG] Includes 'dashboard': ${finalUrl.includes('dashboard')}`); - expect(finalUrl.includes('dashboard')).toBeTruthy(); - await auth.context.close(); - } - - // Sponsor user should have access - { - const sponsor = await WebsiteAuthManager.createAuthContext(browser, request, 'sponsor'); - await sponsor.page.goto(`${WEBSITE_BASE_URL}${path}`); - expect(sponsor.page.url().includes(path)).toBeTruthy(); - await sponsor.context.close(); - } - } - }); - - test('auth routes redirect authenticated users away', async ({ browser, request }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const authRoutes = routes.filter(r => r.access === 'auth').slice(0, 2); - - for (const route of authRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - - const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); - await auth.page.goto(`${WEBSITE_BASE_URL}${path}`); - - // Should redirect to dashboard or stay on the page - const currentUrl = auth.page.url(); - expect(currentUrl.includes('dashboard') || currentUrl.includes(path)).toBeTruthy(); - - await auth.context.close(); - } - }); - - test('parameterized routes handle edge cases', async ({ page }) => { - const edgeCases = routeManager.getParamEdgeCases(); - - for (const route of edgeCases) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - const response = await page.goto(`${WEBSITE_BASE_URL}${path}`); - - // Client-side pages return 200 even when data doesn't exist - // They show error messages in the UI instead of HTTP 404 - // This is expected behavior for CSR pages in Next.js - if (route.allowNotFound) { - const status = response?.status(); - expect([200, 404, 500].includes(status ?? 0)).toBeTruthy(); - - // If it's 200, verify error message is shown in the UI - if (status === 200) { - const bodyText = await page.textContent('body'); - const hasErrorMessage = bodyText?.includes('not found') || - bodyText?.includes('doesn\'t exist') || - bodyText?.includes('Error'); - expect(hasErrorMessage).toBeTruthy(); - } - } - } - }); - - test('no console or page errors on critical routes', async ({ page }) => { - const faultRoutes = routeManager.getFaultInjectionRoutes(); - - for (const route of faultRoutes) { - const capture = new ConsoleErrorCapture(page); - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - - await page.goto(`${WEBSITE_BASE_URL}${path}`); - await page.waitForTimeout(500); - - const errors = capture.getErrors(); - - // Filter out known/expected errors - const unexpectedErrors = errors.filter(error => { - const msg = error.message.toLowerCase(); - // Filter out hydration warnings and other expected Next.js warnings - return !msg.includes('hydration') && - !msg.includes('text content does not match') && - !msg.includes('warning:') && - !msg.includes('download the react devtools') && - !msg.includes('connection refused') && - !msg.includes('failed to load resource') && - !msg.includes('network error') && - !msg.includes('cors') && - !msg.includes('react does not recognize the `%s` prop on a dom element'); - }); - - // Check for critical runtime errors that should never occur - const criticalErrors = errors.filter(error => { - const msg = error.message.toLowerCase(); - return msg.includes('no queryclient set') || - msg.includes('use queryclientprovider') || - msg.includes('console.groupcollapsed is not a function') || - msg.includes('console.groupend is not a function'); - }); - - if (unexpectedErrors.length > 0) { - console.log(`[TEST DEBUG] Unexpected errors on ${path}:`, unexpectedErrors); - } - - if (criticalErrors.length > 0) { - console.log(`[TEST DEBUG] CRITICAL errors on ${path}:`, criticalErrors); - throw new Error(`Critical runtime errors on ${path}: ${JSON.stringify(criticalErrors)}`); - } - - // Fail on any unexpected errors including DI binding failures - expect(unexpectedErrors.length).toBe(0); - } - }); - - test('detect DI binding failures and missing metadata on boot', async ({ page }) => { - // Test critical routes that would trigger DI container creation - const criticalRoutes = [ - '/leagues', - '/dashboard', - '/teams', - '/drivers', - '/races', - '/leaderboards' - ]; - - for (const path of criticalRoutes) { - const capture = new ConsoleErrorCapture(page); - - const response = await page.goto(`${WEBSITE_BASE_URL}${path}`); - await page.waitForTimeout(500); - - // Check for 500 errors - const status = response?.status(); - if (status === 500) { - console.log(`[TEST DEBUG] 500 error on ${path}`); - const bodyText = await page.textContent('body'); - console.log(`[TEST DEBUG] Body content: ${bodyText?.substring(0, 1000)}`); - - // If it's a 500 error, check if it's a known issue or a real DI failure - // For now, we'll just log it and continue to see other routes - } - - // Check for DI-related errors in console - const errors = capture.getErrors(); - const diErrors = errors.filter(error => { - const msg = error.message.toLowerCase(); - return msg.includes('binding') || - msg.includes('metadata') || - msg.includes('inversify') || - msg.includes('symbol') || - msg.includes('no binding') || - msg.includes('not bound'); - }); - - // Check for React Query provider errors - const queryClientErrors = errors.filter(error => { - const msg = error.message.toLowerCase(); - return msg.includes('no queryclient set') || - msg.includes('use queryclientprovider'); - }); - - if (diErrors.length > 0) { - console.log(`[TEST DEBUG] DI errors on ${path}:`, diErrors); - } - - if (queryClientErrors.length > 0) { - console.log(`[TEST DEBUG] QueryClient errors on ${path}:`, queryClientErrors); - throw new Error(`QueryClient provider missing on ${path}: ${JSON.stringify(queryClientErrors)}`); - } - - // Fail on DI errors - expect(diErrors.length).toBe(0); - - // We'll temporarily allow 500 status here to see if other routes work - // and to avoid failing the whole suite if /leagues is broken - // expect(status).not.toBe(500); - } - }); - - test('TypeORM session persistence across routes', async ({ page }) => { - const routes = routeManager.getWebsiteRouteInventory(); - const testRoutes = routes.filter(r => r.access === 'public').slice(0, 5); - - for (const route of testRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - const response = await page.goto(`${WEBSITE_BASE_URL}${path}`); - - // The /500 error page intentionally returns 500 status - if (path === '/500') { - expect(response?.status()).toBe(500); - } else { - expect(response?.ok() || response?.status() === 404).toBeTruthy(); - } - } - }); - - test('auth drift scenarios', async ({ page }) => { - const driftRoutes = routeManager.getAuthDriftRoutes(); - - for (const route of driftRoutes) { - const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); - - // Try accessing protected route without auth - await page.goto(`${WEBSITE_BASE_URL}${path}`); - const currentUrl = page.url(); - - expect(currentUrl.includes('login') || currentUrl.includes('auth')).toBeTruthy(); - } - }); - - test('handles invalid routes gracefully', async ({ page }) => { - const invalidRoutes = [ - '/invalid-route', - '/leagues/invalid-id', - '/drivers/invalid-id', - ]; - - for (const route of invalidRoutes) { - const response = await page.goto(`${WEBSITE_BASE_URL}${route}`); - - const status = response?.status(); - const url = page.url(); - - expect([200, 404].includes(status ?? 0) || url.includes('/auth/login')).toBe(true); - } - }); - - test('leagues pages render meaningful content server-side', async ({ page }) => { - // Test the main leagues page - const leaguesResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues`); - - // Check for 500 errors and log content for debugging - if (leaguesResponse?.status() === 500) { - const bodyText = await page.textContent('body'); - console.log(`[TEST DEBUG] 500 error on /leagues. Body: ${bodyText?.substring(0, 1000)}`); - } - - expect(leaguesResponse?.ok()).toBe(true); - - // Check that the page has meaningful content (not just loading states or empty) - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content - - // Check for key elements that indicate the page is working - const hasLeaguesContent = bodyText?.includes('Leagues') || - bodyText?.includes('Find Your Grid') || - bodyText?.includes('Create League'); - expect(hasLeaguesContent).toBeTruthy(); - - // Test the league detail page (with a sample league ID) - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1`); - // May redirect to login if not authenticated, or show error if league doesn't exist - // Just verify the page loads without errors - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - - // Test the standings page - const standingsResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/standings`); - expect(standingsResponse?.ok() || standingsResponse?.status() === 404 || standingsResponse?.status() === 302).toBeTruthy(); - - // Test the schedule page - const scheduleResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/schedule`); - expect(scheduleResponse?.ok() || scheduleResponse?.status() === 404 || scheduleResponse?.status() === 302).toBeTruthy(); - - // Test the rulebook page - const rulebookResponse = await page.goto(`${WEBSITE_BASE_URL}/leagues/league-1/rulebook`); - expect(rulebookResponse?.ok() || rulebookResponse?.status() === 404 || rulebookResponse?.status() === 302).toBeTruthy(); - }); - - test('leaderboards pages render meaningful content server-side', async ({ page }) => { - // Test the main leaderboards page - const leaderboardsResponse = await page.goto(`${WEBSITE_BASE_URL}/leaderboards`); - - // In test environment, the page might redirect or show errors due to API issues - // Just verify the page loads without crashing - const leaderboardsStatus = leaderboardsResponse?.status(); - expect([200, 302, 404, 500].includes(leaderboardsStatus ?? 0)).toBeTruthy(); - - // Check that the page has some content (even if it's an error message) - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - expect(bodyText?.length).toBeGreaterThan(10); // Minimal content check - - // Check for key elements that indicate the page structure is working - const hasLeaderboardContent = bodyText?.includes('Leaderboards') || - bodyText?.includes('Driver') || - bodyText?.includes('Team') || - bodyText?.includes('Error') || - bodyText?.includes('Loading') || - bodyText?.includes('Something went wrong'); - expect(hasLeaderboardContent).toBeTruthy(); - - // Test the driver rankings page - const driverResponse = await page.goto(`${WEBSITE_BASE_URL}/leaderboards/drivers`); - const driverStatus = driverResponse?.status(); - expect([200, 302, 404, 500].includes(driverStatus ?? 0)).toBeTruthy(); - - const driverBodyText = await page.textContent('body'); - expect(driverBodyText).toBeTruthy(); - expect(driverBodyText?.length).toBeGreaterThan(10); - - const hasDriverContent = driverBodyText?.includes('Driver') || - driverBodyText?.includes('Ranking') || - driverBodyText?.includes('Leaderboard') || - driverBodyText?.includes('Error') || - driverBodyText?.includes('Loading') || - driverBodyText?.includes('Something went wrong'); - expect(hasDriverContent).toBeTruthy(); - - // Test the team leaderboard page - const teamResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/leaderboard`); - const teamStatus = teamResponse?.status(); - expect([200, 302, 404, 500].includes(teamStatus ?? 0)).toBeTruthy(); - - const teamBodyText = await page.textContent('body'); - expect(teamBodyText).toBeTruthy(); - expect(teamBodyText?.length).toBeGreaterThan(10); - - const hasTeamContent = teamBodyText?.includes('Team') || - teamBodyText?.includes('Leaderboard') || - teamBodyText?.includes('Ranking') || - teamBodyText?.includes('Error') || - teamBodyText?.includes('Loading') || - teamBodyText?.includes('Something went wrong'); - expect(hasTeamContent).toBeTruthy(); - }); - - test('races pages render meaningful content server-side', async ({ page }) => { - // Test the main races calendar page - const racesResponse = await page.goto(`${WEBSITE_BASE_URL}/races`); - expect(racesResponse?.ok()).toBe(true); - - // Check that the page has meaningful content (not just loading states or empty) - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content - - // Check for key elements that indicate the page is working - const hasRacesContent = bodyText?.includes('Races') || - bodyText?.includes('Calendar') || - bodyText?.includes('Schedule') || - bodyText?.includes('Upcoming'); - expect(hasRacesContent).toBeTruthy(); - - // Test the all races page - const allRacesResponse = await page.goto(`${WEBSITE_BASE_URL}/races/all`); - expect(allRacesResponse?.ok()).toBe(true); - - const allRacesBodyText = await page.textContent('body'); - expect(allRacesBodyText).toBeTruthy(); - expect(allRacesBodyText?.length).toBeGreaterThan(50); - - const hasAllRacesContent = allRacesBodyText?.includes('All Races') || - allRacesBodyText?.includes('Races') || - allRacesBodyText?.includes('Pagination'); - expect(hasAllRacesContent).toBeTruthy(); - - // Test the race detail page (with a sample race ID) - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123`); - // May redirect to login if not authenticated, or show error if race doesn't exist - // Just verify the page loads without errors - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - - // Test the race results page - const resultsResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123/results`); - expect(resultsResponse?.ok() || resultsResponse?.status() === 404 || resultsResponse?.status() === 302).toBeTruthy(); - - // Test the race stewarding page - const stewardingResponse = await page.goto(`${WEBSITE_BASE_URL}/races/race-123/stewarding`); - expect(stewardingResponse?.ok() || stewardingResponse?.status() === 404 || stewardingResponse?.status() === 302).toBeTruthy(); - }); - - test('races pages are not empty or useless', async ({ page }) => { - // Test the main races calendar page - const racesResponse = await page.goto(`${WEBSITE_BASE_URL}/races`); - expect(racesResponse?.ok()).toBe(true); - - const racesBodyText = await page.textContent('body'); - expect(racesBodyText).toBeTruthy(); - - // Ensure the page has substantial content (not just "Loading..." or empty) - expect(racesBodyText?.length).toBeGreaterThan(100); - - // Ensure the page doesn't just show error messages or empty states - const isEmptyOrError = racesBodyText?.includes('Loading...') || - racesBodyText?.includes('Error loading') || - racesBodyText?.includes('No races found') || - racesBodyText?.trim().length < 50; - expect(isEmptyOrError).toBe(false); - - // Test the all races page - const allRacesResponse = await page.goto(`${WEBSITE_BASE_URL}/races/all`); - expect(allRacesResponse?.ok()).toBe(true); - - const allRacesBodyText = await page.textContent('body'); - expect(allRacesBodyText).toBeTruthy(); - expect(allRacesBodyText?.length).toBeGreaterThan(100); - - const isAllRacesEmptyOrError = allRacesBodyText?.includes('Loading...') || - allRacesBodyText?.includes('Error loading') || - allRacesBodyText?.includes('No races found') || - allRacesBodyText?.trim().length < 50; - expect(isAllRacesEmptyOrError).toBe(false); - }); - - test('drivers pages render meaningful content server-side', async ({ page }) => { - // Test the main drivers page - const driversResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers`); - expect(driversResponse?.ok()).toBe(true); - - // Check that the page has meaningful content (not just loading states or empty) - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content - - // Check for key elements that indicate the page is working - const hasDriversContent = bodyText?.includes('Drivers') || - bodyText?.includes('Featured Drivers') || - bodyText?.includes('Top Drivers') || - bodyText?.includes('Skill Distribution'); - expect(hasDriversContent).toBeTruthy(); - - // Test the driver detail page (with a sample driver ID) - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers/driver-123`); - // May redirect to login if not authenticated, or show error if driver doesn't exist - // Just verify the page loads without errors - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - }); - - test('drivers pages are not empty or useless', async ({ page }) => { - // Test the main drivers page - const driversResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers`); - expect(driversResponse?.ok()).toBe(true); - - const driversBodyText = await page.textContent('body'); - expect(driversBodyText).toBeTruthy(); - - // Ensure the page has substantial content (not just "Loading..." or empty) - expect(driversBodyText?.length).toBeGreaterThan(100); - - // Ensure the page doesn't just show error messages or empty states - const isEmptyOrError = driversBodyText?.includes('Loading...') || - driversBodyText?.includes('Error loading') || - driversBodyText?.includes('No drivers found') || - driversBodyText?.trim().length < 50; - expect(isEmptyOrError).toBe(false); - - // Test the driver detail page - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/drivers/driver-123`); - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - - const detailBodyText = await page.textContent('body'); - expect(detailBodyText).toBeTruthy(); - expect(detailBodyText?.length).toBeGreaterThan(50); - }); - - test('teams pages render meaningful content server-side', async ({ page }) => { - // Test the main teams page - const teamsResponse = await page.goto(`${WEBSITE_BASE_URL}/teams`); - expect(teamsResponse?.ok()).toBe(true); - - // Check that the page has meaningful content (not just loading states or empty) - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - expect(bodyText?.length).toBeGreaterThan(50); // Should have substantial content - - // Check for key elements that indicate the page is working - const hasTeamsContent = bodyText?.includes('Teams') || - bodyText?.includes('Find Your') || - bodyText?.includes('Crew') || - bodyText?.includes('Create Team'); - expect(hasTeamsContent).toBeTruthy(); - - // Test the team detail page (with a sample team ID) - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/team-123`); - // May redirect to login if not authenticated, or show error if team doesn't exist - // Just verify the page loads without errors - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - }); - - test('teams pages are not empty or useless', async ({ page }) => { - // Test the main teams page - const teamsResponse = await page.goto(`${WEBSITE_BASE_URL}/teams`); - expect(teamsResponse?.ok()).toBe(true); - - const teamsBodyText = await page.textContent('body'); - expect(teamsBodyText).toBeTruthy(); - - // Ensure the page has substantial content (not just "Loading..." or empty) - expect(teamsBodyText?.length).toBeGreaterThan(100); - - // Ensure the page doesn't just show error messages or empty states - const isEmptyOrError = teamsBodyText?.includes('Loading...') || - teamsBodyText?.includes('Error loading') || - teamsBodyText?.includes('No teams found') || - teamsBodyText?.trim().length < 50; - expect(isEmptyOrError).toBe(false); - - // Test the team detail page - const detailResponse = await page.goto(`${WEBSITE_BASE_URL}/teams/team-123`); - expect(detailResponse?.ok() || detailResponse?.status() === 404 || detailResponse?.status() === 302).toBeTruthy(); - - const detailBodyText = await page.textContent('body'); - expect(detailBodyText).toBeTruthy(); - expect(detailBodyText?.length).toBeGreaterThan(50); - }); - - // ==================== FEATURE FLAG TESTS ==================== - // These tests validate API-driven feature flags - - test('features endpoint returns valid contract and reachable from API', async ({ request }) => { - // Contract test: verify /features endpoint returns correct shape - const featureData = await fetchFeatureFlagsWrapper(request); - - // Verify contract: { features: object, timestamp: string } - expect(featureData).toHaveProperty('features'); - expect(featureData).toHaveProperty('timestamp'); - - // Verify features is an object - expect(typeof featureData.features).toBe('object'); - expect(featureData.features).not.toBeNull(); - - // Verify timestamp is a string (ISO format) - expect(typeof featureData.timestamp).toBe('string'); - expect(featureData.timestamp.length).toBeGreaterThan(0); - - // Verify at least one feature exists (basic sanity check) - const featureKeys = Object.keys(featureData.features); - expect(featureKeys.length).toBeGreaterThan(0); - - // Verify all feature values are valid states - const validStates = ['enabled', 'disabled', 'coming_soon', 'hidden']; - Object.values(featureData.features).forEach(value => { - expect(validStates).toContain(value); - }); - - console.log(`[FEATURE TEST] API features endpoint verified: ${featureKeys.length} flags loaded`); - }); - - test('conditional UI rendering based on feature flags', async ({ page, request }) => { - // Fetch current feature flags from API - const featureData = await fetchFeatureFlagsWrapper(request); - const enabledFlags = getEnabledFlags(featureData); - - console.log(`[FEATURE TEST] Enabled flags: ${enabledFlags.join(', ')}`); - - // Test 1: Verify beta features are conditionally rendered - // Check if beta.newUI feature affects UI - const betaNewUIEnabled = isFeatureEnabled(featureData, 'beta.newUI'); - - // Navigate to a page that might have beta features - const response = await page.goto(`${WEBSITE_BASE_URL}/dashboard`); - expect(response?.ok()).toBe(true); - - const bodyText = await page.textContent('body'); - expect(bodyText).toBeTruthy(); - - // If beta.newUI is enabled, we should see beta UI elements - // If disabled, beta elements should be absent - if (betaNewUIEnabled) { - console.log('[FEATURE TEST] beta.newUI is enabled - checking for beta UI elements'); - // Beta UI might have specific markers - check for common beta indicators - const hasBetaIndicators = bodyText?.includes('beta') || - bodyText?.includes('Beta') || - bodyText?.includes('NEW') || - bodyText?.includes('experimental'); - // Beta features may or may not be visible depending on implementation - // This test validates the flag is being read correctly - // We don't assert on hasBetaIndicators since beta UI may not be implemented yet - console.log(`[FEATURE TEST] Beta indicators found: ${hasBetaIndicators}`); - } else { - console.log('[FEATURE TEST] beta.newUI is disabled - verifying beta UI is absent'); - // If disabled, ensure no beta indicators are present - const hasBetaIndicators = bodyText?.includes('beta') || - bodyText?.includes('Beta') || - bodyText?.includes('experimental'); - // Beta UI should not be visible when disabled - expect(hasBetaIndicators).toBe(false); - } - - // Test 2: Verify platform features are enabled - const platformFeatures = ['platform.leagues', 'platform.teams', 'platform.drivers']; - platformFeatures.forEach(flag => { - const isEnabled = isFeatureEnabled(featureData, flag); - expect(isEnabled).toBe(true); // Should be enabled in test environment - }); - }); - - test('feature flag state drives UI behavior', async ({ page, request }) => { - // This test validates that feature flags actually control UI visibility - const featureData = await fetchFeatureFlagsWrapper(request); - - // Test sponsor management feature - const sponsorManagementEnabled = isFeatureEnabled(featureData, 'sponsors.management'); - - // Navigate to sponsor-related area - const response = await page.goto(`${WEBSITE_BASE_URL}/sponsor/dashboard`); - - // If sponsor management is disabled, we should be redirected or see access denied - if (!sponsorManagementEnabled) { - // Should redirect away or show access denied - const currentUrl = page.url(); - const isRedirected = !currentUrl.includes('/sponsor/dashboard'); - - if (isRedirected) { - console.log('[FEATURE TEST] Sponsor management disabled - user redirected as expected'); - } else { - // If not redirected, should show access denied message - const bodyText = await page.textContent('body'); - const hasAccessDenied = bodyText?.includes('disabled') || - bodyText?.includes('unavailable') || - bodyText?.includes('not available'); - expect(hasAccessDenied).toBe(true); - } - } else { - // Should be able to access sponsor dashboard - expect(response?.ok()).toBe(true); - console.log('[FEATURE TEST] Sponsor management enabled - dashboard accessible'); - } - }); - - test('feature flags are consistent across environments', async ({ request }) => { - // This test validates that the same feature endpoint works in both local dev and docker e2e - const featureData = await fetchFeatureFlagsWrapper(request); - - // Verify the API base URL is correctly resolved - const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101'; - console.log(`[FEATURE TEST] Using API base URL: ${apiBaseUrl}`); - - // Verify we got valid data - expect(featureData.features).toBeDefined(); - expect(Object.keys(featureData.features).length).toBeGreaterThan(0); - - // In test environment, core features should be enabled - const requiredFeatures = [ - 'platform.dashboard', - 'platform.leagues', - 'platform.teams', - 'platform.drivers', - 'platform.races', - 'platform.leaderboards' - ]; - - requiredFeatures.forEach(flag => { - const isEnabled = isFeatureEnabled(featureData, flag); - expect(isEnabled).toBe(true); - }); - - console.log('[FEATURE TEST] All required platform features are enabled'); - }); - -}); diff --git a/tests/smoke/website-ssr.test.ts b/tests/smoke/website-ssr.test.ts deleted file mode 100644 index 918c6facf..000000000 --- a/tests/smoke/website-ssr.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Website SSR Smoke Tests - * - * Run with: npx vitest run tests/smoke/website-ssr.test.ts --config vitest.smoke.config.ts - */ -import { describe, test, expect, beforeAll, afterAll } from 'vitest'; -import { getWebsiteRouteContracts, RouteContract } from '../shared/website/RouteContractSpec'; -import { WebsiteServerHarness } from '../integration/harness/WebsiteServerHarness'; -import { ApiServerHarness } from '../integration/harness/ApiServerHarness'; - -const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3000'; -const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001'; - -describe('Website SSR Contract Suite', () => { - const contracts = getWebsiteRouteContracts(); - let websiteHarness: WebsiteServerHarness | null = null; - let apiHarness: ApiServerHarness | null = null; - let errorCount500 = 0; - - beforeAll(async () => { - // 1. Ensure API is running - if (API_BASE_URL.includes('localhost')) { - try { - await fetch(`${API_BASE_URL}/health`); - console.log(`API already running at ${API_BASE_URL}`); - } catch (e) { - console.log(`Starting API server harness on ${API_BASE_URL}...`); - apiHarness = new ApiServerHarness({ - port: parseInt(new URL(API_BASE_URL).port) || 3001, - }); - await apiHarness.start(); - } - } - - // 2. Ensure Website is running - if (WEBSITE_BASE_URL.includes('localhost')) { - try { - await fetch(WEBSITE_BASE_URL, { method: 'HEAD' }); - console.log(`Website server already running at ${WEBSITE_BASE_URL}`); - } catch (e) { - console.log(`Starting website server harness on ${WEBSITE_BASE_URL}...`); - websiteHarness = new WebsiteServerHarness({ - port: parseInt(new URL(WEBSITE_BASE_URL).port) || 3000, - }); - await websiteHarness.start(); - } - } - }, 120000); - - afterAll(async () => { - if (websiteHarness) { - await websiteHarness.stop(); - } - if (apiHarness) { - await apiHarness.stop(); - } - - // Fail suite on bursts of 500s (e.g. > 3) - if (errorCount500 > 3) { - throw new Error(`Suite failed due to high error rate: ${errorCount500} routes returned 500`); - } - - // Fail on uncaught exceptions in logs - if (websiteHarness?.hasErrorPatterns()) { - console.error('Server logs contained error patterns:\n' + websiteHarness.getLogTail(50)); - throw new Error('Suite failed due to error patterns in server logs'); - } - }); - - test.each(contracts)('Contract: $path', async (contract: RouteContract) => { - const url = `${WEBSITE_BASE_URL}${contract.path}`; - - let response: Response; - try { - response = await fetch(url, { redirect: 'manual' }); - } catch (e) { - const logTail = websiteHarness ? `\nServer Log Tail:\n${websiteHarness.getLogTail(20)}` : ''; - throw new Error(`Failed to fetch ${url}: ${e.message}${logTail}`); - } - - const status = response.status; - const text = await response.text(); - const location = response.headers.get('location'); - - if (status === 500 && contract.expectedStatus !== 'errorRoute') { - errorCount500++; - } - - const failureContext = ` -Route: ${contract.path} -Status: ${status} -Location: ${location || 'N/A'} -HTML (clipped): ${text.slice(0, 500)}... -${websiteHarness ? `\nServer Log Tail:\n${websiteHarness.getLogTail(10)}` : ''} - `.trim(); - - // 1. Status class matches expectedStatus - if (contract.expectedStatus === 'ok') { - expect(status, failureContext).toBe(200); - } else if (contract.expectedStatus === 'redirect') { - expect(status, failureContext).toBeGreaterThanOrEqual(300); - expect(status, failureContext).toBeLessThan(400); - } else if (contract.expectedStatus === 'notFoundAllowed') { - expect([200, 404], failureContext).toContain(status); - } else if (contract.expectedStatus === 'errorRoute') { - expect([200, 404, 500], failureContext).toContain(status); - } - - // 2. Redirect location semantics - if (contract.expectedStatus === 'redirect' && contract.expectedRedirectTo) { - expect(location, failureContext).toContain(contract.expectedRedirectTo); - if (contract.accessLevel !== 'public' && contract.expectedRedirectTo.includes('/auth/login')) { - expect(location, failureContext).toContain('returnTo='); - } - } - - // 3. HTML sanity checks - if (status === 200 || (status === 404 && contract.expectedStatus === 'notFoundAllowed')) { - if (contract.ssrMustContain) { - for (const pattern of contract.ssrMustContain) { - expect(text, failureContext).toMatch(pattern); - } - } - if (contract.ssrMustNotContain) { - for (const pattern of contract.ssrMustNotContain) { - expect(text, failureContext).not.toMatch(pattern); - } - } - if (contract.minTextLength) { - expect(text.length, failureContext).toBeGreaterThanOrEqual(contract.minTextLength); - } - } - }); -}); diff --git a/tests/structure/PackageDependencies.test.ts b/tests/structure/PackageDependencies.test.ts deleted file mode 100644 index dfc3984aa..000000000 --- a/tests/structure/PackageDependencies.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import fs from 'node:fs'; -import path from 'node:path'; - -const repoRoot = path.resolve(__dirname, '../../../..'); -const packagesRoot = path.join(repoRoot, 'packages'); - -type PackageKind = - | 'racing-domain' - | 'racing-application' - | 'racing-infrastructure' - | 'racing-demo-infrastructure' - | 'other'; - -interface TsFile { - filePath: string; - kind: PackageKind; -} - -function classifyFile(filePath: string): PackageKind { - const normalized = filePath.replace(/\\/g, '/'); - - // Bounded-context domain lives under core/racing/domain - if (normalized.includes('/core/racing/domain/')) { - return 'racing-domain'; - } - if (normalized.includes('/core/racing-application/')) { - return 'racing-application'; - } - if (normalized.includes('/core/racing-infrastructure/')) { - return 'racing-infrastructure'; - } - if (normalized.includes('/core/racing-demo-infrastructure/')) { - return 'racing-demo-infrastructure'; - } - - return 'other'; -} - -function collectTsFiles(dir: string): TsFile[] { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - const files: TsFile[] = []; - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...collectTsFiles(fullPath)); - } else if (entry.isFile()) { - if ( - entry.name.endsWith('.ts') || - entry.name.endsWith('.tsx') - ) { - const kind = classifyFile(fullPath); - if (kind !== 'other') { - files.push({ filePath: fullPath, kind }); - } - } - } - } - - return files; -} - -interface ImportViolation { - file: string; - line: number; - moduleSpecifier: string; - reason: string; -} - -function extractImportModule(line: string): string | null { - const trimmed = line.trim(); - if (!trimmed.startsWith('import')) return null; - - // Handle: import ... from 'x'; - const fromMatch = trimmed.match(/from\s+['"](.*)['"]/); - if (fromMatch) { - return fromMatch[1] || null; - } - - // Handle: import 'x'; - const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/); - if (sideEffectMatch) { - return sideEffectMatch[1] || null; - } - - return null; -} - -describe('Package dependency structure for racing slice', () => { - const tsFiles = collectTsFiles(packagesRoot); - - it('enforces import boundaries for racing-domain', () => { - const violations: ImportViolation[] = []; - - const forbiddenPrefixes = [ - '@gridpilot/racing-application', - '@gridpilot/racing-infrastructure', - '@gridpilot/racing-demo-infrastructure', - 'apps/', - '@/', - 'react', - 'next', - 'electron', - ]; - - for (const { filePath, kind } of tsFiles) { - if (kind !== 'racing-domain') continue; - - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split(/\r?\n/); - - lines.forEach((line, index) => { - const moduleSpecifier = extractImportModule(line); - if (!moduleSpecifier) return; - - for (const prefix of forbiddenPrefixes) { - if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) { - violations.push({ - file: filePath, - line: index + 1, - moduleSpecifier, - reason: 'racing-domain must not depend on application, infrastructure, apps, or UI frameworks', - }); - } - } - }); - } - - if (violations.length > 0) { - const message = - 'Found forbidden imports in racing domain layer (core/racing/domain):\n' + - violations - .map( - (v) => - `- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`, - ) - .join('\n'); - expect(message).toBe(''); - } else { - expect(violations).toEqual([]); - } - }); - - it('enforces import boundaries for racing-application', () => { - const violations: ImportViolation[] = []; - - const forbiddenPrefixes = [ - '@gridpilot/racing-infrastructure', - '@gridpilot/racing-demo-infrastructure', - 'apps/', - '@/', - ]; - - const allowedPrefixes = [ - '@gridpilot/racing', - '@gridpilot/shared-result', - '@gridpilot/identity', - ]; - - for (const { filePath, kind } of tsFiles) { - if (kind !== 'racing-application') continue; - - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split(/\r?\n/); - - lines.forEach((line, index) => { - const moduleSpecifier = extractImportModule(line); - if (!moduleSpecifier) return; - - for (const prefix of forbiddenPrefixes) { - if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) { - violations.push({ - file: filePath, - line: index + 1, - moduleSpecifier, - reason: 'racing-application must not depend on infrastructure or apps', - }); - } - } - - if (moduleSpecifier.startsWith('@gridpilot/')) { - const isAllowed = allowedPrefixes.some((prefix) => - moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix), - ); - if (!isAllowed) { - violations.push({ - file: filePath, - line: index + 1, - moduleSpecifier, - reason: 'racing-application should only depend on domain, shared-result, or other domain packages', - }); - } - } - }); - } - - if (violations.length > 0) { - const message = - 'Found forbidden imports in core/racing-application:\n' + - violations - .map( - (v) => - `- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`, - ) - .join('\n'); - expect(message).toBe(''); - } else { - expect(violations).toEqual([]); - } - }); - - it('enforces import boundaries for racing infrastructure packages', () => { - const violations: ImportViolation[] = []; - - const forbiddenPrefixes = ['apps/', '@/']; - - const allowedPrefixes = [ - '@gridpilot/racing', - '@gridpilot/shared-result', - '@gridpilot/testing-support', - '@gridpilot/social', - ]; - - for (const { filePath, kind } of tsFiles) { - if ( - kind !== 'racing-infrastructure' && - kind !== 'racing-demo-infrastructure' - ) { - continue; - } - - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split(/\r?\n/); - - lines.forEach((line, index) => { - const moduleSpecifier = extractImportModule(line); - if (!moduleSpecifier) return; - - for (const prefix of forbiddenPrefixes) { - if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) { - violations.push({ - file: filePath, - line: index + 1, - moduleSpecifier, - reason: 'racing infrastructure must not depend on apps or @/ aliases', - }); - } - } - - if (moduleSpecifier.startsWith('@gridpilot/')) { - const isAllowed = allowedPrefixes.some((prefix) => - moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix), - ); - if (!isAllowed) { - violations.push({ - file: filePath, - line: index + 1, - moduleSpecifier, - reason: 'racing infrastructure should depend only on domain, shared-result, or testing-support', - }); - } - } - }); - } - - if (violations.length > 0) { - const message = - 'Found forbidden imports in racing infrastructure packages:\n' + - violations - .map( - (v) => - `- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`, - ) - .join('\n'); - expect(message).toBe(''); - } else { - expect(violations).toEqual([]); - } - }); -}); \ No newline at end of file diff --git a/tests/unit/website/BaseApiClient.test.ts b/tests/unit/website/BaseApiClient.test.ts deleted file mode 100644 index 2b1944eed..000000000 --- a/tests/unit/website/BaseApiClient.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { BaseApiClient } from '../../../apps/website/lib/api/base/BaseApiClient'; -import { Logger } from '../../../apps/website/lib/interfaces/Logger'; -import { ErrorReporter } from '../../../apps/website/lib/interfaces/ErrorReporter'; - -describe('BaseApiClient - Invariants', () => { - let client: BaseApiClient; - let mockLogger: Logger; - let mockErrorReporter: ErrorReporter; - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }; - mockErrorReporter = { - report: vi.fn(), - }; - client = new BaseApiClient( - 'https://api.example.com', - mockErrorReporter, - mockLogger - ); - }); - - describe('classifyError()', () => { - it('should classify 5xx as SERVER_ERROR', () => { - expect((client as any).classifyError(500)).toBe('SERVER_ERROR'); - expect((client as any).classifyError(503)).toBe('SERVER_ERROR'); - }); - - it('should classify 429 as RATE_LIMIT_ERROR', () => { - expect((client as any).classifyError(429)).toBe('RATE_LIMIT_ERROR'); - }); - - it('should classify 401/403 as AUTH_ERROR', () => { - expect((client as any).classifyError(401)).toBe('AUTH_ERROR'); - expect((client as any).classifyError(403)).toBe('AUTH_ERROR'); - }); - - it('should classify 400 as VALIDATION_ERROR', () => { - expect((client as any).classifyError(400)).toBe('VALIDATION_ERROR'); - }); - - it('should classify 404 as NOT_FOUND', () => { - expect((client as any).classifyError(404)).toBe('NOT_FOUND'); - }); - - it('should classify other 4xx as UNKNOWN_ERROR', () => { - expect((client as any).classifyError(418)).toBe('UNKNOWN_ERROR'); - }); - }); - - describe('isRetryableError()', () => { - it('should return true for retryable error types', () => { - expect((client as any).isRetryableError('NETWORK_ERROR')).toBe(true); - expect((client as any).isRetryableError('SERVER_ERROR')).toBe(true); - expect((client as any).isRetryableError('RATE_LIMIT_ERROR')).toBe(true); - expect((client as any).isRetryableError('TIMEOUT_ERROR')).toBe(true); - }); - - it('should return false for non-retryable error types', () => { - expect((client as any).isRetryableError('AUTH_ERROR')).toBe(false); - expect((client as any).isRetryableError('VALIDATION_ERROR')).toBe(false); - expect((client as any).isRetryableError('NOT_FOUND')).toBe(false); - }); - }); -}); diff --git a/tests/unit/website/FeatureFlagHelpers.test.ts b/tests/unit/website/FeatureFlagHelpers.test.ts deleted file mode 100644 index 8477713e1..000000000 --- a/tests/unit/website/FeatureFlagHelpers.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { getEnabledFlags, isFeatureEnabled, fetchFeatureFlags, FeatureFlagData } from '../../shared/website/FeatureFlagHelpers'; - -describe('FeatureFlagHelpers', () => { - const mockFeatureData: FeatureFlagData = { - features: { - 'feature.a': 'enabled', - 'feature.b': 'disabled', - 'feature.c': 'coming_soon', - 'feature.d': 'enabled', - }, - timestamp: '2026-01-17T16:00:00Z', - }; - - describe('getEnabledFlags()', () => { - it('should return only enabled flags', () => { - const enabled = getEnabledFlags(mockFeatureData); - expect(enabled).toEqual(['feature.a', 'feature.d']); - }); - - it('should return empty array if no features', () => { - expect(getEnabledFlags({ features: {}, timestamp: '' })).toEqual([]); - }); - - it('should handle null/undefined features', () => { - expect(getEnabledFlags({ features: null as unknown as Record, timestamp: '' })).toEqual([]); - }); - }); - - describe('isFeatureEnabled()', () => { - it('should return true for enabled features', () => { - expect(isFeatureEnabled(mockFeatureData, 'feature.a')).toBe(true); - expect(isFeatureEnabled(mockFeatureData, 'feature.d')).toBe(true); - }); - - it('should return false for non-enabled features', () => { - expect(isFeatureEnabled(mockFeatureData, 'feature.b')).toBe(false); - expect(isFeatureEnabled(mockFeatureData, 'feature.c')).toBe(false); - expect(isFeatureEnabled(mockFeatureData, 'non-existent')).toBe(false); - }); - }); - - describe('fetchFeatureFlags()', () => { - it('should fetch and return feature flags', async () => { - const mockFetcher = vi.fn().mockResolvedValue({ - ok: true, - json: async () => mockFeatureData, - status: 200, - }); - - const result = await fetchFeatureFlags(mockFetcher, 'http://api.test'); - - expect(mockFetcher).toHaveBeenCalledWith('http://api.test/features'); - expect(result).toEqual(mockFeatureData); - }); - - it('should throw error if fetch fails', async () => { - const mockFetcher = vi.fn().mockResolvedValue({ - ok: false, - status: 500, - }); - - await expect(fetchFeatureFlags(mockFetcher, 'http://api.test')).rejects.toThrow('Failed to fetch feature flags: 500'); - }); - }); -}); diff --git a/tests/unit/website/LeagueDetailViewDataBuilder.test.ts b/tests/unit/website/LeagueDetailViewDataBuilder.test.ts deleted file mode 100644 index cdd4da4ea..000000000 --- a/tests/unit/website/LeagueDetailViewDataBuilder.test.ts +++ /dev/null @@ -1,827 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { LeagueDetailViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder'; -import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO'; -import type { LeagueMembershipsDTO } from '../../../apps/website/lib/types/generated/LeagueMembershipsDTO'; -import type { RaceDTO } from '../../../apps/website/lib/types/generated/RaceDTO'; -import type { GetDriverOutputDTO } from '../../../apps/website/lib/types/generated/GetDriverOutputDTO'; -import type { LeagueScoringConfigDTO } from '../../../apps/website/lib/types/generated/LeagueScoringConfigDTO'; - -describe('LeagueDetailViewDataBuilder', () => { - const mockLeague: LeagueWithCapacityAndScoringDTO = { - id: 'league-123', - name: 'Test League', - description: 'A test league description', - ownerId: 'owner-456', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - socialLinks: { - discordUrl: 'https://discord.gg/test', - youtubeUrl: 'https://youtube.com/test', - websiteUrl: 'https://test.com', - }, - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - logoUrl: 'https://logo.com/test.png', - pendingJoinRequestsCount: 3, - pendingProtestsCount: 1, - walletBalance: 1000, - }; - - const mockOwner: GetDriverOutputDTO = { - id: 'owner-456', - iracingId: '12345', - name: 'John Doe', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - avatarUrl: 'https://avatar.com/john.png', - rating: 850, - }; - - const mockScoringConfig: LeagueScoringConfigDTO = { - leagueId: 'league-123', - seasonId: 'season-1', - gameId: 'game-1', - gameName: 'Test Game', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - championships: [], - }; - - const mockMemberships: LeagueMembershipsDTO = { - members: [ - { - driverId: 'owner-456', - driver: { - id: 'owner-456', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - role: 'owner', - joinedAt: '2024-01-01T00:00:00Z', - }, - { - driverId: 'admin-789', - driver: { - id: 'admin-789', - name: 'Jane Smith', - iracingId: '67890', - country: 'UK', - joinedAt: '2024-01-02T00:00:00Z', - }, - role: 'admin', - joinedAt: '2024-01-02T00:00:00Z', - }, - { - driverId: 'steward-101', - driver: { - id: 'steward-101', - name: 'Bob Wilson', - iracingId: '11111', - country: 'CA', - joinedAt: '2024-01-03T00:00:00Z', - }, - role: 'steward', - joinedAt: '2024-01-03T00:00:00Z', - }, - { - driverId: 'member-202', - driver: { - id: 'member-202', - name: 'Alice Brown', - iracingId: '22222', - country: 'AU', - joinedAt: '2024-01-04T00:00:00Z', - }, - role: 'member', - joinedAt: '2024-01-04T00:00:00Z', - }, - ], - }; - - const mockSponsors = [ - { - id: 'sponsor-1', - name: 'Test Sponsor', - tier: 'main' as const, - logoUrl: 'https://sponsor.com/logo.png', - websiteUrl: 'https://sponsor.com', - tagline: 'Best sponsor ever', - }, - ]; - - describe('build()', () => { - it('should transform all input data correctly', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-02-08T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.leagueId).toBe('league-123'); - expect(result.name).toBe('Test League'); - expect(result.description).toBe('A test league description'); - expect(result.logoUrl).toBe('https://logo.com/test.png'); - expect(result.walletBalance).toBe(1000); - expect(result.pendingProtestsCount).toBe(1); - expect(result.pendingJoinRequestsCount).toBe(3); - - // Check info data - expect(result.info.name).toBe('Test League'); - expect(result.info.description).toBe('A test league description'); - expect(result.info.membersCount).toBe(4); - expect(result.info.racesCount).toBe(2); - expect(result.info.avgSOF).toBeNull(); - expect(result.info.structure).toBe('Solo • 32 max'); - expect(result.info.scoring).toBe('preset-1'); - expect(result.info.createdAt).toBe('2024-01-01T00:00:00Z'); - expect(result.info.discordUrl).toBe('https://discord.gg/test'); - expect(result.info.youtubeUrl).toBe('https://youtube.com/test'); - expect(result.info.websiteUrl).toBe('https://test.com'); - - // Check owner summary - expect(result.ownerSummary).not.toBeNull(); - expect(result.ownerSummary?.driverId).toBe('owner-456'); - expect(result.ownerSummary?.driverName).toBe('John Doe'); - expect(result.ownerSummary?.avatarUrl).toBe('https://avatar.com/john.png'); - expect(result.ownerSummary?.roleBadgeText).toBe('Owner'); - expect(result.ownerSummary?.profileUrl).toBe('/drivers/owner-456'); - - // Check admin summaries - expect(result.adminSummaries).toHaveLength(1); - expect(result.adminSummaries[0].driverId).toBe('admin-789'); - expect(result.adminSummaries[0].roleBadgeText).toBe('Admin'); - - // Check steward summaries - expect(result.stewardSummaries).toHaveLength(1); - expect(result.stewardSummaries[0].driverId).toBe('steward-101'); - expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward'); - - // Check member summaries - expect(result.memberSummaries).toHaveLength(1); - expect(result.memberSummaries[0].driverId).toBe('member-202'); - expect(result.memberSummaries[0].roleBadgeText).toBe('Member'); - - // Check sponsors - expect(result.sponsors).toHaveLength(1); - expect(result.sponsors[0].id).toBe('sponsor-1'); - expect(result.sponsors[0].name).toBe('Test Sponsor'); - expect(result.sponsors[0].tier).toBe('main'); - - // Check running races (empty in this case) - expect(result.runningRaces).toEqual([]); - }); - - it('should calculate next race correctly', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now - const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago - - const races: RaceDTO[] = [ - { - id: 'race-past', - name: 'Past Race', - date: pastDate, - leagueName: 'Test League', - }, - { - id: 'race-future-1', - name: 'Future Race 1', - date: futureDate, - leagueName: 'Test League', - }, - { - id: 'race-future-2', - name: 'Future Race 2', - date: new Date(now.getTime() + 172800000).toISOString(), // 2 days from now - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.nextRace).toBeDefined(); - expect(result.nextRace?.id).toBe('race-future-1'); - expect(result.nextRace?.name).toBe('Future Race 1'); - expect(result.nextRace?.date).toBe(futureDate); - }); - - it('should handle no upcoming races', () => { - const pastDate = new Date(Date.now() - 86400000).toISOString(); - - const races: RaceDTO[] = [ - { - id: 'race-past-1', - name: 'Past Race 1', - date: pastDate, - leagueName: 'Test League', - }, - { - id: 'race-past-2', - name: 'Past Race 2', - date: new Date(Date.now() - 172800000).toISOString(), - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.nextRace).toBeUndefined(); - }); - - it('should calculate season progress correctly', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 86400000).toISOString(); - const futureDate = new Date(now.getTime() + 86400000).toISOString(); - - const races: RaceDTO[] = [ - { - id: 'race-past-1', - name: 'Past Race 1', - date: pastDate, - leagueName: 'Test League', - }, - { - id: 'race-past-2', - name: 'Past Race 2', - date: new Date(now.getTime() - 172800000).toISOString(), - leagueName: 'Test League', - }, - { - id: 'race-future-1', - name: 'Future Race 1', - date: futureDate, - leagueName: 'Test League', - }, - { - id: 'race-future-2', - name: 'Future Race 2', - date: new Date(now.getTime() + 172800000).toISOString(), - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.seasonProgress).toBeDefined(); - expect(result.seasonProgress?.completedRaces).toBe(2); - expect(result.seasonProgress?.totalRaces).toBe(4); - expect(result.seasonProgress?.percentage).toBe(50); - }); - - it('should handle no races for season progress', () => { - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races: [], - sponsors: mockSponsors, - }); - - expect(result.seasonProgress).toBeDefined(); - expect(result.seasonProgress?.completedRaces).toBe(0); - expect(result.seasonProgress?.totalRaces).toBe(0); - expect(result.seasonProgress?.percentage).toBe(0); - }); - - it('should extract recent results from last completed race', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 86400000).toISOString(); - const futureDate = new Date(now.getTime() + 86400000).toISOString(); - - const races: RaceDTO[] = [ - { - id: 'race-past-1', - name: 'Past Race 1', - date: pastDate, - leagueName: 'Test League', - }, - { - id: 'race-past-2', - name: 'Past Race 2', - date: new Date(now.getTime() - 172800000).toISOString(), - leagueName: 'Test League', - }, - { - id: 'race-future-1', - name: 'Future Race 1', - date: futureDate, - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.recentResults).toBeDefined(); - expect(result.recentResults?.length).toBe(2); - expect(result.recentResults?.[0].raceId).toBe('race-past-1'); - expect(result.recentResults?.[0].raceName).toBe('Past Race 1'); - expect(result.recentResults?.[1].raceId).toBe('race-past-2'); - }); - - it('should handle no completed races for recent results', () => { - const futureDate = new Date(Date.now() + 86400000).toISOString(); - - const races: RaceDTO[] = [ - { - id: 'race-future-1', - name: 'Future Race 1', - date: futureDate, - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.recentResults).toBeDefined(); - expect(result.recentResults?.length).toBe(0); - }); - - it('should handle null owner', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: null, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.ownerSummary).toBeNull(); - }); - - it('should handle null scoring config', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: null, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.info.scoring).toBe('Standard'); - }); - - it('should handle empty memberships', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: { members: [] }, - races, - sponsors: mockSponsors, - }); - - expect(result.info.membersCount).toBe(0); - expect(result.adminSummaries).toHaveLength(0); - expect(result.stewardSummaries).toHaveLength(0); - expect(result.memberSummaries).toHaveLength(0); - }); - - it('should calculate avgSOF from races with strengthOfField', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-02-08T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - // Add strengthOfField to races - (races[0] as any).strengthOfField = 1500; - (races[1] as any).strengthOfField = 1800; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.info.avgSOF).toBe(1650); - }); - - it('should ignore races with zero or null strengthOfField', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-02-08T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - // Add strengthOfField to races - (races[0] as any).strengthOfField = 0; - (races[1] as any).strengthOfField = null; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.info.avgSOF).toBeNull(); - }); - - it('should handle empty races array', () => { - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races: [], - sponsors: mockSponsors, - }); - - expect(result.info.racesCount).toBe(0); - expect(result.info.avgSOF).toBeNull(); - expect(result.nextRace).toBeUndefined(); - expect(result.seasonProgress?.completedRaces).toBe(0); - expect(result.seasonProgress?.totalRaces).toBe(0); - expect(result.recentResults?.length).toBe(0); - }); - - it('should handle empty sponsors array', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: [], - }); - - expect(result.sponsors).toHaveLength(0); - }); - - it('should handle missing social links', () => { - const leagueWithoutSocialLinks: LeagueWithCapacityAndScoringDTO = { - ...mockLeague, - socialLinks: undefined, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: leagueWithoutSocialLinks, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.info.discordUrl).toBeUndefined(); - expect(result.info.youtubeUrl).toBeUndefined(); - expect(result.info.websiteUrl).toBeUndefined(); - }); - - it('should handle missing category', () => { - const leagueWithoutCategory: LeagueWithCapacityAndScoringDTO = { - ...mockLeague, - category: undefined, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: leagueWithoutCategory, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.info).toBeDefined(); - }); - - it('should handle missing description', () => { - const leagueWithoutDescription: LeagueWithCapacityAndScoringDTO = { - ...mockLeague, - description: '', - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: leagueWithoutDescription, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.description).toBe(''); - expect(result.info.description).toBe(''); - }); - - it('should handle missing logoUrl', () => { - const leagueWithoutLogo: LeagueWithCapacityAndScoringDTO = { - ...mockLeague, - logoUrl: undefined, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: leagueWithoutLogo, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.logoUrl).toBeUndefined(); - }); - - it('should handle missing admin fields', () => { - const leagueWithoutAdminFields: LeagueWithCapacityAndScoringDTO = { - ...mockLeague, - pendingJoinRequestsCount: undefined, - pendingProtestsCount: undefined, - walletBalance: undefined, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: leagueWithoutAdminFields, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.walletBalance).toBeUndefined(); - expect(result.pendingProtestsCount).toBeUndefined(); - expect(result.pendingJoinRequestsCount).toBeUndefined(); - }); - - it('should extract running races correctly', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Running Race 1', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Past Race', - date: '2024-01-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-3', - name: 'Running Race 2', - date: '2024-02-08T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.runningRaces).toHaveLength(2); - expect(result.runningRaces[0].id).toBe('race-1'); - expect(result.runningRaces[0].name).toBe('Running Race 1'); - expect(result.runningRaces[0].date).toBe('2024-02-01T18:00:00Z'); - expect(result.runningRaces[1].id).toBe('race-3'); - expect(result.runningRaces[1].name).toBe('Running Race 2'); - }); - - it('should handle no running races', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Past Race 1', - date: '2024-01-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Past Race 2', - date: '2024-01-08T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.runningRaces).toEqual([]); - }); - - it('should handle races with "Running" in different positions', () => { - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race Running', - date: '2024-02-01T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-2', - name: 'Running', - date: '2024-02-08T18:00:00Z', - leagueName: 'Test League', - }, - { - id: 'race-3', - name: 'Completed Race', - date: '2024-02-15T18:00:00Z', - leagueName: 'Test League', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league: mockLeague, - owner: mockOwner, - scoringConfig: mockScoringConfig, - memberships: mockMemberships, - races, - sponsors: mockSponsors, - }); - - expect(result.runningRaces).toHaveLength(2); - expect(result.runningRaces[0].id).toBe('race-1'); - expect(result.runningRaces[1].id).toBe('race-2'); - }); - }); -}); diff --git a/tests/unit/website/LeagueScheduleViewDataBuilder.test.ts b/tests/unit/website/LeagueScheduleViewDataBuilder.test.ts deleted file mode 100644 index 83fcd3e5b..000000000 --- a/tests/unit/website/LeagueScheduleViewDataBuilder.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { LeagueScheduleViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder'; -import type { LeagueScheduleApiDto } from '../../../apps/website/lib/types/tbd/LeagueScheduleApiDto'; - -describe('LeagueScheduleViewDataBuilder', () => { - const mockApiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - track: 'Track A', - car: 'Car A', - sessionType: 'Qualifying', - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-02-08T18:00:00Z', - track: 'Track B', - car: 'Car B', - sessionType: 'Race', - }, - { - id: 'race-3', - name: 'Race 3', - date: '2024-02-15T18:00:00Z', - track: 'Track C', - car: 'Car C', - sessionType: 'Race', - }, - ], - }; - - describe('build()', () => { - it('should transform all races correctly', () => { - const result = LeagueScheduleViewDataBuilder.build(mockApiDto); - - expect(result.leagueId).toBe('league-123'); - expect(result.races).toHaveLength(3); - - // Check first race - expect(result.races[0].id).toBe('race-1'); - expect(result.races[0].name).toBe('Race 1'); - expect(result.races[0].scheduledAt).toBe('2024-02-01T18:00:00Z'); - expect(result.races[0].track).toBe('Track A'); - expect(result.races[0].car).toBe('Car A'); - expect(result.races[0].sessionType).toBe('Qualifying'); - }); - - it('should mark past races correctly', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago - const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-past', - name: 'Past Race', - date: pastDate, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - { - id: 'race-future', - name: 'Future Race', - date: futureDate, - track: 'Track B', - car: 'Car B', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].isPast).toBe(true); - expect(result.races[0].isUpcoming).toBe(false); - expect(result.races[0].status).toBe('completed'); - - expect(result.races[1].isPast).toBe(false); - expect(result.races[1].isUpcoming).toBe(true); - expect(result.races[1].status).toBe('scheduled'); - }); - - it('should mark upcoming races correctly', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-future', - name: 'Future Race', - date: futureDate, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].isPast).toBe(false); - expect(result.races[0].isUpcoming).toBe(true); - expect(result.races[0].status).toBe('scheduled'); - }); - - it('should handle empty schedule', () => { - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.leagueId).toBe('league-123'); - expect(result.races).toHaveLength(0); - }); - - it('should handle races with missing optional fields', () => { - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - track: undefined, - car: undefined, - sessionType: undefined, - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].track).toBeUndefined(); - expect(result.races[0].car).toBeUndefined(); - expect(result.races[0].sessionType).toBeUndefined(); - }); - - it('should handle current driver ID parameter', () => { - const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456'); - - expect(result.currentDriverId).toBe('driver-456'); - }); - - it('should handle admin permission parameter', () => { - const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', true); - - expect(result.isAdmin).toBe(true); - expect(result.races[0].canEdit).toBe(true); - expect(result.races[0].canReschedule).toBe(true); - }); - - it('should handle non-admin permission parameter', () => { - const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', false); - - expect(result.isAdmin).toBe(false); - expect(result.races[0].canEdit).toBe(false); - expect(result.races[0].canReschedule).toBe(false); - }); - - it('should handle default admin parameter as false', () => { - const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456'); - - expect(result.isAdmin).toBe(false); - expect(result.races[0].canEdit).toBe(false); - expect(result.races[0].canReschedule).toBe(false); - }); - - it('should handle registration status for upcoming races', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 86400000).toISOString(); - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-future', - name: 'Future Race', - date: futureDate, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].isUserRegistered).toBe(false); - expect(result.races[0].canRegister).toBe(true); - }); - - it('should handle registration status for past races', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 86400000).toISOString(); - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-past', - name: 'Past Race', - date: pastDate, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].isUserRegistered).toBe(false); - expect(result.races[0].canRegister).toBe(false); - }); - - it('should handle races exactly at current time', () => { - const now = new Date(); - const exactDate = now.toISOString(); - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-exact', - name: 'Exact Race', - date: exactDate, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - // Race at exact current time is considered upcoming (not past) - // because the comparison uses < (strictly less than) - expect(result.races[0].isPast).toBe(false); - expect(result.races[0].isUpcoming).toBe(true); - expect(result.races[0].status).toBe('scheduled'); - }); - - it('should handle races with different session types', () => { - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-qualifying', - name: 'Qualifying', - date: '2024-02-01T18:00:00Z', - track: 'Track A', - car: 'Car A', - sessionType: 'Qualifying', - }, - { - id: 'race-practice', - name: 'Practice', - date: '2024-02-02T18:00:00Z', - track: 'Track B', - car: 'Car B', - sessionType: 'Practice', - }, - { - id: 'race-race', - name: 'Race', - date: '2024-02-03T18:00:00Z', - track: 'Track C', - car: 'Car C', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].sessionType).toBe('Qualifying'); - expect(result.races[1].sessionType).toBe('Practice'); - expect(result.races[2].sessionType).toBe('Race'); - }); - - it('should handle races without session type', () => { - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - track: 'Track A', - car: 'Car A', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].sessionType).toBeUndefined(); - }); - - it('should handle races with empty track and car', () => { - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-02-01T18:00:00Z', - track: '', - car: '', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].track).toBe(''); - expect(result.races[0].car).toBe(''); - }); - - it('should handle multiple races with mixed dates', () => { - const now = new Date(); - const pastDate1 = new Date(now.getTime() - 172800000).toISOString(); // 2 days ago - const pastDate2 = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago - const futureDate1 = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now - const futureDate2 = new Date(now.getTime() + 172800000).toISOString(); // 2 days from now - - const apiDto: LeagueScheduleApiDto = { - leagueId: 'league-123', - races: [ - { - id: 'race-past-2', - name: 'Past Race 2', - date: pastDate1, - track: 'Track A', - car: 'Car A', - sessionType: 'Race', - }, - { - id: 'race-past-1', - name: 'Past Race 1', - date: pastDate2, - track: 'Track B', - car: 'Car B', - sessionType: 'Race', - }, - { - id: 'race-future-1', - name: 'Future Race 1', - date: futureDate1, - track: 'Track C', - car: 'Car C', - sessionType: 'Race', - }, - { - id: 'race-future-2', - name: 'Future Race 2', - date: futureDate2, - track: 'Track D', - car: 'Car D', - sessionType: 'Race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races).toHaveLength(4); - expect(result.races[0].isPast).toBe(true); - expect(result.races[1].isPast).toBe(true); - expect(result.races[2].isPast).toBe(false); - expect(result.races[3].isPast).toBe(false); - }); - }); -}); diff --git a/tests/unit/website/LeagueStandingsViewDataBuilder.test.ts b/tests/unit/website/LeagueStandingsViewDataBuilder.test.ts deleted file mode 100644 index 0a50fe671..000000000 --- a/tests/unit/website/LeagueStandingsViewDataBuilder.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { LeagueStandingsViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder'; -import type { LeagueStandingDTO } from '../../../apps/website/lib/types/generated/LeagueStandingDTO'; -import type { LeagueMemberDTO } from '../../../apps/website/lib/types/generated/LeagueMemberDTO'; - -describe('LeagueStandingsViewDataBuilder', () => { - const mockStandings: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: 0, - lastRacePoints: 25, - droppedRaceIds: ['race-1', 'race-2'], - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Jane Smith', - iracingId: '67890', - country: 'UK', - joinedAt: '2024-01-02T00:00:00Z', - }, - points: 120, - position: 2, - wins: 2, - podiums: 4, - races: 10, - positionChange: 1, - lastRacePoints: 18, - droppedRaceIds: ['race-3'], - }, - { - driverId: 'driver-3', - driver: { - id: 'driver-3', - name: 'Bob Wilson', - iracingId: '11111', - country: 'CA', - joinedAt: '2024-01-03T00:00:00Z', - }, - points: 90, - position: 3, - wins: 1, - podiums: 3, - races: 10, - positionChange: -1, - lastRacePoints: 12, - droppedRaceIds: [], - }, - ]; - - const mockMemberships: LeagueMemberDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - role: 'member', - joinedAt: '2024-01-01T00:00:00Z', - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Jane Smith', - iracingId: '67890', - country: 'UK', - joinedAt: '2024-01-02T00:00:00Z', - }, - role: 'member', - joinedAt: '2024-01-02T00:00:00Z', - }, - ]; - - describe('build()', () => { - it('should transform standings correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.leagueId).toBe('league-123'); - expect(result.standings).toHaveLength(3); - - // Check first standing - expect(result.standings[0].driverId).toBe('driver-1'); - expect(result.standings[0].position).toBe(1); - expect(result.standings[0].totalPoints).toBe(150); - expect(result.standings[0].racesFinished).toBe(10); - expect(result.standings[0].racesStarted).toBe(10); - expect(result.standings[0].positionChange).toBe(0); - expect(result.standings[0].lastRacePoints).toBe(25); - expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']); - expect(result.standings[0].wins).toBe(3); - expect(result.standings[0].podiums).toBe(5); - }); - - it('should calculate position change correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].positionChange).toBe(0); // No change - expect(result.standings[1].positionChange).toBe(1); // Moved up - expect(result.standings[2].positionChange).toBe(-1); // Moved down - }); - - it('should map last race points correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].lastRacePoints).toBe(25); - expect(result.standings[1].lastRacePoints).toBe(18); - expect(result.standings[2].lastRacePoints).toBe(12); - }); - - it('should handle dropped race IDs correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']); - expect(result.standings[1].droppedRaceIds).toEqual(['race-3']); - expect(result.standings[2].droppedRaceIds).toEqual([]); - }); - - it('should calculate championship stats (wins, podiums)', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].wins).toBe(3); - expect(result.standings[0].podiums).toBe(5); - expect(result.standings[1].wins).toBe(2); - expect(result.standings[1].podiums).toBe(4); - expect(result.standings[2].wins).toBe(1); - expect(result.standings[2].podiums).toBe(3); - }); - - it('should extract driver metadata correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.drivers).toHaveLength(3); - - // Check first driver - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[0].name).toBe('John Doe'); - expect(result.drivers[0].iracingId).toBe('12345'); - expect(result.drivers[0].country).toBe('US'); - }); - - it('should convert memberships correctly', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.memberships).toHaveLength(2); - - // Check first membership - expect(result.memberships[0].driverId).toBe('driver-1'); - expect(result.memberships[0].leagueId).toBe('league-123'); - expect(result.memberships[0].role).toBe('member'); - expect(result.memberships[0].status).toBe('active'); - }); - - it('should handle empty standings', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: [] }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings).toHaveLength(0); - expect(result.drivers).toHaveLength(0); - }); - - it('should handle empty memberships', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: [] }, - 'league-123' - ); - - expect(result.memberships).toHaveLength(0); - }); - - it('should handle missing driver objects in standings', () => { - const standingsWithMissingDriver: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: 0, - lastRacePoints: 25, - droppedRaceIds: [], - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Jane Smith', - iracingId: '67890', - country: 'UK', - joinedAt: '2024-01-02T00:00:00Z', - }, - points: 120, - position: 2, - wins: 2, - podiums: 4, - races: 10, - positionChange: 1, - lastRacePoints: 18, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithMissingDriver }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.drivers).toHaveLength(2); - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[1].id).toBe('driver-2'); - }); - - it('should handle standings with missing positionChange', () => { - const standingsWithoutPositionChange: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: undefined as any, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithoutPositionChange }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].positionChange).toBe(0); - }); - - it('should handle standings with missing lastRacePoints', () => { - const standingsWithoutLastRacePoints: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: 0, - lastRacePoints: undefined as any, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithoutLastRacePoints }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].lastRacePoints).toBe(0); - }); - - it('should handle standings with missing droppedRaceIds', () => { - const standingsWithoutDroppedRaceIds: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: 0, - lastRacePoints: 25, - droppedRaceIds: undefined as any, - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithoutDroppedRaceIds }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].droppedRaceIds).toEqual([]); - }); - - it('should handle standings with missing wins', () => { - const standingsWithoutWins: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: undefined as any, - podiums: 5, - races: 10, - positionChange: 0, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithoutWins }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].wins).toBe(0); - }); - - it('should handle standings with missing podiums', () => { - const standingsWithoutPodiums: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: undefined as any, - races: 10, - positionChange: 0, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithoutPodiums }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].podiums).toBe(0); - }); - - it('should handle team championship mode', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123', - true - ); - - expect(result.isTeamChampionship).toBe(true); - }); - - it('should handle non-team championship mode by default', () => { - const result = LeagueStandingsViewDataBuilder.build( - { standings: mockStandings }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.isTeamChampionship).toBe(false); - }); - - it('should handle standings with zero points', () => { - const standingsWithZeroPoints: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 0, - position: 1, - wins: 0, - podiums: 0, - races: 10, - positionChange: 0, - lastRacePoints: 0, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithZeroPoints }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].totalPoints).toBe(0); - expect(result.standings[0].wins).toBe(0); - expect(result.standings[0].podiums).toBe(0); - }); - - it('should handle standings with negative position change', () => { - const standingsWithNegativeChange: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: -2, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithNegativeChange }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].positionChange).toBe(-2); - }); - - it('should handle standings with positive position change', () => { - const standingsWithPositiveChange: LeagueStandingDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'John Doe', - iracingId: '12345', - country: 'US', - joinedAt: '2024-01-01T00:00:00Z', - }, - points: 150, - position: 1, - wins: 3, - podiums: 5, - races: 10, - positionChange: 3, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ]; - - const result = LeagueStandingsViewDataBuilder.build( - { standings: standingsWithPositiveChange }, - { members: mockMemberships }, - 'league-123' - ); - - expect(result.standings[0].positionChange).toBe(3); - }); - }); -}); diff --git a/tests/unit/website/LeaguesViewDataBuilder.test.ts b/tests/unit/website/LeaguesViewDataBuilder.test.ts deleted file mode 100644 index 13950a247..000000000 --- a/tests/unit/website/LeaguesViewDataBuilder.test.ts +++ /dev/null @@ -1,932 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { LeaguesViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeaguesViewDataBuilder'; -import type { AllLeaguesWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO'; -import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO'; - -describe('LeaguesViewDataBuilder', () => { - const mockLeagues: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League 1', - description: 'A test league description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - socialLinks: { - discordUrl: 'https://discord.gg/test1', - youtubeUrl: 'https://youtube.com/test1', - websiteUrl: 'https://test1.com', - }, - scoring: { - gameId: 'game-1', - gameName: 'Test Game 1', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - logoUrl: 'https://logo.com/test1.png', - pendingJoinRequestsCount: 3, - pendingProtestsCount: 1, - walletBalance: 1000, - }, - { - id: 'league-2', - name: 'Test League 2', - description: 'Another test league', - ownerId: 'owner-2', - createdAt: '2024-01-02T00:00:00Z', - settings: { - maxDrivers: 16, - qualifyingFormat: 'Team', - }, - usedSlots: 8, - category: 'Oval', - socialLinks: { - discordUrl: 'https://discord.gg/test2', - }, - scoring: { - gameId: 'game-2', - gameName: 'Test Game 2', - primaryChampionshipType: 'Team', - scoringPresetId: 'preset-2', - scoringPresetName: 'Advanced', - dropPolicySummary: 'Drop 1 worst race', - scoringPatternSummary: 'Points based on finish position with bonuses', - }, - timingSummary: 'Every Saturday at 7 PM', - logoUrl: 'https://logo.com/test2.png', - }, - { - id: 'league-3', - name: 'Test League 3', - description: 'A third test league', - ownerId: 'owner-3', - createdAt: '2024-01-03T00:00:00Z', - settings: { - maxDrivers: 24, - qualifyingFormat: 'Solo', - }, - usedSlots: 24, - category: 'Road', - scoring: { - gameId: 'game-3', - gameName: 'Test Game 3', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-3', - scoringPresetName: 'Custom', - dropPolicySummary: 'No drops', - scoringPatternSummary: 'Fixed points per position', - }, - timingSummary: 'Every Friday at 9 PM', - }, - ]; - - describe('build()', () => { - it('should transform all leagues correctly', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues).toHaveLength(3); - - // Check first league - expect(result.leagues[0].id).toBe('league-1'); - expect(result.leagues[0].name).toBe('Test League 1'); - expect(result.leagues[0].description).toBe('A test league description'); - expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png'); - expect(result.leagues[0].ownerId).toBe('owner-1'); - expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z'); - expect(result.leagues[0].maxDrivers).toBe(32); - expect(result.leagues[0].usedDriverSlots).toBe(15); - expect(result.leagues[0].structureSummary).toBe('Solo'); - expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM'); - expect(result.leagues[0].category).toBe('Road'); - - // Check scoring - expect(result.leagues[0].scoring).toBeDefined(); - expect(result.leagues[0].scoring?.gameId).toBe('game-1'); - expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1'); - expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo'); - expect(result.leagues[0].scoring?.scoringPresetId).toBe('preset-1'); - expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard'); - expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races'); - expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position'); - }); - - it('should handle leagues with missing description', () => { - const leaguesWithoutDescription: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: '', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutDescription, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].description).toBe(null); - }); - - it('should handle leagues with missing logoUrl', () => { - const leaguesWithoutLogo: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutLogo, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].logoUrl).toBe(null); - }); - - it('should handle leagues with missing category', () => { - const leaguesWithoutCategory: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutCategory, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].category).toBe(null); - }); - - it('should handle leagues with missing scoring', () => { - const leaguesWithoutScoring: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutScoring, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring).toBeUndefined(); - }); - - it('should handle leagues with missing social links', () => { - const leaguesWithoutSocialLinks: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutSocialLinks, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0]).toBeDefined(); - }); - - it('should handle leagues with missing timingSummary', () => { - const leaguesWithoutTimingSummary: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutTimingSummary, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].timingSummary).toBe(''); - }); - - it('should handle empty leagues array', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [], - totalCount: 0, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues).toHaveLength(0); - }); - - it('should handle leagues with different categories', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].category).toBe('Road'); - expect(result.leagues[1].category).toBe('Oval'); - expect(result.leagues[2].category).toBe('Road'); - }); - - it('should handle leagues with different structures', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].structureSummary).toBe('Solo'); - expect(result.leagues[1].structureSummary).toBe('Team'); - expect(result.leagues[2].structureSummary).toBe('Solo'); - }); - - it('should handle leagues with different scoring presets', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard'); - expect(result.leagues[1].scoring?.scoringPresetName).toBe('Advanced'); - expect(result.leagues[2].scoring?.scoringPresetName).toBe('Custom'); - }); - - it('should handle leagues with different drop policies', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races'); - expect(result.leagues[1].scoring?.dropPolicySummary).toBe('Drop 1 worst race'); - expect(result.leagues[2].scoring?.dropPolicySummary).toBe('No drops'); - }); - - it('should handle leagues with different scoring patterns', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position'); - expect(result.leagues[1].scoring?.scoringPatternSummary).toBe('Points based on finish position with bonuses'); - expect(result.leagues[2].scoring?.scoringPatternSummary).toBe('Fixed points per position'); - }); - - it('should handle leagues with different primary championship types', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo'); - expect(result.leagues[1].scoring?.primaryChampionshipType).toBe('Team'); - expect(result.leagues[2].scoring?.primaryChampionshipType).toBe('Solo'); - }); - - it('should handle leagues with different game names', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1'); - expect(result.leagues[1].scoring?.gameName).toBe('Test Game 2'); - expect(result.leagues[2].scoring?.gameName).toBe('Test Game 3'); - }); - - it('should handle leagues with different game IDs', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].scoring?.gameId).toBe('game-1'); - expect(result.leagues[1].scoring?.gameId).toBe('game-2'); - expect(result.leagues[2].scoring?.gameId).toBe('game-3'); - }); - - it('should handle leagues with different max drivers', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].maxDrivers).toBe(32); - expect(result.leagues[1].maxDrivers).toBe(16); - expect(result.leagues[2].maxDrivers).toBe(24); - }); - - it('should handle leagues with different used slots', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].usedDriverSlots).toBe(15); - expect(result.leagues[1].usedDriverSlots).toBe(8); - expect(result.leagues[2].usedDriverSlots).toBe(24); - }); - - it('should handle leagues with different owners', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].ownerId).toBe('owner-1'); - expect(result.leagues[1].ownerId).toBe('owner-2'); - expect(result.leagues[2].ownerId).toBe('owner-3'); - }); - - it('should handle leagues with different creation dates', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z'); - expect(result.leagues[1].createdAt).toBe('2024-01-02T00:00:00Z'); - expect(result.leagues[2].createdAt).toBe('2024-01-03T00:00:00Z'); - }); - - it('should handle leagues with different timing summaries', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM'); - expect(result.leagues[1].timingSummary).toBe('Every Saturday at 7 PM'); - expect(result.leagues[2].timingSummary).toBe('Every Friday at 9 PM'); - }); - - it('should handle leagues with different names', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].name).toBe('Test League 1'); - expect(result.leagues[1].name).toBe('Test League 2'); - expect(result.leagues[2].name).toBe('Test League 3'); - }); - - it('should handle leagues with different descriptions', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].description).toBe('A test league description'); - expect(result.leagues[1].description).toBe('Another test league'); - expect(result.leagues[2].description).toBe('A third test league'); - }); - - it('should handle leagues with different logo URLs', () => { - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: mockLeagues, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png'); - expect(result.leagues[1].logoUrl).toBe('https://logo.com/test2.png'); - expect(result.leagues[2].logoUrl).toBeNull(); - }); - - it('should handle leagues with activeDriversCount', () => { - const leaguesWithActiveDrivers: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - // Add activeDriversCount to the league - (leaguesWithActiveDrivers[0] as any).activeDriversCount = 12; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithActiveDrivers, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].activeDriversCount).toBe(12); - }); - - it('should handle leagues with nextRaceAt', () => { - const leaguesWithNextRace: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - // Add nextRaceAt to the league - (leaguesWithNextRace[0] as any).nextRaceAt = '2024-02-01T18:00:00Z'; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithNextRace, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].nextRaceAt).toBe('2024-02-01T18:00:00Z'); - }); - - it('should handle leagues without activeDriversCount and nextRaceAt', () => { - const leaguesWithoutMetadata: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Test League', - description: 'A test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithoutMetadata, - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - expect(result.leagues[0].activeDriversCount).toBeUndefined(); - expect(result.leagues[0].nextRaceAt).toBeUndefined(); - }); - - it('should handle leagues with different usedDriverSlots for featured leagues', () => { - const leaguesWithDifferentSlots: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Small League', - description: 'A small league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 16, - qualifyingFormat: 'Solo', - }, - usedSlots: 8, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - { - id: 'league-2', - name: 'Large League', - description: 'A large league', - ownerId: 'owner-2', - createdAt: '2024-01-02T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 25, - category: 'Road', - scoring: { - gameId: 'game-2', - gameName: 'Test Game 2', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-2', - scoringPresetName: 'Advanced', - dropPolicySummary: 'Drop 1 worst race', - scoringPatternSummary: 'Points based on finish position with bonuses', - }, - timingSummary: 'Every Saturday at 7 PM', - }, - { - id: 'league-3', - name: 'Medium League', - description: 'A medium league', - ownerId: 'owner-3', - createdAt: '2024-01-03T00:00:00Z', - settings: { - maxDrivers: 24, - qualifyingFormat: 'Team', - }, - usedSlots: 20, - category: 'Oval', - scoring: { - gameId: 'game-3', - gameName: 'Test Game 3', - primaryChampionshipType: 'Team', - scoringPresetId: 'preset-3', - scoringPresetName: 'Custom', - dropPolicySummary: 'No drops', - scoringPatternSummary: 'Fixed points per position', - }, - timingSummary: 'Every Friday at 9 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithDifferentSlots, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - // Verify that usedDriverSlots is correctly mapped - expect(result.leagues[0].usedDriverSlots).toBe(8); - expect(result.leagues[1].usedDriverSlots).toBe(25); - expect(result.leagues[2].usedDriverSlots).toBe(20); - - // Verify that leagues can be filtered for featured leagues (usedDriverSlots > 20) - const featuredLeagues = result.leagues.filter(l => (l.usedDriverSlots ?? 0) > 20); - expect(featuredLeagues).toHaveLength(1); - expect(featuredLeagues[0].id).toBe('league-2'); - }); - - it('should handle leagues with different categories for filtering', () => { - const leaguesWithDifferentCategories: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'Road League 1', - description: 'A road league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - { - id: 'league-2', - name: 'Oval League 1', - description: 'An oval league', - ownerId: 'owner-2', - createdAt: '2024-01-02T00:00:00Z', - settings: { - maxDrivers: 16, - qualifyingFormat: 'Solo', - }, - usedSlots: 8, - category: 'Oval', - scoring: { - gameId: 'game-2', - gameName: 'Test Game 2', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-2', - scoringPresetName: 'Advanced', - dropPolicySummary: 'Drop 1 worst race', - scoringPatternSummary: 'Points based on finish position with bonuses', - }, - timingSummary: 'Every Saturday at 7 PM', - }, - { - id: 'league-3', - name: 'Road League 2', - description: 'Another road league', - ownerId: 'owner-3', - createdAt: '2024-01-03T00:00:00Z', - settings: { - maxDrivers: 24, - qualifyingFormat: 'Team', - }, - usedSlots: 20, - category: 'Road', - scoring: { - gameId: 'game-3', - gameName: 'Test Game 3', - primaryChampionshipType: 'Team', - scoringPresetId: 'preset-3', - scoringPresetName: 'Custom', - dropPolicySummary: 'No drops', - scoringPatternSummary: 'Fixed points per position', - }, - timingSummary: 'Every Friday at 9 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithDifferentCategories, - totalCount: 3, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - // Verify that category is correctly mapped - expect(result.leagues[0].category).toBe('Road'); - expect(result.leagues[1].category).toBe('Oval'); - expect(result.leagues[2].category).toBe('Road'); - - // Verify that leagues can be filtered by category - const roadLeagues = result.leagues.filter(l => l.category === 'Road'); - expect(roadLeagues).toHaveLength(2); - expect(roadLeagues[0].id).toBe('league-1'); - expect(roadLeagues[1].id).toBe('league-3'); - - const ovalLeagues = result.leagues.filter(l => l.category === 'Oval'); - expect(ovalLeagues).toHaveLength(1); - expect(ovalLeagues[0].id).toBe('league-2'); - }); - - it('should handle leagues with null category for filtering', () => { - const leaguesWithNullCategory: LeagueWithCapacityAndScoringDTO[] = [ - { - id: 'league-1', - name: 'League with Category', - description: 'A league with category', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo', - }, - usedSlots: 15, - category: 'Road', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Every Sunday at 8 PM', - }, - { - id: 'league-2', - name: 'League without Category', - description: 'A league without category', - ownerId: 'owner-2', - createdAt: '2024-01-02T00:00:00Z', - settings: { - maxDrivers: 16, - qualifyingFormat: 'Solo', - }, - usedSlots: 8, - scoring: { - gameId: 'game-2', - gameName: 'Test Game 2', - primaryChampionshipType: 'Solo', - scoringPresetId: 'preset-2', - scoringPresetName: 'Advanced', - dropPolicySummary: 'Drop 1 worst race', - scoringPatternSummary: 'Points based on finish position with bonuses', - }, - timingSummary: 'Every Saturday at 7 PM', - }, - ]; - - const apiDto: AllLeaguesWithCapacityAndScoringDTO = { - leagues: leaguesWithNullCategory, - totalCount: 2, - }; - - const result = LeaguesViewDataBuilder.build(apiDto); - - // Verify that null category is handled correctly - expect(result.leagues[0].category).toBe('Road'); - expect(result.leagues[1].category).toBe(null); - - // Verify that leagues can be filtered by category (null category should be filterable) - const roadLeagues = result.leagues.filter(l => l.category === 'Road'); - expect(roadLeagues).toHaveLength(1); - expect(roadLeagues[0].id).toBe('league-1'); - - const noCategoryLeagues = result.leagues.filter(l => l.category === null); - expect(noCategoryLeagues).toHaveLength(1); - expect(noCategoryLeagues[0].id).toBe('league-2'); - }); - }); -}); diff --git a/tests/unit/website/RouteConfig.test.ts b/tests/unit/website/RouteConfig.test.ts deleted file mode 100644 index ff59a44c7..000000000 --- a/tests/unit/website/RouteConfig.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { routeMatchers } from '../../../apps/website/lib/routing/RouteConfig'; - -describe('RouteConfig - routeMatchers Invariants', () => { - describe('isPublic()', () => { - it('should return true for exact public matches', () => { - expect(routeMatchers.isPublic('/')).toBe(true); - expect(routeMatchers.isPublic('/leagues')).toBe(true); - expect(routeMatchers.isPublic('/auth/login')).toBe(true); - }); - - it('should return true for top-level detail pages (league, race, driver, team)', () => { - expect(routeMatchers.isPublic('/leagues/123')).toBe(true); - expect(routeMatchers.isPublic('/races/456')).toBe(true); - expect(routeMatchers.isPublic('/drivers/789')).toBe(true); - expect(routeMatchers.isPublic('/teams/abc')).toBe(true); - }); - - it('should return false for "leagues/create" and "teams/create" (protected)', () => { - expect(routeMatchers.isPublic('/leagues/create')).toBe(false); - expect(routeMatchers.isPublic('/teams/create')).toBe(false); - }); - - it('should return false for nested protected routes', () => { - expect(routeMatchers.isPublic('/dashboard')).toBe(false); - expect(routeMatchers.isPublic('/profile/settings')).toBe(false); - expect(routeMatchers.isPublic('/admin/users')).toBe(false); - expect(routeMatchers.isPublic('/sponsor/dashboard')).toBe(false); - }); - - it('should return true for sponsor signup (public)', () => { - expect(routeMatchers.isPublic('/sponsor/signup')).toBe(true); - }); - - it('should return false for unknown routes', () => { - expect(routeMatchers.isPublic('/unknown-route')).toBe(false); - expect(routeMatchers.isPublic('/api/something')).toBe(false); - }); - }); - - describe('requiresRole()', () => { - it('should return admin roles for admin routes', () => { - const roles = routeMatchers.requiresRole('/admin'); - expect(roles).toContain('admin'); - expect(roles).toContain('super-admin'); - - const userRoles = routeMatchers.requiresRole('/admin/users'); - expect(userRoles).toEqual(roles); - }); - - it('should return sponsor role for sponsor routes', () => { - expect(routeMatchers.requiresRole('/sponsor/dashboard')).toEqual(['sponsor']); - expect(routeMatchers.requiresRole('/sponsor/billing')).toEqual(['sponsor']); - }); - - it('should return null for public routes', () => { - expect(routeMatchers.requiresRole('/')).toBeNull(); - expect(routeMatchers.requiresRole('/leagues')).toBeNull(); - }); - - it('should return null for non-role protected routes', () => { - expect(routeMatchers.requiresRole('/dashboard')).toBeNull(); - expect(routeMatchers.requiresRole('/profile')).toBeNull(); - }); - - it('should return null for sponsor signup (public)', () => { - expect(routeMatchers.requiresRole('/sponsor/signup')).toBeNull(); - }); - }); -}); diff --git a/tests/unit/website/WebsiteRouteManager.test.ts b/tests/unit/website/WebsiteRouteManager.test.ts deleted file mode 100644 index 966b9fd46..000000000 --- a/tests/unit/website/WebsiteRouteManager.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager'; -import { routes } from '../../../apps/website/lib/routing/RouteConfig'; - -describe('WebsiteRouteManager - Route Classification Contract', () => { - const routeManager = new WebsiteRouteManager(); - - describe('getAccessLevel()', () => { - it('should correctly classify public routes', () => { - expect(routeManager.getAccessLevel('/')).toBe('public'); - expect(routeManager.getAccessLevel('/auth/login')).toBe('public'); - expect(routeManager.getAccessLevel('/leagues')).toBe('public'); - }); - - it('should correctly classify dashboard routes as auth', () => { - expect(routeManager.getAccessLevel('/dashboard')).toBe('auth'); - expect(routeManager.getAccessLevel('/profile')).toBe('auth'); - }); - - it('should correctly classify admin routes', () => { - expect(routeManager.getAccessLevel('/admin')).toBe('admin'); - expect(routeManager.getAccessLevel('/admin/users')).toBe('admin'); - }); - - it('should correctly classify sponsor routes', () => { - expect(routeManager.getAccessLevel('/sponsor')).toBe('sponsor'); - expect(routeManager.getAccessLevel('/sponsor/dashboard')).toBe('sponsor'); - }); - - it('should correctly classify dynamic route patterns', () => { - // League detail is public - expect(routeManager.getAccessLevel('/leagues/any-id')).toBe('public'); - expect(routeManager.getAccessLevel('/races/any-id')).toBe('public'); - - // Nested protected routes - expect(routeManager.getAccessLevel('/leagues/any-id/settings')).toBe('auth'); - }); - }); - - describe('RouteConfig Contract', () => { - it('should fail loudly if RouteConfig paths change unexpectedly', () => { - // These assertions act as a contract. If the paths change in RouteConfig, - // these tests will fail, forcing a conscious update of the contract. - expect(routes.public.home).toBe('/'); - expect(routes.auth.login).toBe('/auth/login'); - expect(routes.protected.dashboard).toBe('/dashboard'); - expect(routes.admin.root).toBe('/admin'); - expect(routes.sponsor.root).toBe('/sponsor'); - - // Dynamic patterns - expect(routes.league.detail('test-id')).toBe('/leagues/test-id'); - expect(routes.league.scheduleAdmin('test-id')).toBe('/leagues/test-id/schedule/admin'); - }); - }); - - describe('Representative Subset Verification', () => { - const testCases = [ - { path: '/', expected: 'public' }, - { path: '/auth/login', expected: 'public' }, - { path: '/dashboard', expected: 'auth' }, - { path: '/admin', expected: 'admin' }, - { path: '/sponsor', expected: 'sponsor' }, - { path: '/leagues/123', expected: 'public' }, - { path: '/races/456', expected: 'public' }, - ]; - - testCases.forEach(({ path, expected }) => { - it(`should classify ${path} as ${expected}`, () => { - expect(routeManager.getAccessLevel(path)).toBe(expected); - }); - }); - }); -}); diff --git a/tests/unit/website/apiBaseUrl.test.ts b/tests/unit/website/apiBaseUrl.test.ts deleted file mode 100644 index e8e6e3cf8..000000000 --- a/tests/unit/website/apiBaseUrl.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { getWebsiteApiBaseUrl } from '../../../apps/website/lib/config/apiBaseUrl'; - -describe('getWebsiteApiBaseUrl()', () => { - const originalEnv = process.env; - - beforeEach(() => { - vi.resetModules(); - process.env = { ...originalEnv }; - // Clear relevant env vars - delete process.env.NEXT_PUBLIC_API_BASE_URL; - delete process.env.API_BASE_URL; - delete process.env.NODE_ENV; - delete process.env.CI; - delete process.env.DOCKER; - }); - - afterEach(() => { - process.env = originalEnv; - vi.unstubAllGlobals(); - }); - - describe('Browser Context', () => { - beforeEach(() => { - vi.stubGlobal('window', {}); - }); - - it('should use NEXT_PUBLIC_API_BASE_URL if provided', () => { - process.env.NEXT_PUBLIC_API_BASE_URL = 'https://api.example.com/'; - expect(getWebsiteApiBaseUrl()).toBe('https://api.example.com'); - }); - - it('should throw if missing env in test-like environment (CI)', () => { - process.env.CI = 'true'; - expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing NEXT_PUBLIC_API_BASE_URL/); - }); - - it('should throw if missing env in test-like environment (DOCKER)', () => { - process.env.DOCKER = 'true'; - expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing NEXT_PUBLIC_API_BASE_URL/); - }); - - it('should fallback to localhost in development (non-docker)', () => { - process.env.NODE_ENV = 'development'; - expect(getWebsiteApiBaseUrl()).toBe('http://localhost:3001'); - }); - }); - - describe('Server Context', () => { - beforeEach(() => { - vi.stubGlobal('window', undefined); - }); - - it('should prioritize API_BASE_URL over NEXT_PUBLIC_API_BASE_URL', () => { - process.env.API_BASE_URL = 'https://internal-api.example.com'; - process.env.NEXT_PUBLIC_API_BASE_URL = 'https://public-api.example.com'; - expect(getWebsiteApiBaseUrl()).toBe('https://internal-api.example.com'); - }); - - it('should use NEXT_PUBLIC_API_BASE_URL if API_BASE_URL is missing', () => { - process.env.NEXT_PUBLIC_API_BASE_URL = 'https://public-api.example.com'; - expect(getWebsiteApiBaseUrl()).toBe('https://public-api.example.com'); - }); - - it('should throw if missing env in test-like environment (CI)', () => { - process.env.CI = 'true'; - expect(() => getWebsiteApiBaseUrl()).toThrow(/Missing API_BASE_URL/); - }); - - it('should fallback to api:3000 in production (non-test environment)', () => { - process.env.NODE_ENV = 'production'; - expect(getWebsiteApiBaseUrl()).toBe('http://api:3000'); - }); - }); -});