diff --git a/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.test.ts new file mode 100644 index 000000000..9ccfe35f9 --- /dev/null +++ b/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from 'vitest'; +import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder'; +import type { DashboardStats } from '@/lib/types/admin'; + +describe('AdminDashboardViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => { + const dashboardStats: DashboardStats = { + totalUsers: 1000, + activeUsers: 800, + suspendedUsers: 50, + deletedUsers: 150, + systemAdmins: 5, + recentLogins: 120, + newUsersToday: 15, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result).toEqual({ + stats: { + totalUsers: 1000, + activeUsers: 800, + suspendedUsers: 50, + deletedUsers: 150, + systemAdmins: 5, + recentLogins: 120, + newUsersToday: 15, + }, + }); + }); + + it('should handle zero values correctly', () => { + const dashboardStats: DashboardStats = { + totalUsers: 0, + activeUsers: 0, + suspendedUsers: 0, + deletedUsers: 0, + systemAdmins: 0, + recentLogins: 0, + newUsersToday: 0, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result).toEqual({ + stats: { + totalUsers: 0, + activeUsers: 0, + suspendedUsers: 0, + deletedUsers: 0, + systemAdmins: 0, + recentLogins: 0, + newUsersToday: 0, + }, + }); + }); + + it('should handle large numbers correctly', () => { + const dashboardStats: DashboardStats = { + totalUsers: 1000000, + activeUsers: 750000, + suspendedUsers: 25000, + deletedUsers: 225000, + systemAdmins: 50, + recentLogins: 50000, + newUsersToday: 1000, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result.stats.totalUsers).toBe(1000000); + expect(result.stats.activeUsers).toBe(750000); + expect(result.stats.systemAdmins).toBe(50); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const dashboardStats: DashboardStats = { + totalUsers: 500, + activeUsers: 400, + suspendedUsers: 25, + deletedUsers: 75, + systemAdmins: 3, + recentLogins: 80, + newUsersToday: 10, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers); + expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers); + expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers); + expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers); + expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins); + expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins); + expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday); + }); + + it('should not modify the input DTO', () => { + const dashboardStats: DashboardStats = { + totalUsers: 100, + activeUsers: 80, + suspendedUsers: 5, + deletedUsers: 15, + systemAdmins: 2, + recentLogins: 20, + newUsersToday: 5, + }; + + const originalStats = { ...dashboardStats }; + AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(dashboardStats).toEqual(originalStats); + }); + }); + + describe('edge cases', () => { + it('should handle negative values (if API returns them)', () => { + const dashboardStats: DashboardStats = { + totalUsers: -1, + activeUsers: -1, + suspendedUsers: -1, + deletedUsers: -1, + systemAdmins: -1, + recentLogins: -1, + newUsersToday: -1, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result.stats.totalUsers).toBe(-1); + expect(result.stats.activeUsers).toBe(-1); + }); + + it('should handle very large numbers', () => { + const dashboardStats: DashboardStats = { + totalUsers: Number.MAX_SAFE_INTEGER, + activeUsers: Number.MAX_SAFE_INTEGER - 1000, + suspendedUsers: 100, + deletedUsers: 100, + systemAdmins: 10, + recentLogins: 1000, + newUsersToday: 100, + }; + + const result = AdminDashboardViewDataBuilder.build(dashboardStats); + + expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER); + expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000); + }); + }); +}); diff --git a/apps/website/tests/view-data/admin.test.ts b/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.test.ts similarity index 79% rename from apps/website/tests/view-data/admin.test.ts rename to apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.test.ts index ce01ddf89..a7b1d4be5 100644 --- a/apps/website/tests/view-data/admin.test.ts +++ b/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.test.ts @@ -1,181 +1,7 @@ -/** - * View Data Layer Tests - Admin Functionality - * - * This test file covers the view data layer for admin functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Admin dashboard data transformation - * - User management view models - * - Admin-specific formatting and validation - * - Derived fields for admin UI components - * - Default values and fallbacks for admin views - */ - -import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder'; -import { AdminUsersViewDataBuilder } from '@/lib/builders/view-data/AdminUsersViewDataBuilder'; -import type { DashboardStats } from '@/lib/types/admin'; +import { describe, it, expect } from 'vitest'; +import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder'; import type { UserListResponse } from '@/lib/types/admin'; -describe('AdminDashboardViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => { - const dashboardStats: DashboardStats = { - totalUsers: 1000, - activeUsers: 800, - suspendedUsers: 50, - deletedUsers: 150, - systemAdmins: 5, - recentLogins: 120, - newUsersToday: 15, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result).toEqual({ - stats: { - totalUsers: 1000, - activeUsers: 800, - suspendedUsers: 50, - deletedUsers: 150, - systemAdmins: 5, - recentLogins: 120, - newUsersToday: 15, - }, - }); - }); - - it('should handle zero values correctly', () => { - const dashboardStats: DashboardStats = { - totalUsers: 0, - activeUsers: 0, - suspendedUsers: 0, - deletedUsers: 0, - systemAdmins: 0, - recentLogins: 0, - newUsersToday: 0, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result).toEqual({ - stats: { - totalUsers: 0, - activeUsers: 0, - suspendedUsers: 0, - deletedUsers: 0, - systemAdmins: 0, - recentLogins: 0, - newUsersToday: 0, - }, - }); - }); - - it('should handle large numbers correctly', () => { - const dashboardStats: DashboardStats = { - totalUsers: 1000000, - activeUsers: 750000, - suspendedUsers: 25000, - deletedUsers: 225000, - systemAdmins: 50, - recentLogins: 50000, - newUsersToday: 1000, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result.stats.totalUsers).toBe(1000000); - expect(result.stats.activeUsers).toBe(750000); - expect(result.stats.systemAdmins).toBe(50); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const dashboardStats: DashboardStats = { - totalUsers: 500, - activeUsers: 400, - suspendedUsers: 25, - deletedUsers: 75, - systemAdmins: 3, - recentLogins: 80, - newUsersToday: 10, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers); - expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers); - expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers); - expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers); - expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins); - expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins); - expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday); - }); - - it('should not modify the input DTO', () => { - const dashboardStats: DashboardStats = { - totalUsers: 100, - activeUsers: 80, - suspendedUsers: 5, - deletedUsers: 15, - systemAdmins: 2, - recentLogins: 20, - newUsersToday: 5, - }; - - const originalStats = { ...dashboardStats }; - AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(dashboardStats).toEqual(originalStats); - }); - }); - - describe('edge cases', () => { - it('should handle negative values (if API returns them)', () => { - const dashboardStats: DashboardStats = { - totalUsers: -1, - activeUsers: -1, - suspendedUsers: -1, - deletedUsers: -1, - systemAdmins: -1, - recentLogins: -1, - newUsersToday: -1, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result.stats.totalUsers).toBe(-1); - expect(result.stats.activeUsers).toBe(-1); - }); - - it('should handle very large numbers', () => { - const dashboardStats: DashboardStats = { - totalUsers: Number.MAX_SAFE_INTEGER, - activeUsers: Number.MAX_SAFE_INTEGER - 1000, - suspendedUsers: 100, - deletedUsers: 100, - systemAdmins: 10, - recentLogins: 1000, - newUsersToday: 100, - }; - - const result = AdminDashboardViewDataBuilder.build(dashboardStats); - - expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER); - expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000); - }); - }); -}); - describe('AdminUsersViewDataBuilder', () => { describe('happy paths', () => { it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => { diff --git a/apps/website/lib/builders/view-data/AuthViewDataConsistency.test.ts b/apps/website/lib/builders/view-data/AuthViewDataConsistency.test.ts new file mode 100644 index 000000000..0794cf8c5 --- /dev/null +++ b/apps/website/lib/builders/view-data/AuthViewDataConsistency.test.ts @@ -0,0 +1,249 @@ +import { describe, it, expect } from 'vitest'; +import { LoginViewDataBuilder } from './LoginViewDataBuilder'; +import { SignupViewDataBuilder } from './SignupViewDataBuilder'; +import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder'; +import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder'; +import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO'; +import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO'; +import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO'; +import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO'; + +describe('Auth View Data - Cross-Builder Consistency', () => { + describe('common patterns', () => { + it('should all initialize with isSubmitting false', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.isSubmitting).toBe(false); + expect(signupResult.isSubmitting).toBe(false); + expect(forgotPasswordResult.isSubmitting).toBe(false); + expect(resetPasswordResult.isSubmitting).toBe(false); + }); + + it('should all initialize with submitError undefined', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.submitError).toBeUndefined(); + expect(signupResult.submitError).toBeUndefined(); + expect(forgotPasswordResult.submitError).toBeUndefined(); + expect(resetPasswordResult.submitError).toBeUndefined(); + }); + + it('should all initialize formState.isValid as true', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.isValid).toBe(true); + expect(signupResult.formState.isValid).toBe(true); + expect(forgotPasswordResult.formState.isValid).toBe(true); + expect(resetPasswordResult.formState.isValid).toBe(true); + }); + + it('should all initialize formState.isSubmitting as false', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.isSubmitting).toBe(false); + expect(signupResult.formState.isSubmitting).toBe(false); + expect(forgotPasswordResult.formState.isSubmitting).toBe(false); + expect(resetPasswordResult.formState.isSubmitting).toBe(false); + }); + + it('should all initialize formState.submitError as undefined', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.submitError).toBeUndefined(); + expect(signupResult.formState.submitError).toBeUndefined(); + expect(forgotPasswordResult.formState.submitError).toBeUndefined(); + expect(resetPasswordResult.formState.submitError).toBeUndefined(); + }); + + it('should all initialize formState.submitCount as 0', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.submitCount).toBe(0); + expect(signupResult.formState.submitCount).toBe(0); + expect(forgotPasswordResult.formState.submitCount).toBe(0); + expect(resetPasswordResult.formState.submitCount).toBe(0); + }); + + it('should all initialize form fields with touched false', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.fields.email.touched).toBe(false); + expect(loginResult.formState.fields.password.touched).toBe(false); + expect(loginResult.formState.fields.rememberMe.touched).toBe(false); + + expect(signupResult.formState.fields.firstName.touched).toBe(false); + expect(signupResult.formState.fields.lastName.touched).toBe(false); + expect(signupResult.formState.fields.email.touched).toBe(false); + expect(signupResult.formState.fields.password.touched).toBe(false); + expect(signupResult.formState.fields.confirmPassword.touched).toBe(false); + + expect(forgotPasswordResult.formState.fields.email.touched).toBe(false); + + expect(resetPasswordResult.formState.fields.newPassword.touched).toBe(false); + expect(resetPasswordResult.formState.fields.confirmPassword.touched).toBe(false); + }); + + it('should all initialize form fields with validating false', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.fields.email.validating).toBe(false); + expect(loginResult.formState.fields.password.validating).toBe(false); + expect(loginResult.formState.fields.rememberMe.validating).toBe(false); + + expect(signupResult.formState.fields.firstName.validating).toBe(false); + expect(signupResult.formState.fields.lastName.validating).toBe(false); + expect(signupResult.formState.fields.email.validating).toBe(false); + expect(signupResult.formState.fields.password.validating).toBe(false); + expect(signupResult.formState.fields.confirmPassword.validating).toBe(false); + + expect(forgotPasswordResult.formState.fields.email.validating).toBe(false); + + expect(resetPasswordResult.formState.fields.newPassword.validating).toBe(false); + expect(resetPasswordResult.formState.fields.confirmPassword.validating).toBe(false); + }); + + it('should all initialize form fields with error undefined', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.formState.fields.email.error).toBeUndefined(); + expect(loginResult.formState.fields.password.error).toBeUndefined(); + expect(loginResult.formState.fields.rememberMe.error).toBeUndefined(); + + expect(signupResult.formState.fields.firstName.error).toBeUndefined(); + expect(signupResult.formState.fields.lastName.error).toBeUndefined(); + expect(signupResult.formState.fields.email.error).toBeUndefined(); + expect(signupResult.formState.fields.password.error).toBeUndefined(); + expect(signupResult.formState.fields.confirmPassword.error).toBeUndefined(); + + expect(forgotPasswordResult.formState.fields.email.error).toBeUndefined(); + + expect(resetPasswordResult.formState.fields.newPassword.error).toBeUndefined(); + expect(resetPasswordResult.formState.fields.confirmPassword.error).toBeUndefined(); + }); + }); + + describe('common returnTo handling', () => { + it('should all handle returnTo with query parameters', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard?welcome=true', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard?welcome=true' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?welcome=true' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?welcome=true' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.returnTo).toBe('/dashboard?welcome=true'); + expect(signupResult.returnTo).toBe('/dashboard?welcome=true'); + expect(forgotPasswordResult.returnTo).toBe('/dashboard?welcome=true'); + expect(resetPasswordResult.returnTo).toBe('/dashboard?welcome=true'); + }); + + it('should all handle returnTo with hash fragments', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard#section', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard#section' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard#section' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard#section' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.returnTo).toBe('/dashboard#section'); + expect(signupResult.returnTo).toBe('/dashboard#section'); + expect(forgotPasswordResult.returnTo).toBe('/dashboard#section'); + expect(resetPasswordResult.returnTo).toBe('/dashboard#section'); + }); + + it('should all handle returnTo with encoded characters', () => { + const loginDTO: LoginPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin', hasInsufficientPermissions: false }; + const signupDTO: SignupPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' }; + const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' }; + const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?redirect=%2Fadmin' }; + + const loginResult = LoginViewDataBuilder.build(loginDTO); + const signupResult = SignupViewDataBuilder.build(signupDTO); + const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); + const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); + + expect(loginResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + expect(signupResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + expect(forgotPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + expect(resetPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/AvatarViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/AvatarViewDataBuilder.test.ts new file mode 100644 index 000000000..fa238ab3a --- /dev/null +++ b/apps/website/lib/builders/view-data/AvatarViewDataBuilder.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from 'vitest'; +import { AvatarViewDataBuilder } from './AvatarViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('AvatarViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to AvatarViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle JPEG images', () => { + const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle GIF images', () => { + const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/gif', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/gif'); + }); + + it('should handle SVG images', () => { + const buffer = new TextEncoder().encode(''); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/svg+xml', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/svg+xml'); + }); + + it('should handle WebP images', () => { + const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/webp', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/webp'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + AvatarViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle large buffer', () => { + const buffer = new Uint8Array(1024 * 1024); // 1MB + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with all zeros', () => { + const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with all ones', () => { + const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle different content types', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const contentTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/svg+xml', + 'image/bmp', + 'image/tiff', + ]; + + contentTypes.forEach((contentType) => { + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType, + }; + + const result = AvatarViewDataBuilder.build(mediaDto); + + expect(result.contentType).toBe(contentType); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.test.ts new file mode 100644 index 000000000..f59c6b7a6 --- /dev/null +++ b/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('CategoryIconViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle SVG icons', () => { + const buffer = new TextEncoder().encode(''); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/svg+xml', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/svg+xml'); + }); + + it('should handle small icon files', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + CategoryIconViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with special characters', () => { + const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = CategoryIconViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.test.ts new file mode 100644 index 000000000..7c5316977 --- /dev/null +++ b/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect } from 'vitest'; +import { CompleteOnboardingViewDataBuilder } from './CompleteOnboardingViewDataBuilder'; +import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; + +describe('CompleteOnboardingViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform successful onboarding completion DTO to ViewData correctly', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: 'driver-123', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: true, + driverId: 'driver-123', + errorMessage: undefined, + }); + }); + + it('should handle onboarding completion with error message', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: false, + driverId: undefined, + errorMessage: 'Failed to complete onboarding', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: false, + driverId: undefined, + errorMessage: 'Failed to complete onboarding', + }); + }); + + it('should handle onboarding completion with only success field', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: true, + driverId: undefined, + errorMessage: undefined, + }); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: 'driver-123', + errorMessage: undefined, + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.success).toBe(apiDto.success); + expect(result.driverId).toBe(apiDto.driverId); + expect(result.errorMessage).toBe(apiDto.errorMessage); + }); + + it('should not modify the input DTO', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: 'driver-123', + errorMessage: undefined, + }; + + const originalDto = { ...apiDto }; + CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle false success value', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: false, + driverId: undefined, + errorMessage: 'Error occurred', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.success).toBe(false); + expect(result.driverId).toBeUndefined(); + expect(result.errorMessage).toBe('Error occurred'); + }); + + it('should handle empty string error message', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: false, + driverId: undefined, + errorMessage: '', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.success).toBe(false); + expect(result.errorMessage).toBe(''); + }); + + it('should handle very long driverId', () => { + const longDriverId = 'driver-' + 'a'.repeat(1000); + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: longDriverId, + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.driverId).toBe(longDriverId); + }); + + it('should handle special characters in error message', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: false, + driverId: undefined, + errorMessage: 'Error: "Failed to create driver" (code: 500)', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.errorMessage).toBe('Error: "Failed to create driver" (code: 500)'); + }); + }); + + describe('derived fields calculation', () => { + it('should calculate isSuccessful derived field correctly', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: 'driver-123', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + // Note: The builder doesn't add derived fields, but we can verify the structure + expect(result.success).toBe(true); + expect(result.driverId).toBe('driver-123'); + }); + + it('should handle success with no driverId', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: true, + driverId: undefined, + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.success).toBe(true); + expect(result.driverId).toBeUndefined(); + }); + + it('should handle failure with driverId', () => { + const apiDto: CompleteOnboardingOutputDTO = { + success: false, + driverId: 'driver-123', + errorMessage: 'Partial failure', + }; + + const result = CompleteOnboardingViewDataBuilder.build(apiDto); + + expect(result.success).toBe(false); + expect(result.driverId).toBe('driver-123'); + expect(result.errorMessage).toBe('Partial failure'); + }); + }); +}); diff --git a/apps/website/tests/view-data/dashboard.test.ts b/apps/website/lib/builders/view-data/DashboardViewDataBuilder.test.ts similarity index 55% rename from apps/website/tests/view-data/dashboard.test.ts rename to apps/website/lib/builders/view-data/DashboardViewDataBuilder.test.ts index d37aeb27e..e425ca26f 100644 --- a/apps/website/tests/view-data/dashboard.test.ts +++ b/apps/website/lib/builders/view-data/DashboardViewDataBuilder.test.ts @@ -1,41 +1,6 @@ -/** - * View Data Layer Tests - Dashboard Functionality - * - * This test file covers the view data layer for dashboard functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Dashboard data transformation and aggregation - * - User statistics and metrics view models - * - Activity feed data formatting and sorting - * - Derived dashboard fields (trends, summaries, etc.) - * - Default values and fallbacks for dashboard views - * - Dashboard-specific formatting (dates, numbers, percentages, etc.) - * - Data grouping and categorization for dashboard components - * - Real-time data updates and state management - */ - -import { DashboardViewDataBuilder } from '@/lib/builders/view-data/DashboardViewDataBuilder'; -import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay'; -import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay'; -import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay'; -import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay'; -import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay'; -import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; +import { describe, it, expect } from 'vitest'; +import { DashboardViewDataBuilder } from './DashboardViewDataBuilder'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; -import type { DashboardDriverSummaryDTO } from '@/lib/types/generated/DashboardDriverSummaryDTO'; -import type { DashboardRaceSummaryDTO } from '@/lib/types/generated/DashboardRaceSummaryDTO'; -import type { DashboardFeedSummaryDTO } from '@/lib/types/generated/DashboardFeedSummaryDTO'; -import type { DashboardFriendSummaryDTO } from '@/lib/types/generated/DashboardFriendSummaryDTO'; -import type { DashboardLeagueStandingSummaryDTO } from '@/lib/types/generated/DashboardLeagueStandingSummaryDTO'; describe('DashboardViewDataBuilder', () => { describe('happy paths', () => { @@ -899,596 +864,3 @@ describe('DashboardViewDataBuilder', () => { }); }); }); - -describe('DashboardDateDisplay', () => { - describe('happy paths', () => { - it('should format future date correctly', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now - - const result = DashboardDateDisplay.format(futureDate); - - expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/); - expect(result.time).toMatch(/^\d{2}:\d{2}$/); - expect(result.relative).toBe('1d'); - }); - - it('should format date less than 24 hours correctly', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now - - const result = DashboardDateDisplay.format(futureDate); - - expect(result.relative).toBe('6h'); - }); - - it('should format date more than 24 hours correctly', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now - - const result = DashboardDateDisplay.format(futureDate); - - expect(result.relative).toBe('2d'); - }); - - it('should format past date correctly', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago - - const result = DashboardDateDisplay.format(pastDate); - - expect(result.relative).toBe('Past'); - }); - - it('should format current date correctly', () => { - const now = new Date(); - - const result = DashboardDateDisplay.format(now); - - expect(result.relative).toBe('Now'); - }); - - it('should format date with leading zeros in time', () => { - const date = new Date('2024-01-15T05:03:00'); - - const result = DashboardDateDisplay.format(date); - - expect(result.time).toBe('05:03'); - }); - }); - - describe('edge cases', () => { - it('should handle midnight correctly', () => { - const date = new Date('2024-01-15T00:00:00'); - - const result = DashboardDateDisplay.format(date); - - expect(result.time).toBe('00:00'); - }); - - it('should handle end of day correctly', () => { - const date = new Date('2024-01-15T23:59:59'); - - const result = DashboardDateDisplay.format(date); - - expect(result.time).toBe('23:59'); - }); - - it('should handle different days of week', () => { - const date = new Date('2024-01-15'); // Monday - - const result = DashboardDateDisplay.format(date); - - expect(result.date).toContain('Mon'); - }); - - it('should handle different months', () => { - const date = new Date('2024-01-15'); - - const result = DashboardDateDisplay.format(date); - - expect(result.date).toContain('Jan'); - }); - }); -}); - -describe('DashboardCountDisplay', () => { - describe('happy paths', () => { - it('should format positive numbers correctly', () => { - expect(DashboardCountDisplay.format(0)).toBe('0'); - expect(DashboardCountDisplay.format(1)).toBe('1'); - expect(DashboardCountDisplay.format(100)).toBe('100'); - expect(DashboardCountDisplay.format(1000)).toBe('1000'); - }); - - it('should handle null values', () => { - expect(DashboardCountDisplay.format(null)).toBe('0'); - }); - - it('should handle undefined values', () => { - expect(DashboardCountDisplay.format(undefined)).toBe('0'); - }); - }); - - describe('edge cases', () => { - it('should handle negative numbers', () => { - expect(DashboardCountDisplay.format(-1)).toBe('-1'); - expect(DashboardCountDisplay.format(-100)).toBe('-100'); - }); - - it('should handle large numbers', () => { - expect(DashboardCountDisplay.format(999999)).toBe('999999'); - expect(DashboardCountDisplay.format(1000000)).toBe('1000000'); - }); - - it('should handle decimal numbers', () => { - expect(DashboardCountDisplay.format(1.5)).toBe('1.5'); - expect(DashboardCountDisplay.format(100.99)).toBe('100.99'); - }); - }); -}); - -describe('DashboardRankDisplay', () => { - describe('happy paths', () => { - it('should format rank correctly', () => { - expect(DashboardRankDisplay.format(1)).toBe('1'); - expect(DashboardRankDisplay.format(42)).toBe('42'); - expect(DashboardRankDisplay.format(100)).toBe('100'); - }); - }); - - describe('edge cases', () => { - it('should handle rank 0', () => { - expect(DashboardRankDisplay.format(0)).toBe('0'); - }); - - it('should handle large ranks', () => { - expect(DashboardRankDisplay.format(999999)).toBe('999999'); - }); - }); -}); - -describe('DashboardConsistencyDisplay', () => { - describe('happy paths', () => { - it('should format consistency correctly', () => { - expect(DashboardConsistencyDisplay.format(0)).toBe('0%'); - expect(DashboardConsistencyDisplay.format(50)).toBe('50%'); - expect(DashboardConsistencyDisplay.format(100)).toBe('100%'); - }); - }); - - describe('edge cases', () => { - it('should handle decimal consistency', () => { - expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%'); - expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%'); - }); - - it('should handle negative consistency', () => { - expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%'); - }); - }); -}); - -describe('DashboardLeaguePositionDisplay', () => { - describe('happy paths', () => { - it('should format position correctly', () => { - expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1'); - expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5'); - expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100'); - }); - - it('should handle null values', () => { - expect(DashboardLeaguePositionDisplay.format(null)).toBe('-'); - }); - - it('should handle undefined values', () => { - expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-'); - }); - }); - - describe('edge cases', () => { - it('should handle position 0', () => { - expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0'); - }); - - it('should handle large positions', () => { - expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999'); - }); - }); -}); - -describe('RatingDisplay', () => { - describe('happy paths', () => { - it('should format rating correctly', () => { - expect(RatingDisplay.format(0)).toBe('0'); - expect(RatingDisplay.format(1234.56)).toBe('1,235'); - expect(RatingDisplay.format(9999.99)).toBe('10,000'); - }); - - it('should handle null values', () => { - expect(RatingDisplay.format(null)).toBe('—'); - }); - - it('should handle undefined values', () => { - expect(RatingDisplay.format(undefined)).toBe('—'); - }); - }); - - describe('edge cases', () => { - it('should round down correctly', () => { - expect(RatingDisplay.format(1234.4)).toBe('1,234'); - }); - - it('should round up correctly', () => { - expect(RatingDisplay.format(1234.6)).toBe('1,235'); - }); - - it('should handle decimal ratings', () => { - expect(RatingDisplay.format(1234.5)).toBe('1,235'); - }); - - it('should handle large ratings', () => { - expect(RatingDisplay.format(999999.99)).toBe('1,000,000'); - }); - }); -}); - -describe('Dashboard View Data - Cross-Component Consistency', () => { - describe('common patterns', () => { - it('should all use consistent formatting for numeric values', () => { - const dashboardDTO: DashboardOverviewDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - rating: 1234.56, - globalRank: 42, - totalRaces: 150, - wins: 25, - podiums: 60, - consistency: 85, - }, - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [], - activeLeaguesCount: 3, - nextRace: null, - recentResults: [], - leagueStandingsSummaries: [ - { - leagueId: 'league-1', - leagueName: 'Test League', - position: 5, - totalDrivers: 50, - points: 1250, - }, - ], - feedSummary: { - notificationCount: 0, - items: [], - }, - friends: [ - { id: 'friend-1', name: 'Alice', country: 'UK' }, - { id: 'friend-2', name: 'Bob', country: 'Germany' }, - ], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - // All numeric values should be formatted as strings - expect(typeof result.currentDriver.rating).toBe('string'); - expect(typeof result.currentDriver.rank).toBe('string'); - expect(typeof result.currentDriver.totalRaces).toBe('string'); - expect(typeof result.currentDriver.wins).toBe('string'); - expect(typeof result.currentDriver.podiums).toBe('string'); - expect(typeof result.currentDriver.consistency).toBe('string'); - expect(typeof result.activeLeaguesCount).toBe('string'); - expect(typeof result.friendCount).toBe('string'); - expect(typeof result.leagueStandings[0].position).toBe('string'); - expect(typeof result.leagueStandings[0].points).toBe('string'); - expect(typeof result.leagueStandings[0].totalDrivers).toBe('string'); - }); - - it('should all handle missing data gracefully', () => { - const dashboardDTO: DashboardOverviewDTO = { - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [], - activeLeaguesCount: 0, - nextRace: null, - recentResults: [], - leagueStandingsSummaries: [], - feedSummary: { - notificationCount: 0, - items: [], - }, - friends: [], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - // All fields should have safe defaults - expect(result.currentDriver.name).toBe(''); - expect(result.currentDriver.avatarUrl).toBe(''); - expect(result.currentDriver.country).toBe(''); - expect(result.currentDriver.rating).toBe('0.0'); - expect(result.currentDriver.rank).toBe('0'); - expect(result.currentDriver.totalRaces).toBe('0'); - expect(result.currentDriver.wins).toBe('0'); - expect(result.currentDriver.podiums).toBe('0'); - expect(result.currentDriver.consistency).toBe('0%'); - expect(result.nextRace).toBeNull(); - expect(result.upcomingRaces).toEqual([]); - expect(result.leagueStandings).toEqual([]); - expect(result.feedItems).toEqual([]); - expect(result.friends).toEqual([]); - expect(result.activeLeaguesCount).toBe('0'); - expect(result.friendCount).toBe('0'); - }); - - it('should all preserve ISO timestamps for serialization', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); - const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000); - - const dashboardDTO: DashboardOverviewDTO = { - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [], - activeLeaguesCount: 1, - nextRace: { - id: 'race-1', - track: 'Spa', - car: 'Porsche', - scheduledAt: futureDate.toISOString(), - status: 'scheduled', - isMyLeague: true, - }, - recentResults: [], - leagueStandingsSummaries: [], - feedSummary: { - notificationCount: 1, - items: [ - { - id: 'feed-1', - type: 'notification', - headline: 'Test', - timestamp: feedTimestamp.toISOString(), - }, - ], - }, - friends: [], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - // All timestamps should be preserved as ISO strings - expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString()); - expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString()); - }); - - it('should all handle boolean flags correctly', () => { - const dashboardDTO: DashboardOverviewDTO = { - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [ - { - id: 'race-1', - track: 'Spa', - car: 'Porsche', - scheduledAt: new Date().toISOString(), - status: 'scheduled', - isMyLeague: true, - }, - { - id: 'race-2', - track: 'Monza', - car: 'Ferrari', - scheduledAt: new Date().toISOString(), - status: 'scheduled', - isMyLeague: false, - }, - ], - activeLeaguesCount: 1, - nextRace: null, - recentResults: [], - leagueStandingsSummaries: [], - feedSummary: { - notificationCount: 0, - items: [], - }, - friends: [], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - expect(result.upcomingRaces[0].isMyLeague).toBe(true); - expect(result.upcomingRaces[1].isMyLeague).toBe(false); - }); - }); - - describe('data integrity', () => { - it('should maintain data consistency across transformations', () => { - const dashboardDTO: DashboardOverviewDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - rating: 1234.56, - globalRank: 42, - totalRaces: 150, - wins: 25, - podiums: 60, - consistency: 85, - }, - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [], - activeLeaguesCount: 3, - nextRace: null, - recentResults: [], - leagueStandingsSummaries: [], - feedSummary: { - notificationCount: 5, - items: [], - }, - friends: [ - { id: 'friend-1', name: 'Alice', country: 'UK' }, - { id: 'friend-2', name: 'Bob', country: 'Germany' }, - ], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - // Verify derived fields match their source data - expect(result.friendCount).toBe(dashboardDTO.friends.length.toString()); - expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString()); - expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0); - expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0); - expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0); - expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0); - }); - - it('should handle complex real-world scenarios', () => { - const now = new Date(); - const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); - const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); - const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000); - - const dashboardDTO: DashboardOverviewDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - avatarUrl: 'https://example.com/avatar.jpg', - rating: 2456.78, - globalRank: 15, - totalRaces: 250, - wins: 45, - podiums: 120, - consistency: 92.5, - }, - myUpcomingRaces: [], - otherUpcomingRaces: [], - upcomingRaces: [ - { - id: 'race-1', - leagueId: 'league-1', - leagueName: 'Pro League', - track: 'Spa', - car: 'Porsche 911 GT3', - scheduledAt: race1Date.toISOString(), - status: 'scheduled', - isMyLeague: true, - }, - { - id: 'race-2', - track: 'Monza', - car: 'Ferrari 488 GT3', - scheduledAt: race2Date.toISOString(), - status: 'scheduled', - isMyLeague: false, - }, - ], - activeLeaguesCount: 2, - nextRace: { - id: 'race-1', - leagueId: 'league-1', - leagueName: 'Pro League', - track: 'Spa', - car: 'Porsche 911 GT3', - scheduledAt: race1Date.toISOString(), - status: 'scheduled', - isMyLeague: true, - }, - recentResults: [], - leagueStandingsSummaries: [ - { - leagueId: 'league-1', - leagueName: 'Pro League', - position: 3, - totalDrivers: 100, - points: 2450, - }, - { - leagueId: 'league-2', - leagueName: 'Rookie League', - position: 1, - totalDrivers: 50, - points: 1800, - }, - ], - feedSummary: { - notificationCount: 3, - items: [ - { - id: 'feed-1', - type: 'race_result', - headline: 'Race completed', - body: 'You finished 3rd in the Pro League race', - timestamp: feedTimestamp.toISOString(), - ctaLabel: 'View Results', - ctaHref: '/races/123', - }, - { - id: 'feed-2', - type: 'league_update', - headline: 'League standings updated', - body: 'You moved up 2 positions', - timestamp: feedTimestamp.toISOString(), - }, - ], - }, - friends: [ - { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' }, - { id: 'friend-2', name: 'Bob', country: 'Germany' }, - { id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' }, - ], - }; - - const result = DashboardViewDataBuilder.build(dashboardDTO); - - // Verify all transformations - expect(result.currentDriver.name).toBe('John Doe'); - expect(result.currentDriver.rating).toBe('2,457'); - expect(result.currentDriver.rank).toBe('15'); - expect(result.currentDriver.totalRaces).toBe('250'); - expect(result.currentDriver.wins).toBe('45'); - expect(result.currentDriver.podiums).toBe('120'); - expect(result.currentDriver.consistency).toBe('92.5%'); - - expect(result.nextRace).not.toBeNull(); - expect(result.nextRace?.id).toBe('race-1'); - expect(result.nextRace?.track).toBe('Spa'); - expect(result.nextRace?.isMyLeague).toBe(true); - - expect(result.upcomingRaces).toHaveLength(2); - expect(result.upcomingRaces[0].isMyLeague).toBe(true); - expect(result.upcomingRaces[1].isMyLeague).toBe(false); - - expect(result.leagueStandings).toHaveLength(2); - expect(result.leagueStandings[0].position).toBe('#3'); - expect(result.leagueStandings[0].points).toBe('2450'); - expect(result.leagueStandings[1].position).toBe('#1'); - expect(result.leagueStandings[1].points).toBe('1800'); - - expect(result.feedItems).toHaveLength(2); - expect(result.feedItems[0].type).toBe('race_result'); - expect(result.feedItems[0].ctaLabel).toBe('View Results'); - expect(result.feedItems[1].type).toBe('league_update'); - expect(result.feedItems[1].ctaLabel).toBeUndefined(); - - expect(result.friends).toHaveLength(3); - expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg'); - expect(result.friends[1].avatarUrl).toBe(''); - expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg'); - - expect(result.activeLeaguesCount).toBe('2'); - expect(result.friendCount).toBe('3'); - expect(result.hasUpcomingRaces).toBe(true); - expect(result.hasLeagueStandings).toBe(true); - expect(result.hasFeedItems).toBe(true); - expect(result.hasFriends).toBe(true); - }); - }); -}); diff --git a/apps/website/tests/view-data/drivers.test.ts b/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.test.ts similarity index 50% rename from apps/website/tests/view-data/drivers.test.ts rename to apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.test.ts index 90b946a52..688c943be 100644 --- a/apps/website/tests/view-data/drivers.test.ts +++ b/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.test.ts @@ -1,456 +1,6 @@ -/** - * View Data Layer Tests - Drivers Functionality - * - * This test file covers the view data layer for drivers functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Driver list data transformation and sorting - * - Individual driver profile view models - * - Driver statistics and metrics formatting - * - Derived driver fields (performance ratings, rankings, etc.) - * - Default values and fallbacks for driver views - * - Driver-specific formatting (lap times, points, positions, etc.) - * - Data grouping and categorization for driver components - * - Driver search and filtering view models - * - Driver comparison data transformation - */ - -import { DriversViewDataBuilder } from '@/lib/builders/view-data/DriversViewDataBuilder'; -import { DriverProfileViewDataBuilder } from '@/lib/builders/view-data/DriverProfileViewDataBuilder'; -import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; -import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; -import { DateDisplay } from '@/lib/display-objects/DateDisplay'; -import { FinishDisplay } from '@/lib/display-objects/FinishDisplay'; -import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; -import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; -import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; +import { describe, it, expect } from 'vitest'; +import { DriverProfileViewDataBuilder } from './DriverProfileViewDataBuilder'; import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; -import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO'; -import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO'; -import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO'; -import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO'; -import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO'; -import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO'; - -describe('DriversViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - category: 'Elite', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/john.jpg', - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.75, - skillLevel: 'Advanced', - category: 'Pro', - nationality: 'Canada', - racesCompleted: 120, - wins: 15, - podiums: 45, - isActive: true, - rank: 2, - avatarUrl: 'https://example.com/jane.jpg', - }, - ], - totalRaces: 270, - totalWins: 40, - activeCount: 2, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers).toHaveLength(2); - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[0].name).toBe('John Doe'); - expect(result.drivers[0].rating).toBe(1234.56); - expect(result.drivers[0].ratingLabel).toBe('1,235'); - expect(result.drivers[0].skillLevel).toBe('Pro'); - expect(result.drivers[0].category).toBe('Elite'); - expect(result.drivers[0].nationality).toBe('USA'); - expect(result.drivers[0].racesCompleted).toBe(150); - expect(result.drivers[0].wins).toBe(25); - expect(result.drivers[0].podiums).toBe(60); - expect(result.drivers[0].isActive).toBe(true); - expect(result.drivers[0].rank).toBe(1); - expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg'); - - expect(result.drivers[1].id).toBe('driver-2'); - expect(result.drivers[1].name).toBe('Jane Smith'); - expect(result.drivers[1].rating).toBe(1100.75); - expect(result.drivers[1].ratingLabel).toBe('1,101'); - expect(result.drivers[1].skillLevel).toBe('Advanced'); - expect(result.drivers[1].category).toBe('Pro'); - expect(result.drivers[1].nationality).toBe('Canada'); - expect(result.drivers[1].racesCompleted).toBe(120); - expect(result.drivers[1].wins).toBe(15); - expect(result.drivers[1].podiums).toBe(45); - expect(result.drivers[1].isActive).toBe(true); - expect(result.drivers[1].rank).toBe(2); - expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg'); - - expect(result.totalRaces).toBe(270); - expect(result.totalRacesLabel).toBe('270'); - expect(result.totalWins).toBe(40); - expect(result.totalWinsLabel).toBe('40'); - expect(result.activeCount).toBe(2); - expect(result.activeCountLabel).toBe('2'); - expect(result.totalDriversLabel).toBe('2'); - }); - - it('should handle drivers with missing optional fields', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].category).toBeUndefined(); - expect(result.drivers[0].avatarUrl).toBeUndefined(); - }); - - it('should handle empty drivers array', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers).toEqual([]); - expect(result.totalRaces).toBe(0); - expect(result.totalRacesLabel).toBe('0'); - expect(result.totalWins).toBe(0); - expect(result.totalWinsLabel).toBe('0'); - expect(result.activeCount).toBe(0); - expect(result.activeCountLabel).toBe('0'); - expect(result.totalDriversLabel).toBe('0'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - category: 'Elite', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/john.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name); - expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality); - expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel); - expect(result.totalRaces).toBe(driversDTO.totalRaces); - expect(result.totalWins).toBe(driversDTO.totalWins); - expect(result.activeCount).toBe(driversDTO.activeCount); - }); - - it('should not modify the input DTO', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - category: 'Elite', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/john.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const originalDTO = JSON.parse(JSON.stringify(driversDTO)); - DriversViewDataBuilder.build(driversDTO); - - expect(driversDTO).toEqual(originalDTO); - }); - - it('should transform all numeric fields to formatted strings where appropriate', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - // Rating label should be a formatted string - expect(typeof result.drivers[0].ratingLabel).toBe('string'); - expect(result.drivers[0].ratingLabel).toBe('1,235'); - - // Total counts should be formatted strings - expect(typeof result.totalRacesLabel).toBe('string'); - expect(result.totalRacesLabel).toBe('150'); - expect(typeof result.totalWinsLabel).toBe('string'); - expect(result.totalWinsLabel).toBe('25'); - expect(typeof result.activeCountLabel).toBe('string'); - expect(result.activeCountLabel).toBe('1'); - expect(typeof result.totalDriversLabel).toBe('string'); - expect(result.totalDriversLabel).toBe('1'); - }); - - it('should handle large numbers correctly', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 999999.99, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 10000, - wins: 2500, - podiums: 5000, - isActive: true, - rank: 1, - }, - ], - totalRaces: 10000, - totalWins: 2500, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].ratingLabel).toBe('1,000,000'); - expect(result.totalRacesLabel).toBe('10,000'); - expect(result.totalWinsLabel).toBe('2,500'); - expect(result.activeCountLabel).toBe('1'); - expect(result.totalDriversLabel).toBe('1'); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined rating', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 0, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].ratingLabel).toBe('0'); - }); - - it('should handle drivers with no category', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].category).toBeUndefined(); - }); - - it('should handle inactive drivers', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'Pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: false, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 0, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.drivers[0].isActive).toBe(false); - expect(result.activeCount).toBe(0); - expect(result.activeCountLabel).toBe('0'); - }); - }); - - describe('derived fields', () => { - it('should correctly calculate total drivers label', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 }, - { id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 }, - { id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 }, - ], - totalRaces: 350, - totalWins: 45, - activeCount: 2, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.totalDriversLabel).toBe('3'); - }); - - it('should correctly calculate active count', () => { - const driversDTO: DriversLeaderboardDTO = { - drivers: [ - { id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 }, - { id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 }, - { id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 }, - ], - totalRaces: 350, - totalWins: 45, - activeCount: 2, - }; - - const result = DriversViewDataBuilder.build(driversDTO); - - expect(result.activeCount).toBe(2); - expect(result.activeCountLabel).toBe('2'); - }); - }); - - describe('rating formatting', () => { - it('should format ratings with thousands separators', () => { - expect(RatingDisplay.format(1234.56)).toBe('1,235'); - expect(RatingDisplay.format(9999.99)).toBe('10,000'); - expect(RatingDisplay.format(100000.5)).toBe('100,001'); - }); - - it('should handle null/undefined ratings', () => { - expect(RatingDisplay.format(null)).toBe('—'); - expect(RatingDisplay.format(undefined)).toBe('—'); - }); - - it('should round ratings correctly', () => { - expect(RatingDisplay.format(1234.4)).toBe('1,234'); - expect(RatingDisplay.format(1234.6)).toBe('1,235'); - expect(RatingDisplay.format(1234.5)).toBe('1,235'); - }); - }); - - describe('number formatting', () => { - it('should format numbers with thousands separators', () => { - expect(NumberDisplay.format(1234567)).toBe('1,234,567'); - expect(NumberDisplay.format(1000)).toBe('1,000'); - expect(NumberDisplay.format(999)).toBe('999'); - }); - - it('should handle decimal numbers', () => { - expect(NumberDisplay.format(1234.567)).toBe('1,234.567'); - expect(NumberDisplay.format(1000.5)).toBe('1,000.5'); - }); - }); -}); describe('DriverProfileViewDataBuilder', () => { describe('happy paths', () => { @@ -1643,531 +1193,4 @@ describe('DriverProfileViewDataBuilder', () => { expect(result.socialSummary.friends).toHaveLength(5); }); }); - - describe('date formatting', () => { - it('should format dates correctly', () => { - expect(DateDisplay.formatShort('2024-01-15T00:00:00Z')).toBe('Jan 15, 2024'); - expect(DateDisplay.formatMonthYear('2024-01-15T00:00:00Z')).toBe('Jan 2024'); - expect(DateDisplay.formatShort('2024-12-25T00:00:00Z')).toBe('Dec 25, 2024'); - expect(DateDisplay.formatMonthYear('2024-12-25T00:00:00Z')).toBe('Dec 2024'); - }); - }); - - describe('finish position formatting', () => { - it('should format finish positions correctly', () => { - expect(FinishDisplay.format(1)).toBe('P1'); - expect(FinishDisplay.format(5)).toBe('P5'); - expect(FinishDisplay.format(10)).toBe('P10'); - expect(FinishDisplay.format(100)).toBe('P100'); - }); - - it('should handle null/undefined finish positions', () => { - expect(FinishDisplay.format(null)).toBe('—'); - expect(FinishDisplay.format(undefined)).toBe('—'); - }); - - it('should format average finish positions correctly', () => { - expect(FinishDisplay.formatAverage(5.4)).toBe('P5.4'); - expect(FinishDisplay.formatAverage(1.5)).toBe('P1.5'); - expect(FinishDisplay.formatAverage(10.0)).toBe('P10.0'); - }); - - it('should handle null/undefined average finish positions', () => { - expect(FinishDisplay.formatAverage(null)).toBe('—'); - expect(FinishDisplay.formatAverage(undefined)).toBe('—'); - }); - }); - - describe('percentage formatting', () => { - it('should format percentages correctly', () => { - expect(PercentDisplay.format(0.1234)).toBe('12.3%'); - expect(PercentDisplay.format(0.5)).toBe('50.0%'); - expect(PercentDisplay.format(1.0)).toBe('100.0%'); - }); - - it('should handle null/undefined percentages', () => { - expect(PercentDisplay.format(null)).toBe('0.0%'); - expect(PercentDisplay.format(undefined)).toBe('0.0%'); - }); - - it('should format whole percentages correctly', () => { - expect(PercentDisplay.formatWhole(85)).toBe('85%'); - expect(PercentDisplay.formatWhole(50)).toBe('50%'); - expect(PercentDisplay.formatWhole(100)).toBe('100%'); - }); - - it('should handle null/undefined whole percentages', () => { - expect(PercentDisplay.formatWhole(null)).toBe('0%'); - expect(PercentDisplay.formatWhole(undefined)).toBe('0%'); - }); - }); - - describe('cross-component consistency', () => { - it('should all use consistent formatting for numeric values', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - joinedAt: '2024-01-15T00:00:00Z', - rating: 1234.56, - globalRank: 42, - consistency: 85, - }, - stats: { - totalRaces: 150, - wins: 25, - podiums: 60, - dnfs: 10, - avgFinish: 5.4, - bestFinish: 1, - worstFinish: 25, - finishRate: 0.933, - winRate: 0.167, - podiumRate: 0.4, - percentile: 95, - rating: 1234.56, - consistency: 85, - overallRank: 42, - }, - finishDistribution: { - totalRaces: 150, - wins: 25, - podiums: 60, - topTen: 100, - dnfs: 10, - other: 55, - }, - teamMemberships: [], - socialSummary: { - friendsCount: 0, - friends: [], - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - // All numeric values should be formatted as strings - expect(typeof result.currentDriver?.ratingLabel).toBe('string'); - expect(typeof result.currentDriver?.globalRankLabel).toBe('string'); - expect(typeof result.stats?.totalRacesLabel).toBe('string'); - expect(typeof result.stats?.winsLabel).toBe('string'); - expect(typeof result.stats?.podiumsLabel).toBe('string'); - expect(typeof result.stats?.dnfsLabel).toBe('string'); - expect(typeof result.stats?.avgFinishLabel).toBe('string'); - expect(typeof result.stats?.bestFinishLabel).toBe('string'); - expect(typeof result.stats?.worstFinishLabel).toBe('string'); - expect(typeof result.stats?.ratingLabel).toBe('string'); - expect(typeof result.stats?.consistencyLabel).toBe('string'); - }); - - it('should all handle missing data gracefully', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - joinedAt: '2024-01-15T00:00:00Z', - }, - stats: { - totalRaces: 0, - wins: 0, - podiums: 0, - dnfs: 0, - }, - finishDistribution: { - totalRaces: 0, - wins: 0, - podiums: 0, - topTen: 0, - dnfs: 0, - other: 0, - }, - teamMemberships: [], - socialSummary: { - friendsCount: 0, - friends: [], - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - // All fields should have safe defaults - expect(result.currentDriver?.avatarUrl).toBe(''); - expect(result.currentDriver?.iracingId).toBeNull(); - expect(result.currentDriver?.rating).toBeNull(); - expect(result.currentDriver?.ratingLabel).toBe('—'); - expect(result.currentDriver?.globalRank).toBeNull(); - expect(result.currentDriver?.globalRankLabel).toBe('—'); - expect(result.currentDriver?.consistency).toBeNull(); - expect(result.currentDriver?.bio).toBeNull(); - expect(result.currentDriver?.totalDrivers).toBeNull(); - expect(result.stats?.avgFinish).toBeNull(); - expect(result.stats?.avgFinishLabel).toBe('—'); - expect(result.stats?.bestFinish).toBeNull(); - expect(result.stats?.bestFinishLabel).toBe('—'); - expect(result.stats?.worstFinish).toBeNull(); - expect(result.stats?.worstFinishLabel).toBe('—'); - expect(result.stats?.finishRate).toBeNull(); - expect(result.stats?.winRate).toBeNull(); - expect(result.stats?.podiumRate).toBeNull(); - expect(result.stats?.percentile).toBeNull(); - expect(result.stats?.rating).toBeNull(); - expect(result.stats?.ratingLabel).toBe('—'); - expect(result.stats?.consistency).toBeNull(); - expect(result.stats?.consistencyLabel).toBe('0%'); - expect(result.stats?.overallRank).toBeNull(); - expect(result.finishDistribution).not.toBeNull(); - expect(result.teamMemberships).toEqual([]); - expect(result.socialSummary.friends).toEqual([]); - expect(result.extendedProfile).toBeNull(); - }); - - it('should all preserve ISO timestamps for serialization', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - joinedAt: '2024-01-15T00:00:00Z', - }, - stats: { - totalRaces: 150, - wins: 25, - podiums: 60, - dnfs: 10, - }, - finishDistribution: { - totalRaces: 150, - wins: 25, - podiums: 60, - topTen: 100, - dnfs: 10, - other: 55, - }, - teamMemberships: [ - { - teamId: 'team-1', - teamName: 'Elite Racing', - teamTag: 'ER', - role: 'Driver', - joinedAt: '2024-01-15T00:00:00Z', - isCurrent: true, - }, - ], - socialSummary: { - friendsCount: 0, - friends: [], - }, - extendedProfile: { - socialHandles: [], - achievements: [ - { - id: 'ach-1', - title: 'Champion', - description: 'Won the championship', - icon: 'trophy', - rarity: 'Legendary', - earnedAt: '2024-01-15T00:00:00Z', - }, - ], - racingStyle: 'Aggressive', - favoriteTrack: 'Spa', - favoriteCar: 'Porsche 911 GT3', - timezone: 'America/New_York', - availableHours: 'Evenings', - lookingForTeam: false, - openToRequests: true, - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - // All timestamps should be preserved as ISO strings - expect(result.currentDriver?.joinedAt).toBe('2024-01-15T00:00:00Z'); - expect(result.teamMemberships[0].joinedAt).toBe('2024-01-15T00:00:00Z'); - expect(result.extendedProfile?.achievements[0].earnedAt).toBe('2024-01-15T00:00:00Z'); - }); - - it('should all handle boolean flags correctly', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - joinedAt: '2024-01-15T00:00:00Z', - }, - stats: { - totalRaces: 150, - wins: 25, - podiums: 60, - dnfs: 10, - }, - finishDistribution: { - totalRaces: 150, - wins: 25, - podiums: 60, - topTen: 100, - dnfs: 10, - other: 55, - }, - teamMemberships: [ - { - teamId: 'team-1', - teamName: 'Elite Racing', - teamTag: 'ER', - role: 'Driver', - joinedAt: '2024-01-15T00:00:00Z', - isCurrent: true, - }, - { - teamId: 'team-2', - teamName: 'Old Team', - teamTag: 'OT', - role: 'Driver', - joinedAt: '2023-01-15T00:00:00Z', - isCurrent: false, - }, - ], - socialSummary: { - friendsCount: 0, - friends: [], - }, - extendedProfile: { - socialHandles: [], - achievements: [], - racingStyle: 'Aggressive', - favoriteTrack: 'Spa', - favoriteCar: 'Porsche 911 GT3', - timezone: 'America/New_York', - availableHours: 'Evenings', - lookingForTeam: true, - openToRequests: false, - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - expect(result.teamMemberships[0].isCurrent).toBe(true); - expect(result.teamMemberships[1].isCurrent).toBe(false); - expect(result.extendedProfile?.lookingForTeam).toBe(true); - expect(result.extendedProfile?.openToRequests).toBe(false); - }); - }); - - describe('data integrity', () => { - it('should maintain data consistency across transformations', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - avatarUrl: 'https://example.com/avatar.jpg', - iracingId: '12345', - joinedAt: '2024-01-15T00:00:00Z', - rating: 1234.56, - globalRank: 42, - consistency: 85, - bio: 'Professional sim racer.', - totalDrivers: 1000, - }, - stats: { - totalRaces: 150, - wins: 25, - podiums: 60, - dnfs: 10, - avgFinish: 5.4, - bestFinish: 1, - worstFinish: 25, - finishRate: 0.933, - winRate: 0.167, - podiumRate: 0.4, - percentile: 95, - rating: 1234.56, - consistency: 85, - overallRank: 42, - }, - finishDistribution: { - totalRaces: 150, - wins: 25, - podiums: 60, - topTen: 100, - dnfs: 10, - other: 55, - }, - teamMemberships: [ - { - teamId: 'team-1', - teamName: 'Elite Racing', - teamTag: 'ER', - role: 'Driver', - joinedAt: '2024-01-15T00:00:00Z', - isCurrent: true, - }, - ], - socialSummary: { - friendsCount: 2, - friends: [ - { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' }, - { id: 'friend-2', name: 'Bob', country: 'Germany' }, - ], - }, - extendedProfile: { - socialHandles: [ - { platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' }, - ], - achievements: [ - { id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' }, - ], - racingStyle: 'Aggressive', - favoriteTrack: 'Spa', - favoriteCar: 'Porsche 911 GT3', - timezone: 'America/New_York', - availableHours: 'Evenings', - lookingForTeam: false, - openToRequests: true, - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - // Verify derived fields match their source data - expect(result.socialSummary.friendsCount).toBe(profileDTO.socialSummary.friends.length); - expect(result.teamMemberships.length).toBe(profileDTO.teamMemberships.length); - expect(result.extendedProfile?.achievements.length).toBe(profileDTO.extendedProfile?.achievements.length); - }); - - it('should handle complex real-world scenarios', () => { - const profileDTO: GetDriverProfileOutputDTO = { - currentDriver: { - id: 'driver-123', - name: 'John Doe', - country: 'USA', - avatarUrl: 'https://example.com/avatar.jpg', - iracingId: '12345', - joinedAt: '2024-01-15T00:00:00Z', - rating: 2456.78, - globalRank: 15, - consistency: 92.5, - bio: 'Professional sim racer with 5 years of experience. Specializes in GT3 racing.', - totalDrivers: 1000, - }, - stats: { - totalRaces: 250, - wins: 45, - podiums: 120, - dnfs: 15, - avgFinish: 4.2, - bestFinish: 1, - worstFinish: 30, - finishRate: 0.94, - winRate: 0.18, - podiumRate: 0.48, - percentile: 98, - rating: 2456.78, - consistency: 92.5, - overallRank: 15, - }, - finishDistribution: { - totalRaces: 250, - wins: 45, - podiums: 120, - topTen: 180, - dnfs: 15, - other: 55, - }, - teamMemberships: [ - { - teamId: 'team-1', - teamName: 'Elite Racing', - teamTag: 'ER', - role: 'Driver', - joinedAt: '2024-01-15T00:00:00Z', - isCurrent: true, - }, - { - teamId: 'team-2', - teamName: 'Pro Team', - teamTag: 'PT', - role: 'Reserve Driver', - joinedAt: '2023-06-15T00:00:00Z', - isCurrent: false, - }, - ], - socialSummary: { - friendsCount: 50, - friends: [ - { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' }, - { id: 'friend-2', name: 'Bob', country: 'Germany' }, - { id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' }, - ], - }, - extendedProfile: { - socialHandles: [ - { platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' }, - { platform: 'Discord', handle: 'johndoe#1234', url: '' }, - ], - achievements: [ - { id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' }, - { id: 'ach-2', title: 'Podium Finisher', description: 'Finished on podium 100 times', icon: 'medal', rarity: 'Rare', earnedAt: '2023-12-01T00:00:00Z' }, - ], - racingStyle: 'Aggressive', - favoriteTrack: 'Spa', - favoriteCar: 'Porsche 911 GT3', - timezone: 'America/New_York', - availableHours: 'Evenings and Weekends', - lookingForTeam: false, - openToRequests: true, - }, - }; - - const result = DriverProfileViewDataBuilder.build(profileDTO); - - // Verify all transformations - expect(result.currentDriver?.name).toBe('John Doe'); - expect(result.currentDriver?.ratingLabel).toBe('2,457'); - expect(result.currentDriver?.globalRankLabel).toBe('#15'); - expect(result.currentDriver?.consistency).toBe(92.5); - expect(result.currentDriver?.bio).toBe('Professional sim racer with 5 years of experience. Specializes in GT3 racing.'); - - expect(result.stats?.totalRacesLabel).toBe('250'); - expect(result.stats?.winsLabel).toBe('45'); - expect(result.stats?.podiumsLabel).toBe('120'); - expect(result.stats?.dnfsLabel).toBe('15'); - expect(result.stats?.avgFinishLabel).toBe('P4.2'); - expect(result.stats?.bestFinishLabel).toBe('P1'); - expect(result.stats?.worstFinishLabel).toBe('P30'); - expect(result.stats?.finishRate).toBe(0.94); - expect(result.stats?.winRate).toBe(0.18); - expect(result.stats?.podiumRate).toBe(0.48); - expect(result.stats?.percentile).toBe(98); - expect(result.stats?.ratingLabel).toBe('2,457'); - expect(result.stats?.consistencyLabel).toBe('93%'); - expect(result.stats?.overallRank).toBe(15); - - expect(result.finishDistribution?.totalRaces).toBe(250); - expect(result.finishDistribution?.wins).toBe(45); - expect(result.finishDistribution?.podiums).toBe(120); - expect(result.finishDistribution?.topTen).toBe(180); - expect(result.finishDistribution?.dnfs).toBe(15); - expect(result.finishDistribution?.other).toBe(55); - - expect(result.teamMemberships).toHaveLength(2); - expect(result.teamMemberships[0].isCurrent).toBe(true); - expect(result.teamMemberships[1].isCurrent).toBe(false); - - expect(result.socialSummary.friendsCount).toBe(50); - expect(result.socialSummary.friends).toHaveLength(3); - expect(result.socialSummary.friends[0].avatarUrl).toBe('https://example.com/alice.jpg'); - expect(result.socialSummary.friends[1].avatarUrl).toBe(''); - expect(result.socialSummary.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg'); - - expect(result.extendedProfile?.socialHandles).toHaveLength(2); - expect(result.extendedProfile?.achievements).toHaveLength(2); - expect(result.extendedProfile?.achievements[0].rarityLabel).toBe('Legendary'); - expect(result.extendedProfile?.achievements[1].rarityLabel).toBe('Rare'); - expect(result.extendedProfile?.lookingForTeam).toBe(false); - expect(result.extendedProfile?.openToRequests).toBe(true); - }); - }); }); diff --git a/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.test.ts new file mode 100644 index 000000000..27df76650 --- /dev/null +++ b/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.test.ts @@ -0,0 +1,441 @@ +import { describe, it, expect } from 'vitest'; +import { DriverRankingsViewDataBuilder } from './DriverRankingsViewDataBuilder'; +import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; + +describe('DriverRankingsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform DriverLeaderboardItemDTO array to DriverRankingsViewData correctly', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar1.jpg', + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.0, + skillLevel: 'advanced', + nationality: 'Canada', + racesCompleted: 100, + wins: 15, + podiums: 40, + isActive: true, + rank: 2, + avatarUrl: 'https://example.com/avatar2.jpg', + }, + { + id: 'driver-3', + name: 'Bob Johnson', + rating: 950.0, + skillLevel: 'intermediate', + nationality: 'UK', + racesCompleted: 80, + wins: 10, + podiums: 30, + isActive: true, + rank: 3, + avatarUrl: 'https://example.com/avatar3.jpg', + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + // Verify drivers + expect(result.drivers).toHaveLength(3); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[0].name).toBe('John Doe'); + expect(result.drivers[0].rating).toBe(1234.56); + expect(result.drivers[0].skillLevel).toBe('pro'); + expect(result.drivers[0].nationality).toBe('USA'); + expect(result.drivers[0].racesCompleted).toBe(150); + expect(result.drivers[0].wins).toBe(25); + expect(result.drivers[0].podiums).toBe(60); + expect(result.drivers[0].rank).toBe(1); + expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); + expect(result.drivers[0].winRate).toBe('16.7'); + expect(result.drivers[0].medalBg).toBe('bg-warning-amber'); + expect(result.drivers[0].medalColor).toBe('text-warning-amber'); + + // Verify podium (top 3 with special ordering: 2nd, 1st, 3rd) + expect(result.podium).toHaveLength(3); + expect(result.podium[0].id).toBe('driver-1'); + expect(result.podium[0].name).toBe('John Doe'); + expect(result.podium[0].rating).toBe(1234.56); + expect(result.podium[0].wins).toBe(25); + expect(result.podium[0].podiums).toBe(60); + expect(result.podium[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); + expect(result.podium[0].position).toBe(2); // 2nd place + + expect(result.podium[1].id).toBe('driver-2'); + expect(result.podium[1].position).toBe(1); // 1st place + + expect(result.podium[2].id).toBe('driver-3'); + expect(result.podium[2].position).toBe(3); // 3rd place + + // Verify default values + expect(result.searchQuery).toBe(''); + expect(result.selectedSkill).toBe('all'); + expect(result.sortBy).toBe('rank'); + expect(result.showFilters).toBe(false); + }); + + it('should handle empty driver array', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = []; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers).toEqual([]); + expect(result.podium).toEqual([]); + expect(result.searchQuery).toBe(''); + expect(result.selectedSkill).toBe('all'); + expect(result.sortBy).toBe('rank'); + expect(result.showFilters).toBe(false); + }); + + it('should handle less than 3 drivers for podium', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar1.jpg', + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.0, + skillLevel: 'advanced', + nationality: 'Canada', + racesCompleted: 100, + wins: 15, + podiums: 40, + isActive: true, + rank: 2, + avatarUrl: 'https://example.com/avatar2.jpg', + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers).toHaveLength(2); + expect(result.podium).toHaveLength(2); + expect(result.podium[0].position).toBe(2); // 2nd place + expect(result.podium[1].position).toBe(1); // 1st place + }); + + it('should handle missing avatar URLs with empty string fallback', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].avatarUrl).toBe(''); + expect(result.podium[0].avatarUrl).toBe(''); + }); + + it('should calculate win rate correctly', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 100, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.0, + skillLevel: 'advanced', + nationality: 'Canada', + racesCompleted: 50, + wins: 10, + podiums: 25, + isActive: true, + rank: 2, + }, + { + id: 'driver-3', + name: 'Bob Johnson', + rating: 950.0, + skillLevel: 'intermediate', + nationality: 'UK', + racesCompleted: 0, + wins: 0, + podiums: 0, + isActive: true, + rank: 3, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].winRate).toBe('25.0'); + expect(result.drivers[1].winRate).toBe('20.0'); + expect(result.drivers[2].winRate).toBe('0.0'); + }); + + it('should assign correct medal colors based on position', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.0, + skillLevel: 'advanced', + nationality: 'Canada', + racesCompleted: 100, + wins: 15, + podiums: 40, + isActive: true, + rank: 2, + }, + { + id: 'driver-3', + name: 'Bob Johnson', + rating: 950.0, + skillLevel: 'intermediate', + nationality: 'UK', + racesCompleted: 80, + wins: 10, + podiums: 30, + isActive: true, + rank: 3, + }, + { + id: 'driver-4', + name: 'Alice Brown', + rating: 800.0, + skillLevel: 'beginner', + nationality: 'Germany', + racesCompleted: 60, + wins: 5, + podiums: 15, + isActive: true, + rank: 4, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].medalBg).toBe('bg-warning-amber'); + expect(result.drivers[0].medalColor).toBe('text-warning-amber'); + expect(result.drivers[1].medalBg).toBe('bg-gray-300'); + expect(result.drivers[1].medalColor).toBe('text-gray-300'); + expect(result.drivers[2].medalBg).toBe('bg-orange-700'); + expect(result.drivers[2].medalColor).toBe('text-orange-700'); + expect(result.drivers[3].medalBg).toBe('bg-gray-800'); + expect(result.drivers[3].medalColor).toBe('text-gray-400'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-123', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar.jpg', + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].name).toBe(driverDTOs[0].name); + expect(result.drivers[0].nationality).toBe(driverDTOs[0].nationality); + expect(result.drivers[0].avatarUrl).toBe(driverDTOs[0].avatarUrl); + expect(result.drivers[0].skillLevel).toBe(driverDTOs[0].skillLevel); + }); + + it('should not modify the input DTO', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-123', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar.jpg', + }, + ]; + + const originalDTO = JSON.parse(JSON.stringify(driverDTOs)); + DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(driverDTOs).toEqual(originalDTO); + }); + + it('should handle large numbers correctly', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 999999.99, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 10000, + wins: 2500, + podiums: 5000, + isActive: true, + rank: 1, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].rating).toBe(999999.99); + expect(result.drivers[0].wins).toBe(2500); + expect(result.drivers[0].podiums).toBe(5000); + expect(result.drivers[0].racesCompleted).toBe(10000); + expect(result.drivers[0].winRate).toBe('25.0'); + }); + }); + + describe('edge cases', () => { + it('should handle null/undefined avatar URLs', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: null as any, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].avatarUrl).toBe(''); + expect(result.podium[0].avatarUrl).toBe(''); + }); + + it('should handle null/undefined rating', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: null as any, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].rating).toBeNull(); + expect(result.podium[0].rating).toBeNull(); + }); + + it('should handle zero races completed for win rate calculation', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 0, + wins: 0, + podiums: 0, + isActive: true, + rank: 1, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].winRate).toBe('0.0'); + }); + + it('should handle rank 0', () => { + const driverDTOs: DriverLeaderboardItemDTO[] = [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 0, + }, + ]; + + const result = DriverRankingsViewDataBuilder.build(driverDTOs); + + expect(result.drivers[0].rank).toBe(0); + expect(result.drivers[0].medalBg).toBe('bg-gray-800'); + expect(result.drivers[0].medalColor).toBe('text-gray-400'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/DriversViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/DriversViewDataBuilder.test.ts new file mode 100644 index 000000000..9f818a79f --- /dev/null +++ b/apps/website/lib/builders/view-data/DriversViewDataBuilder.test.ts @@ -0,0 +1,382 @@ +import { describe, it, expect } from 'vitest'; +import { DriversViewDataBuilder } from './DriversViewDataBuilder'; +import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; + +describe('DriversViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + category: 'Elite', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/john.jpg', + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.75, + skillLevel: 'Advanced', + category: 'Pro', + nationality: 'Canada', + racesCompleted: 120, + wins: 15, + podiums: 45, + isActive: true, + rank: 2, + avatarUrl: 'https://example.com/jane.jpg', + }, + ], + totalRaces: 270, + totalWins: 40, + activeCount: 2, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers).toHaveLength(2); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[0].name).toBe('John Doe'); + expect(result.drivers[0].rating).toBe(1234.56); + expect(result.drivers[0].ratingLabel).toBe('1,235'); + expect(result.drivers[0].skillLevel).toBe('Pro'); + expect(result.drivers[0].category).toBe('Elite'); + expect(result.drivers[0].nationality).toBe('USA'); + expect(result.drivers[0].racesCompleted).toBe(150); + expect(result.drivers[0].wins).toBe(25); + expect(result.drivers[0].podiums).toBe(60); + expect(result.drivers[0].isActive).toBe(true); + expect(result.drivers[0].rank).toBe(1); + expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg'); + + expect(result.drivers[1].id).toBe('driver-2'); + expect(result.drivers[1].name).toBe('Jane Smith'); + expect(result.drivers[1].rating).toBe(1100.75); + expect(result.drivers[1].ratingLabel).toBe('1,101'); + expect(result.drivers[1].skillLevel).toBe('Advanced'); + expect(result.drivers[1].category).toBe('Pro'); + expect(result.drivers[1].nationality).toBe('Canada'); + expect(result.drivers[1].racesCompleted).toBe(120); + expect(result.drivers[1].wins).toBe(15); + expect(result.drivers[1].podiums).toBe(45); + expect(result.drivers[1].isActive).toBe(true); + expect(result.drivers[1].rank).toBe(2); + expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg'); + + expect(result.totalRaces).toBe(270); + expect(result.totalRacesLabel).toBe('270'); + expect(result.totalWins).toBe(40); + expect(result.totalWinsLabel).toBe('40'); + expect(result.activeCount).toBe(2); + expect(result.activeCountLabel).toBe('2'); + expect(result.totalDriversLabel).toBe('2'); + }); + + it('should handle drivers with missing optional fields', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].category).toBeUndefined(); + expect(result.drivers[0].avatarUrl).toBeUndefined(); + }); + + it('should handle empty drivers array', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [], + totalRaces: 0, + totalWins: 0, + activeCount: 0, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers).toEqual([]); + expect(result.totalRaces).toBe(0); + expect(result.totalRacesLabel).toBe('0'); + expect(result.totalWins).toBe(0); + expect(result.totalWinsLabel).toBe('0'); + expect(result.activeCount).toBe(0); + expect(result.activeCountLabel).toBe('0'); + expect(result.totalDriversLabel).toBe('0'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + category: 'Elite', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/john.jpg', + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name); + expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality); + expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel); + expect(result.totalRaces).toBe(driversDTO.totalRaces); + expect(result.totalWins).toBe(driversDTO.totalWins); + expect(result.activeCount).toBe(driversDTO.activeCount); + }); + + it('should not modify the input DTO', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + category: 'Elite', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/john.jpg', + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const originalDTO = JSON.parse(JSON.stringify(driversDTO)); + DriversViewDataBuilder.build(driversDTO); + + expect(driversDTO).toEqual(originalDTO); + }); + + it('should transform all numeric fields to formatted strings where appropriate', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + // Rating label should be a formatted string + expect(typeof result.drivers[0].ratingLabel).toBe('string'); + expect(result.drivers[0].ratingLabel).toBe('1,235'); + + // Total counts should be formatted strings + expect(typeof result.totalRacesLabel).toBe('string'); + expect(result.totalRacesLabel).toBe('150'); + expect(typeof result.totalWinsLabel).toBe('string'); + expect(result.totalWinsLabel).toBe('25'); + expect(typeof result.activeCountLabel).toBe('string'); + expect(result.activeCountLabel).toBe('1'); + expect(typeof result.totalDriversLabel).toBe('string'); + expect(result.totalDriversLabel).toBe('1'); + }); + + it('should handle large numbers correctly', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 999999.99, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 10000, + wins: 2500, + podiums: 5000, + isActive: true, + rank: 1, + }, + ], + totalRaces: 10000, + totalWins: 2500, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].ratingLabel).toBe('1,000,000'); + expect(result.totalRacesLabel).toBe('10,000'); + expect(result.totalWinsLabel).toBe('2,500'); + expect(result.activeCountLabel).toBe('1'); + expect(result.totalDriversLabel).toBe('1'); + }); + }); + + describe('edge cases', () => { + it('should handle null/undefined rating', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 0, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].ratingLabel).toBe('0'); + }); + + it('should handle drivers with no category', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].category).toBeUndefined(); + }); + + it('should handle inactive drivers', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'Pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: false, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 0, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.drivers[0].isActive).toBe(false); + expect(result.activeCount).toBe(0); + expect(result.activeCountLabel).toBe('0'); + }); + }); + + describe('derived fields', () => { + it('should correctly calculate total drivers label', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 }, + { id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 }, + { id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 }, + ], + totalRaces: 350, + totalWins: 45, + activeCount: 2, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.totalDriversLabel).toBe('3'); + }); + + it('should correctly calculate active count', () => { + const driversDTO: DriversLeaderboardDTO = { + drivers: [ + { id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 }, + { id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 }, + { id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 }, + ], + totalRaces: 350, + totalWins: 45, + activeCount: 2, + }; + + const result = DriversViewDataBuilder.build(driversDTO); + + expect(result.activeCount).toBe(2); + expect(result.activeCountLabel).toBe('2'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.test.ts new file mode 100644 index 000000000..5108fcfe1 --- /dev/null +++ b/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect } from 'vitest'; +import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder'; +import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO'; + +describe('ForgotPasswordViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform ForgotPasswordPageDTO to ForgotPasswordViewData correctly', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result).toEqual({ + returnTo: '/login', + showSuccess: false, + formState: { + fields: { + email: { value: '', error: undefined, touched: false, validating: false }, + }, + isValid: true, + isSubmitting: false, + submitError: undefined, + submitCount: 0, + }, + isSubmitting: false, + submitError: undefined, + }); + }); + + it('should handle empty returnTo path', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.returnTo).toBe(''); + }); + + it('should handle returnTo with query parameters', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login?error=expired', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.returnTo).toBe('/login?error=expired'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.returnTo).toBe(forgotPasswordPageDTO.returnTo); + }); + + it('should not modify the input DTO', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const originalDTO = { ...forgotPasswordPageDTO }; + ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(forgotPasswordPageDTO).toEqual(originalDTO); + }); + + it('should initialize form field with default values', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.formState.fields.email.value).toBe(''); + expect(result.formState.fields.email.error).toBeUndefined(); + expect(result.formState.fields.email.touched).toBe(false); + expect(result.formState.fields.email.validating).toBe(false); + }); + + it('should initialize form state with default values', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.formState.isValid).toBe(true); + expect(result.formState.isSubmitting).toBe(false); + expect(result.formState.submitError).toBeUndefined(); + expect(result.formState.submitCount).toBe(0); + }); + + it('should initialize UI state flags correctly', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.showSuccess).toBe(false); + expect(result.isSubmitting).toBe(false); + expect(result.submitError).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + it('should handle returnTo with encoded characters', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login?redirect=%2Fdashboard', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.returnTo).toBe('/login?redirect=%2Fdashboard'); + }); + + it('should handle returnTo with hash fragment', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login#section', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.returnTo).toBe('/login#section'); + }); + }); + + describe('form state structure', () => { + it('should have email field', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + expect(result.formState.fields).toHaveProperty('email'); + }); + + it('should have consistent field state structure', () => { + const forgotPasswordPageDTO: ForgotPasswordPageDTO = { + returnTo: '/login', + }; + + const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); + + const field = result.formState.fields.email; + expect(field).toHaveProperty('value'); + expect(field).toHaveProperty('error'); + expect(field).toHaveProperty('touched'); + expect(field).toHaveProperty('validating'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/HealthViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/HealthViewDataBuilder.test.ts new file mode 100644 index 000000000..3146443b2 --- /dev/null +++ b/apps/website/lib/builders/view-data/HealthViewDataBuilder.test.ts @@ -0,0 +1,553 @@ +import { describe, it, expect } from 'vitest'; +import { HealthViewDataBuilder, HealthDTO } from './HealthViewDataBuilder'; + +describe('HealthViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform HealthDTO to HealthViewData correctly', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: 99.95, + responseTime: 150, + errorRate: 0.05, + lastCheck: new Date().toISOString(), + checksPassed: 995, + checksFailed: 5, + components: [ + { + name: 'Database', + status: 'ok', + lastCheck: new Date().toISOString(), + responseTime: 50, + errorRate: 0.01, + }, + { + name: 'API', + status: 'ok', + lastCheck: new Date().toISOString(), + responseTime: 100, + errorRate: 0.02, + }, + ], + alerts: [ + { + id: 'alert-1', + type: 'info', + title: 'System Update', + message: 'System updated successfully', + timestamp: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe('ok'); + expect(result.overallStatus.statusLabel).toBe('Healthy'); + expect(result.overallStatus.statusColor).toBe('#10b981'); + expect(result.overallStatus.statusIcon).toBe('✓'); + expect(result.metrics.uptime).toBe('99.95%'); + expect(result.metrics.responseTime).toBe('150ms'); + expect(result.metrics.errorRate).toBe('0.05%'); + expect(result.metrics.checksPassed).toBe(995); + expect(result.metrics.checksFailed).toBe(5); + expect(result.metrics.totalChecks).toBe(1000); + expect(result.metrics.successRate).toBe('99.5%'); + expect(result.components).toHaveLength(2); + expect(result.components[0].name).toBe('Database'); + expect(result.components[0].status).toBe('ok'); + expect(result.components[0].statusLabel).toBe('Healthy'); + expect(result.alerts).toHaveLength(1); + expect(result.alerts[0].id).toBe('alert-1'); + expect(result.alerts[0].type).toBe('info'); + expect(result.hasAlerts).toBe(true); + expect(result.hasDegradedComponents).toBe(false); + expect(result.hasErrorComponents).toBe(false); + }); + + it('should handle missing optional fields gracefully', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe('ok'); + expect(result.metrics.uptime).toBe('N/A'); + expect(result.metrics.responseTime).toBe('N/A'); + expect(result.metrics.errorRate).toBe('N/A'); + expect(result.metrics.checksPassed).toBe(0); + expect(result.metrics.checksFailed).toBe(0); + expect(result.metrics.totalChecks).toBe(0); + expect(result.metrics.successRate).toBe('N/A'); + expect(result.components).toEqual([]); + expect(result.alerts).toEqual([]); + expect(result.hasAlerts).toBe(false); + expect(result.hasDegradedComponents).toBe(false); + expect(result.hasErrorComponents).toBe(false); + }); + + it('should handle degraded status correctly', () => { + const healthDTO: HealthDTO = { + status: 'degraded', + timestamp: new Date().toISOString(), + uptime: 95.5, + responseTime: 500, + errorRate: 4.5, + components: [ + { + name: 'Database', + status: 'degraded', + lastCheck: new Date().toISOString(), + responseTime: 200, + errorRate: 2.0, + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe('degraded'); + expect(result.overallStatus.statusLabel).toBe('Degraded'); + expect(result.overallStatus.statusColor).toBe('#f59e0b'); + expect(result.overallStatus.statusIcon).toBe('⚠'); + expect(result.metrics.uptime).toBe('95.50%'); + expect(result.metrics.responseTime).toBe('500ms'); + expect(result.metrics.errorRate).toBe('4.50%'); + expect(result.hasDegradedComponents).toBe(true); + }); + + it('should handle error status correctly', () => { + const healthDTO: HealthDTO = { + status: 'error', + timestamp: new Date().toISOString(), + uptime: 85.2, + responseTime: 2000, + errorRate: 14.8, + components: [ + { + name: 'Database', + status: 'error', + lastCheck: new Date().toISOString(), + responseTime: 1500, + errorRate: 10.0, + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe('error'); + expect(result.overallStatus.statusLabel).toBe('Error'); + expect(result.overallStatus.statusColor).toBe('#ef4444'); + expect(result.overallStatus.statusIcon).toBe('✕'); + expect(result.metrics.uptime).toBe('85.20%'); + expect(result.metrics.responseTime).toBe('2.00s'); + expect(result.metrics.errorRate).toBe('14.80%'); + expect(result.hasErrorComponents).toBe(true); + }); + + it('should handle multiple components with mixed statuses', () => { + const healthDTO: HealthDTO = { + status: 'degraded', + timestamp: new Date().toISOString(), + components: [ + { + name: 'Database', + status: 'ok', + lastCheck: new Date().toISOString(), + }, + { + name: 'API', + status: 'degraded', + lastCheck: new Date().toISOString(), + }, + { + name: 'Cache', + status: 'error', + lastCheck: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.components).toHaveLength(3); + expect(result.hasDegradedComponents).toBe(true); + expect(result.hasErrorComponents).toBe(true); + expect(result.components[0].statusLabel).toBe('Healthy'); + expect(result.components[1].statusLabel).toBe('Degraded'); + expect(result.components[2].statusLabel).toBe('Error'); + }); + + it('should handle multiple alerts with different severities', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + alerts: [ + { + id: 'alert-1', + type: 'critical', + title: 'Critical Alert', + message: 'Critical issue detected', + timestamp: new Date().toISOString(), + }, + { + id: 'alert-2', + type: 'warning', + title: 'Warning Alert', + message: 'Warning message', + timestamp: new Date().toISOString(), + }, + { + id: 'alert-3', + type: 'info', + title: 'Info Alert', + message: 'Informational message', + timestamp: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.alerts).toHaveLength(3); + expect(result.hasAlerts).toBe(true); + expect(result.alerts[0].severity).toBe('Critical'); + expect(result.alerts[0].severityColor).toBe('#ef4444'); + expect(result.alerts[1].severity).toBe('Warning'); + expect(result.alerts[1].severityColor).toBe('#f59e0b'); + expect(result.alerts[2].severity).toBe('Info'); + expect(result.alerts[2].severityColor).toBe('#3b82f6'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const now = new Date(); + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: now.toISOString(), + uptime: 99.99, + responseTime: 100, + errorRate: 0.01, + lastCheck: now.toISOString(), + checksPassed: 9999, + checksFailed: 1, + components: [ + { + name: 'Test Component', + status: 'ok', + lastCheck: now.toISOString(), + responseTime: 50, + errorRate: 0.005, + }, + ], + alerts: [ + { + id: 'test-alert', + type: 'info', + title: 'Test Alert', + message: 'Test message', + timestamp: now.toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe(healthDTO.status); + expect(result.overallStatus.timestamp).toBe(healthDTO.timestamp); + expect(result.metrics.uptime).toBe('99.99%'); + expect(result.metrics.responseTime).toBe('100ms'); + expect(result.metrics.errorRate).toBe('0.01%'); + expect(result.metrics.lastCheck).toBe(healthDTO.lastCheck); + expect(result.metrics.checksPassed).toBe(healthDTO.checksPassed); + expect(result.metrics.checksFailed).toBe(healthDTO.checksFailed); + expect(result.components[0].name).toBe(healthDTO.components![0].name); + expect(result.components[0].status).toBe(healthDTO.components![0].status); + expect(result.alerts[0].id).toBe(healthDTO.alerts![0].id); + expect(result.alerts[0].type).toBe(healthDTO.alerts![0].type); + }); + + it('should not modify the input DTO', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: 99.95, + responseTime: 150, + errorRate: 0.05, + components: [ + { + name: 'Database', + status: 'ok', + lastCheck: new Date().toISOString(), + }, + ], + }; + + const originalDTO = JSON.parse(JSON.stringify(healthDTO)); + HealthViewDataBuilder.build(healthDTO); + + expect(healthDTO).toEqual(originalDTO); + }); + + it('should transform all numeric fields to formatted strings', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: 99.95, + responseTime: 150, + errorRate: 0.05, + checksPassed: 995, + checksFailed: 5, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(typeof result.metrics.uptime).toBe('string'); + expect(typeof result.metrics.responseTime).toBe('string'); + expect(typeof result.metrics.errorRate).toBe('string'); + expect(typeof result.metrics.successRate).toBe('string'); + }); + + it('should handle large numbers correctly', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: 99.999, + responseTime: 5000, + errorRate: 0.001, + checksPassed: 999999, + checksFailed: 1, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.uptime).toBe('100.00%'); + expect(result.metrics.responseTime).toBe('5.00s'); + expect(result.metrics.errorRate).toBe('0.00%'); + expect(result.metrics.successRate).toBe('100.0%'); + }); + }); + + describe('edge cases', () => { + it('should handle null/undefined numeric fields', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: null as any, + responseTime: undefined, + errorRate: null as any, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.uptime).toBe('N/A'); + expect(result.metrics.responseTime).toBe('N/A'); + expect(result.metrics.errorRate).toBe('N/A'); + }); + + it('should handle negative numeric values', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: -1, + responseTime: -100, + errorRate: -0.5, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.uptime).toBe('N/A'); + expect(result.metrics.responseTime).toBe('N/A'); + expect(result.metrics.errorRate).toBe('N/A'); + }); + + it('should handle empty components and alerts arrays', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + components: [], + alerts: [], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.components).toEqual([]); + expect(result.alerts).toEqual([]); + expect(result.hasAlerts).toBe(false); + expect(result.hasDegradedComponents).toBe(false); + expect(result.hasErrorComponents).toBe(false); + }); + + it('should handle component with missing optional fields', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + components: [ + { + name: 'Test Component', + status: 'ok', + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.components[0].lastCheck).toBeDefined(); + expect(result.components[0].formattedLastCheck).toBeDefined(); + expect(result.components[0].responseTime).toBe('N/A'); + expect(result.components[0].errorRate).toBe('N/A'); + }); + + it('should handle alert with missing optional fields', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + alerts: [ + { + id: 'alert-1', + type: 'info', + title: 'Test Alert', + message: 'Test message', + timestamp: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.alerts[0].id).toBe('alert-1'); + expect(result.alerts[0].type).toBe('info'); + expect(result.alerts[0].title).toBe('Test Alert'); + expect(result.alerts[0].message).toBe('Test message'); + expect(result.alerts[0].timestamp).toBeDefined(); + expect(result.alerts[0].formattedTimestamp).toBeDefined(); + expect(result.alerts[0].relativeTime).toBeDefined(); + }); + + it('should handle unknown status', () => { + const healthDTO: HealthDTO = { + status: 'unknown', + timestamp: new Date().toISOString(), + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.overallStatus.status).toBe('unknown'); + expect(result.overallStatus.statusLabel).toBe('Unknown'); + expect(result.overallStatus.statusColor).toBe('#6b7280'); + expect(result.overallStatus.statusIcon).toBe('?'); + }); + }); + + describe('derived fields', () => { + it('should correctly calculate hasAlerts', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + alerts: [ + { + id: 'alert-1', + type: 'info', + title: 'Test', + message: 'Test message', + timestamp: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.hasAlerts).toBe(true); + }); + + it('should correctly calculate hasDegradedComponents', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + components: [ + { + name: 'Component 1', + status: 'ok', + lastCheck: new Date().toISOString(), + }, + { + name: 'Component 2', + status: 'degraded', + lastCheck: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.hasDegradedComponents).toBe(true); + }); + + it('should correctly calculate hasErrorComponents', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + components: [ + { + name: 'Component 1', + status: 'ok', + lastCheck: new Date().toISOString(), + }, + { + name: 'Component 2', + status: 'error', + lastCheck: new Date().toISOString(), + }, + ], + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.hasErrorComponents).toBe(true); + }); + + it('should correctly calculate totalChecks', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + checksPassed: 100, + checksFailed: 20, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.totalChecks).toBe(120); + }); + + it('should correctly calculate successRate', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + checksPassed: 90, + checksFailed: 10, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.successRate).toBe('90.0%'); + }); + + it('should handle zero checks correctly', () => { + const healthDTO: HealthDTO = { + status: 'ok', + timestamp: new Date().toISOString(), + checksPassed: 0, + checksFailed: 0, + }; + + const result = HealthViewDataBuilder.build(healthDTO); + + expect(result.metrics.totalChecks).toBe(0); + expect(result.metrics.successRate).toBe('N/A'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.test.ts new file mode 100644 index 000000000..b2cafb243 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.test.ts @@ -0,0 +1,600 @@ +import { describe, it, expect } from 'vitest'; +import { LeaderboardsViewDataBuilder } from './LeaderboardsViewDataBuilder'; + +describe('LeaderboardsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform Leaderboards DTO to LeaderboardsViewData correctly', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar1.jpg', + }, + { + id: 'driver-2', + name: 'Jane Smith', + rating: 1100.0, + skillLevel: 'advanced', + nationality: 'Canada', + racesCompleted: 100, + wins: 15, + podiums: 40, + isActive: true, + rank: 2, + avatarUrl: 'https://example.com/avatar2.jpg', + }, + ], + totalRaces: 250, + totalWins: 40, + activeCount: 2, + }, + teams: { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo1.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + { + id: 'team-2', + name: 'Speed Demons', + tag: 'SD', + logoUrl: 'https://example.com/logo2.jpg', + memberCount: 8, + rating: 1200, + totalWins: 20, + totalRaces: 150, + performanceLevel: 'advanced', + isRecruiting: true, + createdAt: '2023-06-01', + }, + ], + recruitingCount: 5, + groupsBySkillLevel: 'pro,advanced,intermediate', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo1.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + { + id: 'team-2', + name: 'Speed Demons', + tag: 'SD', + logoUrl: 'https://example.com/logo2.jpg', + memberCount: 8, + rating: 1200, + totalWins: 20, + totalRaces: 150, + performanceLevel: 'advanced', + isRecruiting: true, + createdAt: '2023-06-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + // Verify drivers + expect(result.drivers).toHaveLength(2); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[0].name).toBe('John Doe'); + expect(result.drivers[0].rating).toBe(1234.56); + expect(result.drivers[0].skillLevel).toBe('pro'); + expect(result.drivers[0].nationality).toBe('USA'); + expect(result.drivers[0].wins).toBe(25); + expect(result.drivers[0].podiums).toBe(60); + expect(result.drivers[0].racesCompleted).toBe(150); + expect(result.drivers[0].rank).toBe(1); + expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); + expect(result.drivers[0].position).toBe(1); + + // Verify teams + expect(result.teams).toHaveLength(2); + expect(result.teams[0].id).toBe('team-1'); + expect(result.teams[0].name).toBe('Racing Team Alpha'); + expect(result.teams[0].tag).toBe('RTA'); + expect(result.teams[0].memberCount).toBe(15); + expect(result.teams[0].totalWins).toBe(50); + expect(result.teams[0].totalRaces).toBe(200); + expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg'); + expect(result.teams[0].position).toBe(1); + expect(result.teams[0].isRecruiting).toBe(false); + expect(result.teams[0].performanceLevel).toBe('elite'); + expect(result.teams[0].rating).toBe(1500); + expect(result.teams[0].category).toBeUndefined(); + }); + + it('should handle empty driver and team arrays', () => { + const leaderboardsDTO = { + drivers: { + drivers: [], + totalRaces: 0, + totalWins: 0, + activeCount: 0, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers).toEqual([]); + expect(result.teams).toEqual([]); + }); + + it('should handle missing avatar URLs with empty string fallback', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].avatarUrl).toBe(''); + expect(result.teams[0].logoUrl).toBe(''); + }); + + it('should handle missing optional team fields with defaults', () => { + const leaderboardsDTO = { + drivers: { + drivers: [], + totalRaces: 0, + totalWins: 0, + activeCount: 0, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.teams[0].rating).toBe(0); + expect(result.teams[0].logoUrl).toBe(''); + }); + + it('should calculate position based on index', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { id: 'driver-1', name: 'Driver 1', rating: 1000, skillLevel: 'pro', nationality: 'USA', racesCompleted: 100, wins: 10, podiums: 30, isActive: true, rank: 1 }, + { id: 'driver-2', name: 'Driver 2', rating: 900, skillLevel: 'advanced', nationality: 'Canada', racesCompleted: 80, wins: 8, podiums: 25, isActive: true, rank: 2 }, + { id: 'driver-3', name: 'Driver 3', rating: 800, skillLevel: 'intermediate', nationality: 'UK', racesCompleted: 60, wins: 5, podiums: 15, isActive: true, rank: 3 }, + ], + totalRaces: 240, + totalWins: 23, + activeCount: 3, + }, + teams: { + teams: [], + recruitingCount: 1, + groupsBySkillLevel: 'elite,advanced,intermediate', + topTeams: [ + { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, + { id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' }, + { id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].position).toBe(1); + expect(result.drivers[1].position).toBe(2); + expect(result.drivers[2].position).toBe(3); + + expect(result.teams[0].position).toBe(1); + expect(result.teams[1].position).toBe(2); + expect(result.teams[2].position).toBe(3); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-123', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar.jpg', + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 5, + groupsBySkillLevel: 'pro,advanced', + topTeams: [ + { + id: 'team-123', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].name).toBe(leaderboardsDTO.drivers.drivers[0].name); + expect(result.drivers[0].nationality).toBe(leaderboardsDTO.drivers.drivers[0].nationality); + expect(result.drivers[0].avatarUrl).toBe(leaderboardsDTO.drivers.drivers[0].avatarUrl); + expect(result.teams[0].name).toBe(leaderboardsDTO.teams.topTeams[0].name); + expect(result.teams[0].tag).toBe(leaderboardsDTO.teams.topTeams[0].tag); + expect(result.teams[0].logoUrl).toBe(leaderboardsDTO.teams.topTeams[0].logoUrl); + }); + + it('should not modify the input DTO', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-123', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar.jpg', + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 5, + groupsBySkillLevel: 'pro,advanced', + topTeams: [ + { + id: 'team-123', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const originalDTO = JSON.parse(JSON.stringify(leaderboardsDTO)); + LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(leaderboardsDTO).toEqual(originalDTO); + }); + + it('should handle large numbers correctly', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 999999.99, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 10000, + wins: 2500, + podiums: 5000, + isActive: true, + rank: 1, + avatarUrl: 'https://example.com/avatar.jpg', + }, + ], + totalRaces: 10000, + totalWins: 2500, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 100, + rating: 999999, + totalWins: 5000, + totalRaces: 10000, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].rating).toBe(999999.99); + expect(result.drivers[0].wins).toBe(2500); + expect(result.drivers[0].podiums).toBe(5000); + expect(result.drivers[0].racesCompleted).toBe(10000); + expect(result.teams[0].rating).toBe(999999); + expect(result.teams[0].totalWins).toBe(5000); + expect(result.teams[0].totalRaces).toBe(10000); + }); + }); + + describe('edge cases', () => { + it('should handle null/undefined avatar URLs', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: 1234.56, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + avatarUrl: null as any, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: undefined as any, + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].avatarUrl).toBe(''); + expect(result.teams[0].logoUrl).toBe(''); + }); + + it('should handle null/undefined rating', () => { + const leaderboardsDTO = { + drivers: { + drivers: [ + { + id: 'driver-1', + name: 'John Doe', + rating: null as any, + skillLevel: 'pro', + nationality: 'USA', + racesCompleted: 150, + wins: 25, + podiums: 60, + isActive: true, + rank: 1, + }, + ], + totalRaces: 150, + totalWins: 25, + activeCount: 1, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + rating: null as any, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.drivers[0].rating).toBeNull(); + expect(result.teams[0].rating).toBe(0); + }); + + it('should handle null/undefined totalWins and totalRaces', () => { + const leaderboardsDTO = { + drivers: { + drivers: [], + totalRaces: 0, + totalWins: 0, + activeCount: 0, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: null as any, + totalRaces: null as any, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.teams[0].totalWins).toBe(0); + expect(result.teams[0].totalRaces).toBe(0); + }); + + it('should handle empty performance level', () => { + const leaderboardsDTO = { + drivers: { + drivers: [], + totalRaces: 0, + totalWins: 0, + activeCount: 0, + }, + teams: { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: '', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + }, + }; + + const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); + + expect(result.teams[0].performanceLevel).toBe('N/A'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.test.ts new file mode 100644 index 000000000..c664385c9 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('LeagueCoverViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to LeagueCoverViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle JPEG cover images', () => { + const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle WebP cover images', () => { + const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/webp', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/webp'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + LeagueCoverViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle large cover images', () => { + const buffer = new Uint8Array(2 * 1024 * 1024); // 2MB + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle buffer with all zeros', () => { + const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with all ones', () => { + const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueCoverViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.test.ts new file mode 100644 index 000000000..229dfea94 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.test.ts @@ -0,0 +1,577 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueDetailViewDataBuilder } from './LeagueDetailViewDataBuilder'; +import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; +import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; +import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; +import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; +import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; + +describe('LeagueDetailViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform league DTOs to LeagueDetailViewData correctly', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Pro League', + description: 'A competitive league for experienced drivers', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Solo • 32 max', + }, + usedSlots: 25, + category: 'competitive', + scoring: { + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-1', + scoringPresetName: 'Standard', + dropPolicySummary: 'Drop 2 worst races', + scoringPatternSummary: 'Points based on finish position', + }, + timingSummary: 'Weekly races on Sundays', + logoUrl: 'https://example.com/logo.png', + pendingJoinRequestsCount: 3, + pendingProtestsCount: 1, + walletBalance: 1000, + }; + + const owner: GetDriverOutputDTO = { + id: 'owner-1', + name: 'John Doe', + iracingId: '12345', + country: 'USA', + bio: 'Experienced driver', + joinedAt: '2023-01-01T00:00:00.000Z', + avatarUrl: 'https://example.com/avatar.jpg', + }; + + const scoringConfig: LeagueScoringConfigDTO = { + id: 'config-1', + leagueId: 'league-1', + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-1', + scoringPresetName: 'Standard', + dropPolicySummary: 'Drop 2 worst races', + scoringPatternSummary: 'Points based on finish position', + dropRaces: 2, + pointsPerRace: 100, + pointsForWin: 25, + pointsForPodium: [20, 15, 10], + }; + + const memberships: LeagueMembershipsDTO = { + members: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + { + driverId: 'driver-2', + driver: { + id: 'driver-2', + name: 'Bob', + iracingId: '22222', + country: 'Germany', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + role: 'steward', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + { + driverId: 'driver-3', + driver: { + id: 'driver-3', + name: 'Charlie', + iracingId: '33333', + country: 'France', + joinedAt: '2023-08-01T00:00:00.000Z', + }, + role: 'member', + joinedAt: '2023-08-01T00:00:00.000Z', + }, + ], + }; + + const races: RaceDTO[] = [ + { + id: 'race-1', + name: 'Race 1', + date: '2024-01-15T14:00:00.000Z', + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + strengthOfField: 1500, + }, + { + id: 'race-2', + name: 'Race 2', + date: '2024-01-22T14:00:00.000Z', + track: 'Monza', + car: 'Ferrari 488 GT3', + sessionType: 'race', + strengthOfField: 1600, + }, + ]; + + const sponsors: any[] = [ + { + id: 'sponsor-1', + name: 'Sponsor A', + tier: 'main', + logoUrl: 'https://example.com/sponsor-a.png', + websiteUrl: 'https://sponsor-a.com', + tagline: 'Premium racing gear', + }, + ]; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner, + scoringConfig, + memberships, + races, + sponsors, + }); + + expect(result.leagueId).toBe('league-1'); + expect(result.name).toBe('Pro League'); + expect(result.description).toBe('A competitive league for experienced drivers'); + expect(result.logoUrl).toBe('https://example.com/logo.png'); + expect(result.info.name).toBe('Pro League'); + expect(result.info.description).toBe('A competitive league for experienced drivers'); + expect(result.info.membersCount).toBe(3); + expect(result.info.racesCount).toBe(2); + expect(result.info.avgSOF).toBe(1550); + expect(result.info.structure).toBe('Solo • 32 max'); + expect(result.info.scoring).toBe('preset-1'); + expect(result.info.createdAt).toBe('2024-01-01T00:00:00.000Z'); + expect(result.info.discordUrl).toBeUndefined(); + expect(result.info.youtubeUrl).toBeUndefined(); + expect(result.info.websiteUrl).toBeUndefined(); + expect(result.ownerSummary).not.toBeNull(); + expect(result.ownerSummary?.driverId).toBe('owner-1'); + expect(result.ownerSummary?.driverName).toBe('John Doe'); + expect(result.ownerSummary?.avatarUrl).toBe('https://example.com/avatar.jpg'); + expect(result.ownerSummary?.roleBadgeText).toBe('Owner'); + expect(result.adminSummaries).toHaveLength(1); + expect(result.adminSummaries[0].driverId).toBe('driver-1'); + expect(result.adminSummaries[0].driverName).toBe('Alice'); + expect(result.adminSummaries[0].roleBadgeText).toBe('Admin'); + expect(result.stewardSummaries).toHaveLength(1); + expect(result.stewardSummaries[0].driverId).toBe('driver-2'); + expect(result.stewardSummaries[0].driverName).toBe('Bob'); + expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward'); + expect(result.memberSummaries).toHaveLength(1); + expect(result.memberSummaries[0].driverId).toBe('driver-3'); + expect(result.memberSummaries[0].driverName).toBe('Charlie'); + expect(result.memberSummaries[0].roleBadgeText).toBe('Member'); + expect(result.sponsors).toHaveLength(1); + expect(result.sponsors[0].id).toBe('sponsor-1'); + expect(result.sponsors[0].name).toBe('Sponsor A'); + expect(result.sponsors[0].tier).toBe('main'); + expect(result.walletBalance).toBe(1000); + expect(result.pendingProtestsCount).toBe(1); + expect(result.pendingJoinRequestsCount).toBe(3); + }); + + it('should handle league with no owner', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(result.ownerSummary).toBeNull(); + }); + + it('should handle league with no scoring config', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(result.info.scoring).toBe('Standard'); + }); + + it('should handle league with no races', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(result.info.racesCount).toBe(0); + expect(result.info.avgSOF).toBeNull(); + expect(result.runningRaces).toEqual([]); + expect(result.nextRace).toBeUndefined(); + expect(result.seasonProgress).toEqual({ + completedRaces: 0, + totalRaces: 0, + percentage: 0, + }); + expect(result.recentResults).toEqual([]); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Solo • 32 max', + }, + usedSlots: 20, + category: 'test', + scoring: { + gameId: 'game-1', + gameName: 'Test Game', + primaryChampionshipType: 'Test Type', + scoringPresetId: 'preset-1', + scoringPresetName: 'Test Preset', + dropPolicySummary: 'Test drop policy', + scoringPatternSummary: 'Test pattern', + }, + timingSummary: 'Test timing', + logoUrl: 'https://example.com/test.png', + pendingJoinRequestsCount: 5, + pendingProtestsCount: 2, + walletBalance: 500, + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(result.leagueId).toBe(league.id); + expect(result.name).toBe(league.name); + expect(result.description).toBe(league.description); + expect(result.logoUrl).toBe(league.logoUrl); + expect(result.walletBalance).toBe(league.walletBalance); + expect(result.pendingProtestsCount).toBe(league.pendingProtestsCount); + expect(result.pendingJoinRequestsCount).toBe(league.pendingJoinRequestsCount); + }); + + it('should not modify the input DTOs', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 20, + }; + + const originalLeague = JSON.parse(JSON.stringify(league)); + LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(league).toEqual(originalLeague); + }); + }); + + describe('edge cases', () => { + it('should handle league with missing optional fields', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Minimal League', + description: '', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races: [], + sponsors: [], + }); + + expect(result.description).toBe(''); + expect(result.logoUrl).toBeUndefined(); + expect(result.info.description).toBe(''); + expect(result.info.discordUrl).toBeUndefined(); + expect(result.info.youtubeUrl).toBeUndefined(); + expect(result.info.websiteUrl).toBeUndefined(); + }); + + it('should handle races with missing strengthOfField', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const races: RaceDTO[] = [ + { + id: 'race-1', + name: 'Race 1', + date: '2024-01-15T14:00:00.000Z', + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ]; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races, + sponsors: [], + }); + + expect(result.info.avgSOF).toBeNull(); + }); + + it('should handle races with zero strengthOfField', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const races: RaceDTO[] = [ + { + id: 'race-1', + name: 'Race 1', + date: '2024-01-15T14:00:00.000Z', + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + strengthOfField: 0, + }, + ]; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races, + sponsors: [], + }); + + expect(result.info.avgSOF).toBeNull(); + }); + + it('should handle races with different dates for next race calculation', () => { + const now = new Date(); + const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now + + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const races: RaceDTO[] = [ + { + id: 'race-1', + name: 'Past Race', + date: pastDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + { + id: 'race-2', + name: 'Future Race', + date: futureDate.toISOString(), + track: 'Monza', + car: 'Ferrari 488 GT3', + sessionType: 'race', + }, + ]; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships: { members: [] }, + races, + sponsors: [], + }); + + expect(result.nextRace).toBeDefined(); + expect(result.nextRace?.id).toBe('race-2'); + expect(result.nextRace?.name).toBe('Future Race'); + expect(result.seasonProgress.completedRaces).toBe(1); + expect(result.seasonProgress.totalRaces).toBe(2); + expect(result.seasonProgress.percentage).toBe(50); + expect(result.recentResults).toHaveLength(1); + expect(result.recentResults[0].raceId).toBe('race-1'); + }); + + it('should handle members with different roles', () => { + const league: LeagueWithCapacityAndScoringDTO = { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 10, + }; + + const memberships: LeagueMembershipsDTO = { + members: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Admin', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + { + driverId: 'driver-2', + driver: { + id: 'driver-2', + name: 'Steward', + iracingId: '22222', + country: 'Germany', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + role: 'steward', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + { + driverId: 'driver-3', + driver: { + id: 'driver-3', + name: 'Member', + iracingId: '33333', + country: 'France', + joinedAt: '2023-08-01T00:00:00.000Z', + }, + role: 'member', + joinedAt: '2023-08-01T00:00:00.000Z', + }, + ], + }; + + const result = LeagueDetailViewDataBuilder.build({ + league, + owner: null, + scoringConfig: null, + memberships, + races: [], + sponsors: [], + }); + + expect(result.adminSummaries).toHaveLength(1); + expect(result.stewardSummaries).toHaveLength(1); + expect(result.memberSummaries).toHaveLength(1); + expect(result.info.membersCount).toBe(3); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.test.ts new file mode 100644 index 000000000..d38e322f7 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueLogoViewDataBuilder } from './LeagueLogoViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('LeagueLogoViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to LeagueLogoViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle SVG league logos', () => { + const buffer = new TextEncoder().encode(''); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/svg+xml', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/svg+xml'); + }); + + it('should handle transparent PNG logos', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + LeagueLogoViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle small logo files', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with special characters', () => { + const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = LeagueLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.test.ts new file mode 100644 index 000000000..372db28cc --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueRosterAdminViewDataBuilder } from './LeagueRosterAdminViewDataBuilder'; +import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; +import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO'; + +describe('LeagueRosterAdminViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform roster DTOs to LeagueRosterAdminViewData correctly', () => { + const members: LeagueRosterMemberDTO[] = [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + { + driverId: 'driver-2', + driver: { + id: 'driver-2', + name: 'Bob', + iracingId: '22222', + country: 'Germany', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + role: 'member', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + ]; + + const joinRequests: LeagueRosterJoinRequestDTO[] = [ + { + id: 'request-1', + leagueId: 'league-1', + driverId: 'driver-3', + requestedAt: '2024-01-15T10:00:00.000Z', + message: 'I would like to join this league', + driver: {}, + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members, + joinRequests, + }); + + expect(result.leagueId).toBe('league-1'); + expect(result.members).toHaveLength(2); + expect(result.members[0].driverId).toBe('driver-1'); + expect(result.members[0].driver.id).toBe('driver-1'); + expect(result.members[0].driver.name).toBe('Alice'); + expect(result.members[0].role).toBe('admin'); + expect(result.members[0].joinedAt).toBe('2023-06-01T00:00:00.000Z'); + expect(result.members[0].formattedJoinedAt).toBeDefined(); + expect(result.members[1].driverId).toBe('driver-2'); + expect(result.members[1].driver.id).toBe('driver-2'); + expect(result.members[1].driver.name).toBe('Bob'); + expect(result.members[1].role).toBe('member'); + expect(result.members[1].joinedAt).toBe('2023-07-01T00:00:00.000Z'); + expect(result.members[1].formattedJoinedAt).toBeDefined(); + expect(result.joinRequests).toHaveLength(1); + expect(result.joinRequests[0].id).toBe('request-1'); + expect(result.joinRequests[0].driver.id).toBe('driver-3'); + expect(result.joinRequests[0].driver.name).toBe('Unknown Driver'); + expect(result.joinRequests[0].requestedAt).toBe('2024-01-15T10:00:00.000Z'); + expect(result.joinRequests[0].formattedRequestedAt).toBeDefined(); + expect(result.joinRequests[0].message).toBe('I would like to join this league'); + }); + + it('should handle empty members and join requests', () => { + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members: [], + joinRequests: [], + }); + + expect(result.leagueId).toBe('league-1'); + expect(result.members).toHaveLength(0); + expect(result.joinRequests).toHaveLength(0); + }); + + it('should handle members without driver details', () => { + const members: LeagueRosterMemberDTO[] = [ + { + driverId: 'driver-1', + driver: undefined as any, + role: 'member', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members, + joinRequests: [], + }); + + expect(result.members[0].driver.name).toBe('Unknown Driver'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const members: LeagueRosterMemberDTO[] = [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + ]; + + const joinRequests: LeagueRosterJoinRequestDTO[] = [ + { + id: 'request-1', + leagueId: 'league-1', + driverId: 'driver-3', + requestedAt: '2024-01-15T10:00:00.000Z', + message: 'I would like to join this league', + driver: {}, + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members, + joinRequests, + }); + + expect(result.leagueId).toBe('league-1'); + expect(result.members[0].driverId).toBe(members[0].driverId); + expect(result.members[0].driver.id).toBe(members[0].driver.id); + expect(result.members[0].driver.name).toBe(members[0].driver.name); + expect(result.members[0].role).toBe(members[0].role); + expect(result.members[0].joinedAt).toBe(members[0].joinedAt); + expect(result.joinRequests[0].id).toBe(joinRequests[0].id); + expect(result.joinRequests[0].requestedAt).toBe(joinRequests[0].requestedAt); + expect(result.joinRequests[0].message).toBe(joinRequests[0].message); + }); + + it('should not modify the input DTOs', () => { + const members: LeagueRosterMemberDTO[] = [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + ]; + + const joinRequests: LeagueRosterJoinRequestDTO[] = [ + { + id: 'request-1', + leagueId: 'league-1', + driverId: 'driver-3', + requestedAt: '2024-01-15T10:00:00.000Z', + message: 'I would like to join this league', + driver: {}, + }, + ]; + + const originalMembers = JSON.parse(JSON.stringify(members)); + const originalRequests = JSON.parse(JSON.stringify(joinRequests)); + + LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members, + joinRequests, + }); + + expect(members).toEqual(originalMembers); + expect(joinRequests).toEqual(originalRequests); + }); + }); + + describe('edge cases', () => { + it('should handle members with missing driver field', () => { + const members: LeagueRosterMemberDTO[] = [ + { + driverId: 'driver-1', + driver: undefined as any, + role: 'member', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members, + joinRequests: [], + }); + + expect(result.members[0].driver.name).toBe('Unknown Driver'); + }); + + it('should handle join requests with missing driver field', () => { + const joinRequests: LeagueRosterJoinRequestDTO[] = [ + { + id: 'request-1', + leagueId: 'league-1', + driverId: 'driver-3', + requestedAt: '2024-01-15T10:00:00.000Z', + message: 'I would like to join this league', + driver: undefined, + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members: [], + joinRequests, + }); + + expect(result.joinRequests[0].driver.name).toBe('Unknown Driver'); + }); + + it('should handle join requests without message', () => { + const joinRequests: LeagueRosterJoinRequestDTO[] = [ + { + id: 'request-1', + leagueId: 'league-1', + driverId: 'driver-3', + requestedAt: '2024-01-15T10:00:00.000Z', + driver: {}, + }, + ]; + + const result = LeagueRosterAdminViewDataBuilder.build({ + leagueId: 'league-1', + members: [], + joinRequests, + }); + + expect(result.joinRequests[0].message).toBeUndefined(); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.test.ts new file mode 100644 index 000000000..213eca602 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.test.ts @@ -0,0 +1,211 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueScheduleViewDataBuilder } from './LeagueScheduleViewDataBuilder'; + +describe('LeagueScheduleViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform schedule DTO to LeagueScheduleViewData correctly', () => { + const now = new Date(); + const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Past Race', + date: pastDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + { + id: 'race-2', + name: 'Future Race', + date: futureDate.toISOString(), + track: 'Monza', + car: 'Ferrari 488 GT3', + sessionType: 'race', + }, + ], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', true); + + expect(result.leagueId).toBe('league-1'); + expect(result.races).toHaveLength(2); + expect(result.races[0].id).toBe('race-1'); + expect(result.races[0].name).toBe('Past Race'); + expect(result.races[0].scheduledAt).toBe(pastDate.toISOString()); + expect(result.races[0].track).toBe('Spa'); + expect(result.races[0].car).toBe('Porsche 911 GT3'); + expect(result.races[0].sessionType).toBe('race'); + expect(result.races[0].isPast).toBe(true); + expect(result.races[0].isUpcoming).toBe(false); + expect(result.races[0].status).toBe('completed'); + expect(result.races[0].isUserRegistered).toBe(false); + expect(result.races[0].canRegister).toBe(false); + expect(result.races[0].canEdit).toBe(true); + expect(result.races[0].canReschedule).toBe(true); + expect(result.races[1].id).toBe('race-2'); + expect(result.races[1].name).toBe('Future Race'); + expect(result.races[1].scheduledAt).toBe(futureDate.toISOString()); + expect(result.races[1].track).toBe('Monza'); + expect(result.races[1].car).toBe('Ferrari 488 GT3'); + expect(result.races[1].sessionType).toBe('race'); + expect(result.races[1].isPast).toBe(false); + expect(result.races[1].isUpcoming).toBe(true); + expect(result.races[1].status).toBe('scheduled'); + expect(result.races[1].isUserRegistered).toBe(false); + expect(result.races[1].canRegister).toBe(true); + expect(result.races[1].canEdit).toBe(true); + expect(result.races[1].canReschedule).toBe(true); + expect(result.currentDriverId).toBe('driver-1'); + expect(result.isAdmin).toBe(true); + }); + + it('should handle empty races list', () => { + const apiDto = { + leagueId: 'league-1', + races: [], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto); + + expect(result.leagueId).toBe('league-1'); + expect(result.races).toHaveLength(0); + }); + + it('should handle non-admin user', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Future Race', + date: futureDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', false); + + expect(result.races[0].canEdit).toBe(false); + expect(result.races[0].canReschedule).toBe(false); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Test Race', + date: futureDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto); + + expect(result.leagueId).toBe(apiDto.leagueId); + expect(result.races[0].id).toBe(apiDto.races[0].id); + expect(result.races[0].name).toBe(apiDto.races[0].name); + expect(result.races[0].scheduledAt).toBe(apiDto.races[0].date); + expect(result.races[0].track).toBe(apiDto.races[0].track); + expect(result.races[0].car).toBe(apiDto.races[0].car); + expect(result.races[0].sessionType).toBe(apiDto.races[0].sessionType); + }); + + it('should not modify the input DTO', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Test Race', + date: futureDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ], + }; + + const originalDto = JSON.parse(JSON.stringify(apiDto)); + LeagueScheduleViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle races with missing optional fields', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Test Race', + date: futureDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto); + + expect(result.races[0].track).toBe('Spa'); + expect(result.races[0].car).toBe('Porsche 911 GT3'); + expect(result.races[0].sessionType).toBe('race'); + }); + + it('should handle races at exactly the current time', () => { + const now = new Date(); + const currentRaceDate = new Date(now.getTime()); + + const apiDto = { + leagueId: 'league-1', + races: [ + { + id: 'race-1', + name: 'Current Race', + date: currentRaceDate.toISOString(), + track: 'Spa', + car: 'Porsche 911 GT3', + sessionType: 'race', + }, + ], + }; + + const result = LeagueScheduleViewDataBuilder.build(apiDto); + + // Race at current time should be considered past + expect(result.races[0].isPast).toBe(true); + expect(result.races[0].isUpcoming).toBe(false); + expect(result.races[0].status).toBe('completed'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.test.ts new file mode 100644 index 000000000..b092115ff --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.test.ts @@ -0,0 +1,464 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueStandingsViewDataBuilder } from './LeagueStandingsViewDataBuilder'; + +describe('LeagueStandingsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform standings DTOs to LeagueStandingsViewData correctly', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: ['race-1', 'race-2'], + }, + { + driverId: 'driver-2', + driver: { + id: 'driver-2', + name: 'Bob', + iracingId: '22222', + country: 'Germany', + }, + points: 1100, + position: 2, + wins: 3, + podiums: 8, + races: 15, + positionChange: -1, + lastRacePoints: 15, + droppedRaceIds: [], + }, + ], + }; + + const membershipsDto = { + members: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'member', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + { + driverId: 'driver-2', + driver: { + id: 'driver-2', + name: 'Bob', + iracingId: '22222', + country: 'Germany', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + role: 'member', + joinedAt: '2023-07-01T00:00:00.000Z', + }, + ], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.leagueId).toBe('league-1'); + expect(result.isTeamChampionship).toBe(false); + expect(result.currentDriverId).toBeNull(); + expect(result.isAdmin).toBe(false); + expect(result.standings).toHaveLength(2); + expect(result.standings[0].driverId).toBe('driver-1'); + expect(result.standings[0].position).toBe(1); + expect(result.standings[0].totalPoints).toBe(1250); + expect(result.standings[0].racesFinished).toBe(15); + expect(result.standings[0].racesStarted).toBe(15); + expect(result.standings[0].avgFinish).toBeNull(); + expect(result.standings[0].penaltyPoints).toBe(0); + expect(result.standings[0].bonusPoints).toBe(0); + expect(result.standings[0].positionChange).toBe(2); + expect(result.standings[0].lastRacePoints).toBe(25); + expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']); + expect(result.standings[0].wins).toBe(5); + expect(result.standings[0].podiums).toBe(10); + expect(result.standings[1].driverId).toBe('driver-2'); + expect(result.standings[1].position).toBe(2); + expect(result.standings[1].totalPoints).toBe(1100); + expect(result.standings[1].racesFinished).toBe(15); + expect(result.standings[1].racesStarted).toBe(15); + expect(result.standings[1].avgFinish).toBeNull(); + expect(result.standings[1].penaltyPoints).toBe(0); + expect(result.standings[1].bonusPoints).toBe(0); + expect(result.standings[1].positionChange).toBe(-1); + expect(result.standings[1].lastRacePoints).toBe(15); + expect(result.standings[1].droppedRaceIds).toEqual([]); + expect(result.standings[1].wins).toBe(3); + expect(result.standings[1].podiums).toBe(8); + expect(result.drivers).toHaveLength(2); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[0].name).toBe('Alice'); + expect(result.drivers[0].iracingId).toBe('11111'); + expect(result.drivers[0].country).toBe('UK'); + expect(result.drivers[0].avatarUrl).toBeNull(); + expect(result.drivers[1].id).toBe('driver-2'); + expect(result.drivers[1].name).toBe('Bob'); + expect(result.drivers[1].iracingId).toBe('22222'); + expect(result.drivers[1].country).toBe('Germany'); + expect(result.drivers[1].avatarUrl).toBeNull(); + expect(result.memberships).toHaveLength(2); + expect(result.memberships[0].driverId).toBe('driver-1'); + expect(result.memberships[0].leagueId).toBe('league-1'); + expect(result.memberships[0].role).toBe('member'); + expect(result.memberships[0].joinedAt).toBe('2023-06-01T00:00:00.000Z'); + expect(result.memberships[0].status).toBe('active'); + expect(result.memberships[1].driverId).toBe('driver-2'); + expect(result.memberships[1].leagueId).toBe('league-1'); + expect(result.memberships[1].role).toBe('member'); + expect(result.memberships[1].joinedAt).toBe('2023-07-01T00:00:00.000Z'); + expect(result.memberships[1].status).toBe('active'); + }); + + it('should handle empty standings and memberships', () => { + const standingsDto = { + standings: [], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.standings).toHaveLength(0); + expect(result.drivers).toHaveLength(0); + expect(result.memberships).toHaveLength(0); + }); + + it('should handle team championship mode', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: [], + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + true + ); + + expect(result.isTeamChampionship).toBe(true); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: ['race-1'], + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.standings[0].driverId).toBe(standingsDto.standings[0].driverId); + expect(result.standings[0].position).toBe(standingsDto.standings[0].position); + expect(result.standings[0].totalPoints).toBe(standingsDto.standings[0].points); + expect(result.standings[0].racesFinished).toBe(standingsDto.standings[0].races); + expect(result.standings[0].racesStarted).toBe(standingsDto.standings[0].races); + expect(result.standings[0].positionChange).toBe(standingsDto.standings[0].positionChange); + expect(result.standings[0].lastRacePoints).toBe(standingsDto.standings[0].lastRacePoints); + expect(result.standings[0].droppedRaceIds).toEqual(standingsDto.standings[0].droppedRaceIds); + expect(result.standings[0].wins).toBe(standingsDto.standings[0].wins); + expect(result.standings[0].podiums).toBe(standingsDto.standings[0].podiums); + expect(result.drivers[0].id).toBe(standingsDto.standings[0].driver.id); + expect(result.drivers[0].name).toBe(standingsDto.standings[0].driver.name); + expect(result.drivers[0].iracingId).toBe(standingsDto.standings[0].driver.iracingId); + expect(result.drivers[0].country).toBe(standingsDto.standings[0].driver.country); + }); + + it('should not modify the input DTOs', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: ['race-1'], + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const originalStandings = JSON.parse(JSON.stringify(standingsDto)); + const originalMemberships = JSON.parse(JSON.stringify(membershipsDto)); + + LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(standingsDto).toEqual(originalStandings); + expect(membershipsDto).toEqual(originalMemberships); + }); + }); + + describe('edge cases', () => { + it('should handle standings with missing optional fields', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.standings[0].positionChange).toBe(0); + expect(result.standings[0].lastRacePoints).toBe(0); + expect(result.standings[0].droppedRaceIds).toEqual([]); + }); + + it('should handle standings with missing driver field', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: undefined as any, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: [], + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.drivers).toHaveLength(0); + }); + + it('should handle duplicate drivers in standings', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: [], + }, + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1100, + position: 2, + wins: 3, + podiums: 8, + races: 15, + positionChange: -1, + lastRacePoints: 15, + droppedRaceIds: [], + }, + ], + }; + + const membershipsDto = { + members: [], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + // Should only have one driver entry + expect(result.drivers).toHaveLength(1); + expect(result.drivers[0].id).toBe('driver-1'); + }); + + it('should handle members with different roles', () => { + const standingsDto = { + standings: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + }, + points: 1250, + position: 1, + wins: 5, + podiums: 10, + races: 15, + positionChange: 2, + lastRacePoints: 25, + droppedRaceIds: [], + }, + ], + }; + + const membershipsDto = { + members: [ + { + driverId: 'driver-1', + driver: { + id: 'driver-1', + name: 'Alice', + iracingId: '11111', + country: 'UK', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + role: 'admin', + joinedAt: '2023-06-01T00:00:00.000Z', + }, + ], + }; + + const result = LeagueStandingsViewDataBuilder.build( + standingsDto, + membershipsDto, + 'league-1', + false + ); + + expect(result.memberships[0].role).toBe('admin'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.test.ts new file mode 100644 index 000000000..96c418083 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.test.ts @@ -0,0 +1,351 @@ +import { describe, it, expect } from 'vitest'; +import { LeaguesViewDataBuilder } from './LeaguesViewDataBuilder'; +import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO'; + +describe('LeaguesViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform AllLeaguesWithCapacityAndScoringDTO to LeaguesViewData correctly', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Pro League', + description: 'A competitive league for experienced drivers', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Solo • 32 max', + }, + usedSlots: 25, + category: 'competitive', + scoring: { + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-1', + scoringPresetName: 'Standard', + dropPolicySummary: 'Drop 2 worst races', + scoringPatternSummary: 'Points based on finish position', + }, + timingSummary: 'Weekly races on Sundays', + logoUrl: 'https://example.com/logo.png', + pendingJoinRequestsCount: 3, + pendingProtestsCount: 1, + walletBalance: 1000, + }, + { + id: 'league-2', + name: 'Rookie League', + description: null, + ownerId: 'owner-2', + createdAt: '2024-02-01T00:00:00.000Z', + settings: { + maxDrivers: 16, + qualifyingFormat: 'Solo • 16 max', + }, + usedSlots: 10, + category: 'rookie', + scoring: { + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-2', + scoringPresetName: 'Rookie', + dropPolicySummary: 'No drops', + scoringPatternSummary: 'Points based on finish position', + }, + timingSummary: 'Bi-weekly races', + logoUrl: null, + pendingJoinRequestsCount: 0, + pendingProtestsCount: 0, + walletBalance: 0, + }, + ], + totalCount: 2, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues).toHaveLength(2); + expect(result.leagues[0]).toEqual({ + id: 'league-1', + name: 'Pro League', + description: 'A competitive league for experienced drivers', + logoUrl: 'https://example.com/logo.png', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + maxDrivers: 32, + usedDriverSlots: 25, + activeDriversCount: undefined, + nextRaceAt: undefined, + maxTeams: undefined, + usedTeamSlots: undefined, + structureSummary: 'Solo • 32 max', + timingSummary: 'Weekly races on Sundays', + category: 'competitive', + scoring: { + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-1', + scoringPresetName: 'Standard', + dropPolicySummary: 'Drop 2 worst races', + scoringPatternSummary: 'Points based on finish position', + }, + }); + expect(result.leagues[1]).toEqual({ + id: 'league-2', + name: 'Rookie League', + description: null, + logoUrl: null, + ownerId: 'owner-2', + createdAt: '2024-02-01T00:00:00.000Z', + maxDrivers: 16, + usedDriverSlots: 10, + activeDriversCount: undefined, + nextRaceAt: undefined, + maxTeams: undefined, + usedTeamSlots: undefined, + structureSummary: 'Solo • 16 max', + timingSummary: 'Bi-weekly races', + category: 'rookie', + scoring: { + gameId: 'game-1', + gameName: 'iRacing', + primaryChampionshipType: 'Single Championship', + scoringPresetId: 'preset-2', + scoringPresetName: 'Rookie', + dropPolicySummary: 'No drops', + scoringPatternSummary: 'Points based on finish position', + }, + }); + }); + + it('should handle empty leagues list', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [], + totalCount: 0, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues).toHaveLength(0); + }); + + it('should handle leagues with missing optional fields', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Minimal League', + description: '', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 20, + }, + usedSlots: 5, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].description).toBe(null); + expect(result.leagues[0].logoUrl).toBe(null); + expect(result.leagues[0].category).toBe(null); + expect(result.leagues[0].scoring).toBeUndefined(); + expect(result.leagues[0].timingSummary).toBe(''); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Solo • 32 max', + }, + usedSlots: 20, + category: 'test', + scoring: { + gameId: 'game-1', + gameName: 'Test Game', + primaryChampionshipType: 'Test Type', + scoringPresetId: 'preset-1', + scoringPresetName: 'Test Preset', + dropPolicySummary: 'Test drop policy', + scoringPatternSummary: 'Test pattern', + }, + timingSummary: 'Test timing', + logoUrl: 'https://example.com/test.png', + pendingJoinRequestsCount: 5, + pendingProtestsCount: 2, + walletBalance: 500, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].id).toBe(leaguesDTO.leagues[0].id); + expect(result.leagues[0].name).toBe(leaguesDTO.leagues[0].name); + expect(result.leagues[0].description).toBe(leaguesDTO.leagues[0].description); + expect(result.leagues[0].logoUrl).toBe(leaguesDTO.leagues[0].logoUrl); + expect(result.leagues[0].ownerId).toBe(leaguesDTO.leagues[0].ownerId); + expect(result.leagues[0].createdAt).toBe(leaguesDTO.leagues[0].createdAt); + expect(result.leagues[0].maxDrivers).toBe(leaguesDTO.leagues[0].settings.maxDrivers); + expect(result.leagues[0].usedDriverSlots).toBe(leaguesDTO.leagues[0].usedSlots); + expect(result.leagues[0].structureSummary).toBe(leaguesDTO.leagues[0].settings.qualifyingFormat); + expect(result.leagues[0].timingSummary).toBe(leaguesDTO.leagues[0].timingSummary); + expect(result.leagues[0].category).toBe(leaguesDTO.leagues[0].category); + expect(result.leagues[0].scoring).toEqual(leaguesDTO.leagues[0].scoring); + }); + + it('should not modify the input DTO', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Test League', + description: 'Test description', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Solo • 32 max', + }, + usedSlots: 20, + category: 'test', + scoring: { + gameId: 'game-1', + gameName: 'Test Game', + primaryChampionshipType: 'Test Type', + scoringPresetId: 'preset-1', + scoringPresetName: 'Test Preset', + dropPolicySummary: 'Test drop policy', + scoringPatternSummary: 'Test pattern', + }, + timingSummary: 'Test timing', + logoUrl: 'https://example.com/test.png', + pendingJoinRequestsCount: 5, + pendingProtestsCount: 2, + walletBalance: 500, + }, + ], + totalCount: 1, + }; + + const originalDTO = JSON.parse(JSON.stringify(leaguesDTO)); + LeaguesViewDataBuilder.build(leaguesDTO); + + expect(leaguesDTO).toEqual(originalDTO); + }); + }); + + describe('edge cases', () => { + it('should handle leagues with very long descriptions', () => { + const longDescription = 'A'.repeat(1000); + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Test League', + description: longDescription, + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 20, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].description).toBe(longDescription); + }); + + it('should handle leagues with special characters in name', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'League & Co. (2024)', + description: 'Test league', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 20, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].name).toBe('League & Co. (2024)'); + }); + + it('should handle leagues with zero used slots', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Empty League', + description: 'No members yet', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 0, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].usedDriverSlots).toBe(0); + }); + + it('should handle leagues with maximum capacity', () => { + const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { + leagues: [ + { + id: 'league-1', + name: 'Full League', + description: 'At maximum capacity', + ownerId: 'owner-1', + createdAt: '2024-01-01T00:00:00.000Z', + settings: { + maxDrivers: 32, + }, + usedSlots: 32, + }, + ], + totalCount: 1, + }; + + const result = LeaguesViewDataBuilder.build(leaguesDTO); + + expect(result.leagues[0].usedDriverSlots).toBe(32); + expect(result.leagues[0].maxDrivers).toBe(32); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LoginViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LoginViewDataBuilder.test.ts new file mode 100644 index 000000000..500690676 --- /dev/null +++ b/apps/website/lib/builders/view-data/LoginViewDataBuilder.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect } from 'vitest'; +import { LoginViewDataBuilder } from './LoginViewDataBuilder'; +import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO'; + +describe('LoginViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform LoginPageDTO to LoginViewData correctly', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result).toEqual({ + returnTo: '/dashboard', + hasInsufficientPermissions: false, + showPassword: false, + showErrorDetails: false, + formState: { + fields: { + email: { value: '', error: undefined, touched: false, validating: false }, + password: { value: '', error: undefined, touched: false, validating: false }, + rememberMe: { value: false, error: undefined, touched: false, validating: false }, + }, + isValid: true, + isSubmitting: false, + submitError: undefined, + submitCount: 0, + }, + isSubmitting: false, + submitError: undefined, + }); + }); + + it('should handle insufficient permissions flag correctly', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/admin', + hasInsufficientPermissions: true, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.hasInsufficientPermissions).toBe(true); + expect(result.returnTo).toBe('/admin'); + }); + + it('should handle empty returnTo path', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.returnTo).toBe(''); + expect(result.hasInsufficientPermissions).toBe(false); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.returnTo).toBe(loginPageDTO.returnTo); + expect(result.hasInsufficientPermissions).toBe(loginPageDTO.hasInsufficientPermissions); + }); + + it('should not modify the input DTO', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const originalDTO = { ...loginPageDTO }; + LoginViewDataBuilder.build(loginPageDTO); + + expect(loginPageDTO).toEqual(originalDTO); + }); + + it('should initialize form fields with default values', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.formState.fields.email.value).toBe(''); + expect(result.formState.fields.email.error).toBeUndefined(); + expect(result.formState.fields.email.touched).toBe(false); + expect(result.formState.fields.email.validating).toBe(false); + + expect(result.formState.fields.password.value).toBe(''); + expect(result.formState.fields.password.error).toBeUndefined(); + expect(result.formState.fields.password.touched).toBe(false); + expect(result.formState.fields.password.validating).toBe(false); + + expect(result.formState.fields.rememberMe.value).toBe(false); + expect(result.formState.fields.rememberMe.error).toBeUndefined(); + expect(result.formState.fields.rememberMe.touched).toBe(false); + expect(result.formState.fields.rememberMe.validating).toBe(false); + }); + + it('should initialize form state with default values', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.formState.isValid).toBe(true); + expect(result.formState.isSubmitting).toBe(false); + expect(result.formState.submitError).toBeUndefined(); + expect(result.formState.submitCount).toBe(0); + }); + + it('should initialize UI state flags correctly', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.showPassword).toBe(false); + expect(result.showErrorDetails).toBe(false); + expect(result.isSubmitting).toBe(false); + expect(result.submitError).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + it('should handle special characters in returnTo path', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard?param=value&other=test', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.returnTo).toBe('/dashboard?param=value&other=test'); + }); + + it('should handle returnTo with hash fragment', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard#section', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.returnTo).toBe('/dashboard#section'); + }); + + it('should handle returnTo with encoded characters', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard?redirect=%2Fadmin', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + }); + }); + + describe('form state structure', () => { + it('should have all required form fields', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + expect(result.formState.fields).toHaveProperty('email'); + expect(result.formState.fields).toHaveProperty('password'); + expect(result.formState.fields).toHaveProperty('rememberMe'); + }); + + it('should have consistent field state structure', () => { + const loginPageDTO: LoginPageDTO = { + returnTo: '/dashboard', + hasInsufficientPermissions: false, + }; + + const result = LoginViewDataBuilder.build(loginPageDTO); + + const fields = result.formState.fields; + Object.values(fields).forEach((field) => { + expect(field).toHaveProperty('value'); + expect(field).toHaveProperty('error'); + expect(field).toHaveProperty('touched'); + expect(field).toHaveProperty('validating'); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.test.ts new file mode 100644 index 000000000..9abed51f1 --- /dev/null +++ b/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from 'vitest'; +import { OnboardingPageViewDataBuilder } from './OnboardingPageViewDataBuilder'; + +describe('OnboardingPageViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform driver data to ViewData correctly when driver exists', () => { + const apiDto = { id: 'driver-123', name: 'Test Driver' }; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: true, + }); + }); + + it('should handle empty object as driver data', () => { + const apiDto = {}; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: true, + }); + }); + + it('should handle null driver data', () => { + const apiDto = null; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle undefined driver data', () => { + const apiDto = undefined; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: false, + }); + }); + }); + + describe('data transformation', () => { + it('should preserve all driver data fields in the output', () => { + const apiDto = { + id: 'driver-123', + name: 'Test Driver', + email: 'test@example.com', + createdAt: '2024-01-01T00:00:00.000Z', + }; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result.isAlreadyOnboarded).toBe(true); + }); + + it('should not modify the input driver data', () => { + const apiDto = { id: 'driver-123', name: 'Test Driver' }; + const originalDto = { ...apiDto }; + + OnboardingPageViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle empty string as driver data', () => { + const apiDto = ''; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle zero as driver data', () => { + const apiDto = 0; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle false as driver data', () => { + const apiDto = false; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle array as driver data', () => { + const apiDto = ['driver-123']; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: true, + }); + }); + + it('should handle function as driver data', () => { + const apiDto = () => {}; + + const result = OnboardingPageViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + isAlreadyOnboarded: true, + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.test.ts new file mode 100644 index 000000000..2e77a0f13 --- /dev/null +++ b/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect } from 'vitest'; +import { OnboardingViewDataBuilder } from './OnboardingViewDataBuilder'; +import { Result } from '@/lib/contracts/Result'; + +describe('OnboardingViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform successful onboarding check to ViewData correctly', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({ + isAlreadyOnboarded: false, + }); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isOk()).toBe(true); + expect(result.unwrap()).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle already onboarded user correctly', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({ + isAlreadyOnboarded: true, + }); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isOk()).toBe(true); + expect(result.unwrap()).toEqual({ + isAlreadyOnboarded: true, + }); + }); + + it('should handle missing isAlreadyOnboarded field with default false', () => { + const apiDto: Result<{ isAlreadyOnboarded?: boolean }, any> = Result.ok({}); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isOk()).toBe(true); + expect(result.unwrap()).toEqual({ + isAlreadyOnboarded: false, + }); + }); + }); + + describe('error handling', () => { + it('should propagate unauthorized error', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unauthorized'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('unauthorized'); + }); + + it('should propagate notFound error', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('notFound'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('notFound'); + }); + + it('should propagate serverError', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('serverError'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('serverError'); + }); + + it('should propagate networkError', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('networkError'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('networkError'); + }); + + it('should propagate validationError', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('validationError'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('validationError'); + }); + + it('should propagate unknown error', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unknown'); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isErr()).toBe(true); + expect(result.getError()).toBe('unknown'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({ + isAlreadyOnboarded: false, + }); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.unwrap().isAlreadyOnboarded).toBe(false); + }); + + it('should not modify the input DTO', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({ + isAlreadyOnboarded: false, + }); + + const originalDto = { ...apiDto.unwrap() }; + OnboardingViewDataBuilder.build(apiDto); + + expect(apiDto.unwrap()).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle null isAlreadyOnboarded as false', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean | null }, any> = Result.ok({ + isAlreadyOnboarded: null, + }); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isOk()).toBe(true); + expect(result.unwrap()).toEqual({ + isAlreadyOnboarded: false, + }); + }); + + it('should handle undefined isAlreadyOnboarded as false', () => { + const apiDto: Result<{ isAlreadyOnboarded: boolean | undefined }, any> = Result.ok({ + isAlreadyOnboarded: undefined, + }); + + const result = OnboardingViewDataBuilder.build(apiDto); + + expect(result.isOk()).toBe(true); + expect(result.unwrap()).toEqual({ + isAlreadyOnboarded: false, + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/RacesViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/RacesViewDataBuilder.test.ts new file mode 100644 index 000000000..2a14c9451 --- /dev/null +++ b/apps/website/lib/builders/view-data/RacesViewDataBuilder.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from 'vitest'; +import { RacesViewDataBuilder } from './RacesViewDataBuilder'; +import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO'; + +describe('RacesViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform RacesPageDataDTO to RacesViewData correctly', () => { + const now = new Date(); + const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + const apiDto: RacesPageDataDTO = { + races: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: pastDate.toISOString(), + status: 'completed', + leagueId: 'league-1', + leagueName: 'Pro League', + strengthOfField: 1500, + isUpcoming: false, + isLive: false, + isPast: true, + }, + { + id: 'race-2', + track: 'Monza', + car: 'Ferrari 488 GT3', + scheduledAt: futureDate.toISOString(), + status: 'scheduled', + leagueId: 'league-1', + leagueName: 'Pro League', + strengthOfField: 1600, + isUpcoming: true, + isLive: false, + isPast: false, + }, + ], + }; + + const result = RacesViewDataBuilder.build(apiDto); + + expect(result.races).toHaveLength(2); + expect(result.totalCount).toBe(2); + expect(result.completedCount).toBe(1); + expect(result.scheduledCount).toBe(1); + expect(result.leagues).toHaveLength(1); + expect(result.leagues[0]).toEqual({ id: 'league-1', name: 'Pro League' }); + expect(result.upcomingRaces).toHaveLength(1); + expect(result.upcomingRaces[0].id).toBe('race-2'); + expect(result.recentResults).toHaveLength(1); + expect(result.recentResults[0].id).toBe('race-1'); + expect(result.racesByDate).toHaveLength(2); + }); + + it('should handle empty races list', () => { + const apiDto: RacesPageDataDTO = { + races: [], + }; + + const result = RacesViewDataBuilder.build(apiDto); + + expect(result.races).toHaveLength(0); + expect(result.totalCount).toBe(0); + expect(result.leagues).toHaveLength(0); + expect(result.racesByDate).toHaveLength(0); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const now = new Date(); + const apiDto: RacesPageDataDTO = { + races: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: now.toISOString(), + status: 'scheduled', + leagueId: 'league-1', + leagueName: 'Pro League', + strengthOfField: 1500, + isUpcoming: true, + isLive: false, + isPast: false, + }, + ], + }; + + const result = RacesViewDataBuilder.build(apiDto); + + expect(result.races[0].id).toBe(apiDto.races[0].id); + expect(result.races[0].track).toBe(apiDto.races[0].track); + expect(result.races[0].car).toBe(apiDto.races[0].car); + expect(result.races[0].scheduledAt).toBe(apiDto.races[0].scheduledAt); + expect(result.races[0].status).toBe(apiDto.races[0].status); + expect(result.races[0].leagueId).toBe(apiDto.races[0].leagueId); + expect(result.races[0].leagueName).toBe(apiDto.races[0].leagueName); + expect(result.races[0].strengthOfField).toBe(apiDto.races[0].strengthOfField); + }); + + it('should not modify the input DTO', () => { + const now = new Date(); + const apiDto: RacesPageDataDTO = { + races: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: now.toISOString(), + status: 'scheduled', + isUpcoming: true, + isLive: false, + isPast: false, + }, + ], + }; + + const originalDto = JSON.parse(JSON.stringify(apiDto)); + RacesViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle races with missing optional fields', () => { + const now = new Date(); + const apiDto: RacesPageDataDTO = { + races: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: now.toISOString(), + status: 'scheduled', + isUpcoming: true, + isLive: false, + isPast: false, + }, + ], + }; + + const result = RacesViewDataBuilder.build(apiDto); + + expect(result.races[0].leagueId).toBeUndefined(); + expect(result.races[0].leagueName).toBeUndefined(); + expect(result.races[0].strengthOfField).toBeNull(); + }); + + it('should handle multiple races on the same date', () => { + const date = '2024-01-15T14:00:00.000Z'; + const apiDto: RacesPageDataDTO = { + races: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche', + scheduledAt: date, + status: 'scheduled', + isUpcoming: true, + isLive: false, + isPast: false, + }, + { + id: 'race-2', + track: 'Monza', + car: 'Ferrari', + scheduledAt: date, + status: 'scheduled', + isUpcoming: true, + isLive: false, + isPast: false, + }, + ], + }; + + const result = RacesViewDataBuilder.build(apiDto); + + expect(result.racesByDate).toHaveLength(1); + expect(result.racesByDate[0].races).toHaveLength(2); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.test.ts new file mode 100644 index 000000000..335cbc91c --- /dev/null +++ b/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect } from 'vitest'; +import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder'; +import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO'; + +describe('ResetPasswordViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform ResetPasswordPageDTO to ResetPasswordViewData correctly', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result).toEqual({ + token: 'abc123def456', + returnTo: '/login', + showSuccess: false, + formState: { + fields: { + newPassword: { value: '', error: undefined, touched: false, validating: false }, + confirmPassword: { value: '', error: undefined, touched: false, validating: false }, + }, + isValid: true, + isSubmitting: false, + submitError: undefined, + submitCount: 0, + }, + isSubmitting: false, + submitError: undefined, + }); + }); + + it('should handle empty returnTo path', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.returnTo).toBe(''); + }); + + it('should handle returnTo with query parameters', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login?success=true', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.returnTo).toBe('/login?success=true'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.token).toBe(resetPasswordPageDTO.token); + expect(result.returnTo).toBe(resetPasswordPageDTO.returnTo); + }); + + it('should not modify the input DTO', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const originalDTO = { ...resetPasswordPageDTO }; + ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(resetPasswordPageDTO).toEqual(originalDTO); + }); + + it('should initialize form fields with default values', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.formState.fields.newPassword.value).toBe(''); + expect(result.formState.fields.newPassword.error).toBeUndefined(); + expect(result.formState.fields.newPassword.touched).toBe(false); + expect(result.formState.fields.newPassword.validating).toBe(false); + + expect(result.formState.fields.confirmPassword.value).toBe(''); + expect(result.formState.fields.confirmPassword.error).toBeUndefined(); + expect(result.formState.fields.confirmPassword.touched).toBe(false); + expect(result.formState.fields.confirmPassword.validating).toBe(false); + }); + + it('should initialize form state with default values', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.formState.isValid).toBe(true); + expect(result.formState.isSubmitting).toBe(false); + expect(result.formState.submitError).toBeUndefined(); + expect(result.formState.submitCount).toBe(0); + }); + + it('should initialize UI state flags correctly', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.showSuccess).toBe(false); + expect(result.isSubmitting).toBe(false); + expect(result.submitError).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + it('should handle token with special characters', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc-123_def.456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.token).toBe('abc-123_def.456'); + }); + + it('should handle token with URL-encoded characters', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc%20123%40def', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.token).toBe('abc%20123%40def'); + }); + + it('should handle returnTo with encoded characters', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login?redirect=%2Fdashboard', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.returnTo).toBe('/login?redirect=%2Fdashboard'); + }); + + it('should handle returnTo with hash fragment', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login#section', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.returnTo).toBe('/login#section'); + }); + }); + + describe('form state structure', () => { + it('should have all required form fields', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + expect(result.formState.fields).toHaveProperty('newPassword'); + expect(result.formState.fields).toHaveProperty('confirmPassword'); + }); + + it('should have consistent field state structure', () => { + const resetPasswordPageDTO: ResetPasswordPageDTO = { + token: 'abc123def456', + returnTo: '/login', + }; + + const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); + + const fields = result.formState.fields; + Object.values(fields).forEach((field) => { + expect(field).toHaveProperty('value'); + expect(field).toHaveProperty('error'); + expect(field).toHaveProperty('touched'); + expect(field).toHaveProperty('validating'); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/SignupViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/SignupViewDataBuilder.test.ts new file mode 100644 index 000000000..3caf3b8e8 --- /dev/null +++ b/apps/website/lib/builders/view-data/SignupViewDataBuilder.test.ts @@ -0,0 +1,188 @@ +import { describe, it, expect } from 'vitest'; +import { SignupViewDataBuilder } from './SignupViewDataBuilder'; +import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO'; + +describe('SignupViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform SignupPageDTO to SignupViewData correctly', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result).toEqual({ + returnTo: '/dashboard', + formState: { + fields: { + firstName: { value: '', error: undefined, touched: false, validating: false }, + lastName: { value: '', error: undefined, touched: false, validating: false }, + email: { value: '', error: undefined, touched: false, validating: false }, + password: { value: '', error: undefined, touched: false, validating: false }, + confirmPassword: { value: '', error: undefined, touched: false, validating: false }, + }, + isValid: true, + isSubmitting: false, + submitError: undefined, + submitCount: 0, + }, + isSubmitting: false, + submitError: undefined, + }); + }); + + it('should handle empty returnTo path', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.returnTo).toBe(''); + }); + + it('should handle returnTo with query parameters', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard?welcome=true', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.returnTo).toBe('/dashboard?welcome=true'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.returnTo).toBe(signupPageDTO.returnTo); + }); + + it('should not modify the input DTO', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const originalDTO = { ...signupPageDTO }; + SignupViewDataBuilder.build(signupPageDTO); + + expect(signupPageDTO).toEqual(originalDTO); + }); + + it('should initialize all signup form fields with default values', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.formState.fields.firstName.value).toBe(''); + expect(result.formState.fields.firstName.error).toBeUndefined(); + expect(result.formState.fields.firstName.touched).toBe(false); + expect(result.formState.fields.firstName.validating).toBe(false); + + expect(result.formState.fields.lastName.value).toBe(''); + expect(result.formState.fields.lastName.error).toBeUndefined(); + expect(result.formState.fields.lastName.touched).toBe(false); + expect(result.formState.fields.lastName.validating).toBe(false); + + expect(result.formState.fields.email.value).toBe(''); + expect(result.formState.fields.email.error).toBeUndefined(); + expect(result.formState.fields.email.touched).toBe(false); + expect(result.formState.fields.email.validating).toBe(false); + + expect(result.formState.fields.password.value).toBe(''); + expect(result.formState.fields.password.error).toBeUndefined(); + expect(result.formState.fields.password.touched).toBe(false); + expect(result.formState.fields.password.validating).toBe(false); + + expect(result.formState.fields.confirmPassword.value).toBe(''); + expect(result.formState.fields.confirmPassword.error).toBeUndefined(); + expect(result.formState.fields.confirmPassword.touched).toBe(false); + expect(result.formState.fields.confirmPassword.validating).toBe(false); + }); + + it('should initialize form state with default values', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.formState.isValid).toBe(true); + expect(result.formState.isSubmitting).toBe(false); + expect(result.formState.submitError).toBeUndefined(); + expect(result.formState.submitCount).toBe(0); + }); + + it('should initialize UI state flags correctly', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.isSubmitting).toBe(false); + expect(result.submitError).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + it('should handle returnTo with encoded characters', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard?redirect=%2Fadmin', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin'); + }); + + it('should handle returnTo with hash fragment', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard#section', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.returnTo).toBe('/dashboard#section'); + }); + }); + + describe('form state structure', () => { + it('should have all required form fields', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + expect(result.formState.fields).toHaveProperty('firstName'); + expect(result.formState.fields).toHaveProperty('lastName'); + expect(result.formState.fields).toHaveProperty('email'); + expect(result.formState.fields).toHaveProperty('password'); + expect(result.formState.fields).toHaveProperty('confirmPassword'); + }); + + it('should have consistent field state structure', () => { + const signupPageDTO: SignupPageDTO = { + returnTo: '/dashboard', + }; + + const result = SignupViewDataBuilder.build(signupPageDTO); + + const fields = result.formState.fields; + Object.values(fields).forEach((field) => { + expect(field).toHaveProperty('value'); + expect(field).toHaveProperty('error'); + expect(field).toHaveProperty('touched'); + expect(field).toHaveProperty('validating'); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.test.ts new file mode 100644 index 000000000..98e883952 --- /dev/null +++ b/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { SponsorDashboardViewDataBuilder } from './SponsorDashboardViewDataBuilder'; +import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO'; + +describe('SponsorDashboardViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform SponsorDashboardDTO to SponsorDashboardViewData correctly', () => { + const apiDto: SponsorDashboardDTO = { + sponsorName: 'Test Sponsor', + metrics: { + impressions: 5000, + viewers: 1000, + exposure: 500, + }, + investment: { + activeSponsorships: 5, + totalSpent: 5000, + }, + sponsorships: [], + }; + + const result = SponsorDashboardViewDataBuilder.build(apiDto); + + expect(result.sponsorName).toBe('Test Sponsor'); + expect(result.totalImpressions).toBe('5,000'); + expect(result.totalInvestment).toBe('$5,000.00'); + expect(result.activeSponsorships).toBe(5); + expect(result.metrics.impressionsChange).toBe(15); + }); + + it('should handle low impressions correctly', () => { + const apiDto: SponsorDashboardDTO = { + sponsorName: 'Test Sponsor', + metrics: { + impressions: 500, + viewers: 100, + exposure: 50, + }, + investment: { + activeSponsorships: 1, + totalSpent: 1000, + }, + sponsorships: [], + }; + + const result = SponsorDashboardViewDataBuilder.build(apiDto); + + expect(result.metrics.impressionsChange).toBe(-5); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto: SponsorDashboardDTO = { + sponsorName: 'Test Sponsor', + metrics: { + impressions: 5000, + viewers: 1000, + exposure: 500, + }, + investment: { + activeSponsorships: 5, + totalSpent: 5000, + }, + sponsorships: [], + }; + + const result = SponsorDashboardViewDataBuilder.build(apiDto); + + expect(result.sponsorName).toBe(apiDto.sponsorName); + expect(result.activeSponsorships).toBe(apiDto.investment.activeSponsorships); + }); + + it('should not modify the input DTO', () => { + const apiDto: SponsorDashboardDTO = { + sponsorName: 'Test Sponsor', + metrics: { + impressions: 5000, + viewers: 1000, + exposure: 500, + }, + investment: { + activeSponsorships: 5, + totalSpent: 5000, + }, + sponsorships: [], + }; + + const originalDto = JSON.parse(JSON.stringify(apiDto)); + SponsorDashboardViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.test.ts new file mode 100644 index 000000000..0af1cb235 --- /dev/null +++ b/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect } from 'vitest'; +import { SponsorLogoViewDataBuilder } from './SponsorLogoViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('SponsorLogoViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to SponsorLogoViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle JPEG sponsor logos', () => { + const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle SVG sponsor logos', () => { + const buffer = new TextEncoder().encode('Sponsor'); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/svg+xml', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/svg+xml'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + SponsorLogoViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle large sponsor logos', () => { + const buffer = new Uint8Array(3 * 1024 * 1024); // 3MB + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle buffer with all zeros', () => { + const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with all ones', () => { + const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle different content types', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const contentTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/svg+xml', + 'image/bmp', + 'image/tiff', + ]; + + contentTypes.forEach((contentType) => { + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType, + }; + + const result = SponsorLogoViewDataBuilder.build(mediaDto); + + expect(result.contentType).toBe(contentType); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.test.ts new file mode 100644 index 000000000..d355acef7 --- /dev/null +++ b/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from 'vitest'; +import { TeamLogoViewDataBuilder } from './TeamLogoViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('TeamLogoViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to TeamLogoViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle JPEG team logos', () => { + const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle SVG team logos', () => { + const buffer = new TextEncoder().encode(''); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/svg+xml', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/svg+xml'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + TeamLogoViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle small logo files', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with special characters', () => { + const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle different content types', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const contentTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/svg+xml', + 'image/bmp', + 'image/tiff', + ]; + + contentTypes.forEach((contentType) => { + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType, + }; + + const result = TeamLogoViewDataBuilder.build(mediaDto); + + expect(result.contentType).toBe(contentType); + }); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.test.ts new file mode 100644 index 000000000..fee5d68a9 --- /dev/null +++ b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.test.ts @@ -0,0 +1,430 @@ +import { describe, it, expect } from 'vitest'; +import { TeamRankingsViewDataBuilder } from './TeamRankingsViewDataBuilder'; +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; + +describe('TeamRankingsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform GetTeamsLeaderboardOutputDTO to TeamRankingsViewData correctly', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo1.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + { + id: 'team-2', + name: 'Speed Demons', + tag: 'SD', + logoUrl: 'https://example.com/logo2.jpg', + memberCount: 8, + rating: 1200, + totalWins: 20, + totalRaces: 150, + performanceLevel: 'advanced', + isRecruiting: true, + createdAt: '2023-06-01', + }, + { + id: 'team-3', + name: 'Rookie Racers', + tag: 'RR', + logoUrl: 'https://example.com/logo3.jpg', + memberCount: 5, + rating: 800, + totalWins: 5, + totalRaces: 50, + performanceLevel: 'intermediate', + isRecruiting: false, + createdAt: '2023-09-01', + }, + ], + recruitingCount: 5, + groupsBySkillLevel: 'elite,advanced,intermediate', + topTeams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo1.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + { + id: 'team-2', + name: 'Speed Demons', + tag: 'SD', + logoUrl: 'https://example.com/logo2.jpg', + memberCount: 8, + rating: 1200, + totalWins: 20, + totalRaces: 150, + performanceLevel: 'advanced', + isRecruiting: true, + createdAt: '2023-06-01', + }, + ], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + // Verify teams + expect(result.teams).toHaveLength(3); + expect(result.teams[0].id).toBe('team-1'); + expect(result.teams[0].name).toBe('Racing Team Alpha'); + expect(result.teams[0].tag).toBe('RTA'); + expect(result.teams[0].memberCount).toBe(15); + expect(result.teams[0].totalWins).toBe(50); + expect(result.teams[0].totalRaces).toBe(200); + expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg'); + expect(result.teams[0].position).toBe(1); + expect(result.teams[0].isRecruiting).toBe(false); + expect(result.teams[0].performanceLevel).toBe('elite'); + expect(result.teams[0].rating).toBe(1500); + expect(result.teams[0].category).toBeUndefined(); + + // Verify podium (top 3) + expect(result.podium).toHaveLength(3); + expect(result.podium[0].id).toBe('team-1'); + expect(result.podium[0].position).toBe(1); + expect(result.podium[1].id).toBe('team-2'); + expect(result.podium[1].position).toBe(2); + expect(result.podium[2].id).toBe('team-3'); + expect(result.podium[2].position).toBe(3); + + // Verify recruiting count + expect(result.recruitingCount).toBe(5); + }); + + it('should handle empty team array', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams).toEqual([]); + expect(result.podium).toEqual([]); + expect(result.recruitingCount).toBe(0); + }); + + it('should handle less than 3 teams for podium', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo1.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + { + id: 'team-2', + name: 'Speed Demons', + tag: 'SD', + logoUrl: 'https://example.com/logo2.jpg', + memberCount: 8, + rating: 1200, + totalWins: 20, + totalRaces: 150, + performanceLevel: 'advanced', + isRecruiting: true, + createdAt: '2023-06-01', + }, + ], + recruitingCount: 2, + groupsBySkillLevel: 'elite,advanced', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams).toHaveLength(2); + expect(result.podium).toHaveLength(2); + expect(result.podium[0].position).toBe(1); + expect(result.podium[1].position).toBe(2); + }); + + it('should handle missing avatar URLs with empty string fallback', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].logoUrl).toBe(''); + }); + + it('should calculate position based on index', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, + { id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' }, + { id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' }, + { id: 'team-4', name: 'Team 4', tag: 'T4', memberCount: 4, totalWins: 5, totalRaces: 40, performanceLevel: 'beginner', isRecruiting: true, createdAt: '2023-04-01' }, + ], + recruitingCount: 2, + groupsBySkillLevel: 'elite,advanced,intermediate,beginner', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].position).toBe(1); + expect(result.teams[1].position).toBe(2); + expect(result.teams[2].position).toBe(3); + expect(result.teams[3].position).toBe(4); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-123', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 5, + groupsBySkillLevel: 'elite,advanced', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].name).toBe(teamDTO.teams[0].name); + expect(result.teams[0].tag).toBe(teamDTO.teams[0].tag); + expect(result.teams[0].logoUrl).toBe(teamDTO.teams[0].logoUrl); + expect(result.teams[0].memberCount).toBe(teamDTO.teams[0].memberCount); + expect(result.teams[0].rating).toBe(teamDTO.teams[0].rating); + expect(result.teams[0].totalWins).toBe(teamDTO.teams[0].totalWins); + expect(result.teams[0].totalRaces).toBe(teamDTO.teams[0].totalRaces); + expect(result.teams[0].performanceLevel).toBe(teamDTO.teams[0].performanceLevel); + expect(result.teams[0].isRecruiting).toBe(teamDTO.teams[0].isRecruiting); + }); + + it('should not modify the input DTO', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-123', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 15, + rating: 1500, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 5, + groupsBySkillLevel: 'elite,advanced', + topTeams: [], + }; + + const originalDTO = JSON.parse(JSON.stringify(teamDTO)); + TeamRankingsViewDataBuilder.build(teamDTO); + + expect(teamDTO).toEqual(originalDTO); + }); + + it('should handle large numbers correctly', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: 'https://example.com/logo.jpg', + memberCount: 100, + rating: 999999, + totalWins: 5000, + totalRaces: 10000, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].rating).toBe(999999); + expect(result.teams[0].totalWins).toBe(5000); + expect(result.teams[0].totalRaces).toBe(10000); + }); + }); + + describe('edge cases', () => { + it('should handle null/undefined logo URLs', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + logoUrl: null as any, + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].logoUrl).toBe(''); + }); + + it('should handle null/undefined rating', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + rating: null as any, + totalWins: 50, + totalRaces: 200, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].rating).toBe(0); + }); + + it('should handle null/undefined totalWins and totalRaces', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: null as any, + totalRaces: null as any, + performanceLevel: 'elite', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].totalWins).toBe(0); + expect(result.teams[0].totalRaces).toBe(0); + }); + + it('should handle empty performance level', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + tag: 'RTA', + memberCount: 15, + totalWins: 50, + totalRaces: 200, + performanceLevel: '', + isRecruiting: false, + createdAt: '2023-01-01', + }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].performanceLevel).toBe('N/A'); + }); + + it('should handle position 0', () => { + const teamDTO: GetTeamsLeaderboardOutputDTO = { + teams: [ + { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, + ], + recruitingCount: 0, + groupsBySkillLevel: '', + topTeams: [], + }; + + const result = TeamRankingsViewDataBuilder.build(teamDTO); + + expect(result.teams[0].position).toBe(1); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/TeamsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.test.ts new file mode 100644 index 000000000..fd2457883 --- /dev/null +++ b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect } from 'vitest'; +import { TeamsViewDataBuilder } from './TeamsViewDataBuilder'; + +describe('TeamsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform TeamsPageDto to TeamsViewData correctly', () => { + const apiDto = { + teams: [ + { + id: 'team-1', + name: 'Racing Team Alpha', + memberCount: 15, + logoUrl: 'https://example.com/logo1.jpg', + rating: 1500, + totalWins: 50, + totalRaces: 200, + region: 'USA', + isRecruiting: false, + category: 'competitive', + performanceLevel: 'elite', + description: 'A top-tier racing team', + }, + { + id: 'team-2', + name: 'Speed Demons', + memberCount: 8, + logoUrl: 'https://example.com/logo2.jpg', + rating: 1200, + totalWins: 20, + totalRaces: 150, + region: 'UK', + isRecruiting: true, + category: 'casual', + performanceLevel: 'advanced', + description: 'Fast and fun', + }, + ], + }; + + const result = TeamsViewDataBuilder.build(apiDto as any); + + expect(result.teams).toHaveLength(2); + expect(result.teams[0]).toEqual({ + teamId: 'team-1', + teamName: 'Racing Team Alpha', + memberCount: 15, + logoUrl: 'https://example.com/logo1.jpg', + ratingLabel: '1,500', + ratingValue: 1500, + winsLabel: '50', + racesLabel: '200', + region: 'USA', + isRecruiting: false, + category: 'competitive', + performanceLevel: 'elite', + description: 'A top-tier racing team', + countryCode: 'USA', + }); + expect(result.teams[1]).toEqual({ + teamId: 'team-2', + teamName: 'Speed Demons', + memberCount: 8, + logoUrl: 'https://example.com/logo2.jpg', + ratingLabel: '1,200', + ratingValue: 1200, + winsLabel: '20', + racesLabel: '150', + region: 'UK', + isRecruiting: true, + category: 'casual', + performanceLevel: 'advanced', + description: 'Fast and fun', + countryCode: 'UK', + }); + }); + + it('should handle empty teams list', () => { + const apiDto = { + teams: [], + }; + + const result = TeamsViewDataBuilder.build(apiDto as any); + + expect(result.teams).toHaveLength(0); + }); + + it('should handle teams with missing optional fields', () => { + const apiDto = { + teams: [ + { + id: 'team-1', + name: 'Minimal Team', + memberCount: 5, + }, + ], + }; + + const result = TeamsViewDataBuilder.build(apiDto as any); + + expect(result.teams[0].ratingValue).toBe(0); + expect(result.teams[0].winsLabel).toBe('0'); + expect(result.teams[0].racesLabel).toBe('0'); + expect(result.teams[0].logoUrl).toBeUndefined(); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto = { + teams: [ + { + id: 'team-1', + name: 'Test Team', + memberCount: 10, + rating: 1000, + totalWins: 5, + totalRaces: 20, + region: 'EU', + isRecruiting: true, + category: 'test', + performanceLevel: 'test-level', + description: 'test-desc', + }, + ], + }; + + const result = TeamsViewDataBuilder.build(apiDto as any); + + expect(result.teams[0].teamId).toBe(apiDto.teams[0].id); + expect(result.teams[0].teamName).toBe(apiDto.teams[0].name); + expect(result.teams[0].memberCount).toBe(apiDto.teams[0].memberCount); + expect(result.teams[0].ratingValue).toBe(apiDto.teams[0].rating); + expect(result.teams[0].region).toBe(apiDto.teams[0].region); + expect(result.teams[0].isRecruiting).toBe(apiDto.teams[0].isRecruiting); + expect(result.teams[0].category).toBe(apiDto.teams[0].category); + expect(result.teams[0].performanceLevel).toBe(apiDto.teams[0].performanceLevel); + expect(result.teams[0].description).toBe(apiDto.teams[0].description); + }); + + it('should not modify the input DTO', () => { + const apiDto = { + teams: [ + { + id: 'team-1', + name: 'Test Team', + memberCount: 10, + }, + ], + }; + + const originalDto = JSON.parse(JSON.stringify(apiDto)); + TeamsViewDataBuilder.build(apiDto as any); + + expect(apiDto).toEqual(originalDto); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.test.ts new file mode 100644 index 000000000..24b75a678 --- /dev/null +++ b/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect } from 'vitest'; +import { TrackImageViewDataBuilder } from './TrackImageViewDataBuilder'; +import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; + +describe('TrackImageViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform MediaBinaryDTO to TrackImageViewData correctly', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle JPEG track images', () => { + const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle WebP track images', () => { + const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/webp', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/webp'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBeDefined(); + expect(result.contentType).toBe(mediaDto.contentType); + }); + + it('should not modify the input DTO', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const originalDto = { ...mediaDto }; + TrackImageViewDataBuilder.build(mediaDto); + + expect(mediaDto).toEqual(originalDto); + }); + + it('should convert buffer to base64 string', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(typeof result.buffer).toBe('string'); + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + }); + }); + + describe('edge cases', () => { + it('should handle empty buffer', () => { + const buffer = new Uint8Array([]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(''); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle large track images', () => { + const buffer = new Uint8Array(5 * 1024 * 1024); // 5MB + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/jpeg', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/jpeg'); + }); + + it('should handle buffer with all zeros', () => { + const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle buffer with all ones', () => { + const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType: 'image/png', + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); + expect(result.contentType).toBe('image/png'); + }); + + it('should handle different content types', () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const contentTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/svg+xml', + 'image/bmp', + 'image/tiff', + ]; + + contentTypes.forEach((contentType) => { + const mediaDto: MediaBinaryDTO = { + buffer: buffer.buffer, + contentType, + }; + + const result = TrackImageViewDataBuilder.build(mediaDto); + + expect(result.contentType).toBe(contentType); + }); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardConsistencyDisplay.test.ts b/apps/website/lib/display-objects/DashboardConsistencyDisplay.test.ts new file mode 100644 index 000000000..a78b7133c --- /dev/null +++ b/apps/website/lib/display-objects/DashboardConsistencyDisplay.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay'; + +describe('DashboardConsistencyDisplay', () => { + describe('happy paths', () => { + it('should format consistency correctly', () => { + expect(DashboardConsistencyDisplay.format(0)).toBe('0%'); + expect(DashboardConsistencyDisplay.format(50)).toBe('50%'); + expect(DashboardConsistencyDisplay.format(100)).toBe('100%'); + }); + }); + + describe('edge cases', () => { + it('should handle decimal consistency', () => { + expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%'); + expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%'); + }); + + it('should handle negative consistency', () => { + expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%'); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardCountDisplay.test.ts b/apps/website/lib/display-objects/DashboardCountDisplay.test.ts new file mode 100644 index 000000000..f6fcbb047 --- /dev/null +++ b/apps/website/lib/display-objects/DashboardCountDisplay.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardCountDisplay } from './DashboardCountDisplay'; + +describe('DashboardCountDisplay', () => { + describe('happy paths', () => { + it('should format positive numbers correctly', () => { + expect(DashboardCountDisplay.format(0)).toBe('0'); + expect(DashboardCountDisplay.format(1)).toBe('1'); + expect(DashboardCountDisplay.format(100)).toBe('100'); + expect(DashboardCountDisplay.format(1000)).toBe('1000'); + }); + + it('should handle null values', () => { + expect(DashboardCountDisplay.format(null)).toBe('0'); + }); + + it('should handle undefined values', () => { + expect(DashboardCountDisplay.format(undefined)).toBe('0'); + }); + }); + + describe('edge cases', () => { + it('should handle negative numbers', () => { + expect(DashboardCountDisplay.format(-1)).toBe('-1'); + expect(DashboardCountDisplay.format(-100)).toBe('-100'); + }); + + it('should handle large numbers', () => { + expect(DashboardCountDisplay.format(999999)).toBe('999999'); + expect(DashboardCountDisplay.format(1000000)).toBe('1000000'); + }); + + it('should handle decimal numbers', () => { + expect(DashboardCountDisplay.format(1.5)).toBe('1.5'); + expect(DashboardCountDisplay.format(100.99)).toBe('100.99'); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardDateDisplay.test.ts b/apps/website/lib/display-objects/DashboardDateDisplay.test.ts new file mode 100644 index 000000000..635e68710 --- /dev/null +++ b/apps/website/lib/display-objects/DashboardDateDisplay.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardDateDisplay } from './DashboardDateDisplay'; + +describe('DashboardDateDisplay', () => { + describe('happy paths', () => { + it('should format future date correctly', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now + + const result = DashboardDateDisplay.format(futureDate); + + expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/); + expect(result.time).toMatch(/^\d{2}:\d{2}$/); + expect(result.relative).toBe('1d'); + }); + + it('should format date less than 24 hours correctly', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now + + const result = DashboardDateDisplay.format(futureDate); + + expect(result.relative).toBe('6h'); + }); + + it('should format date more than 24 hours correctly', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now + + const result = DashboardDateDisplay.format(futureDate); + + expect(result.relative).toBe('2d'); + }); + + it('should format past date correctly', () => { + const now = new Date(); + const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago + + const result = DashboardDateDisplay.format(pastDate); + + expect(result.relative).toBe('Past'); + }); + + it('should format current date correctly', () => { + const now = new Date(); + + const result = DashboardDateDisplay.format(now); + + expect(result.relative).toBe('Now'); + }); + + it('should format date with leading zeros in time', () => { + const date = new Date('2024-01-15T05:03:00'); + + const result = DashboardDateDisplay.format(date); + + expect(result.time).toBe('05:03'); + }); + }); + + describe('edge cases', () => { + it('should handle midnight correctly', () => { + const date = new Date('2024-01-15T00:00:00'); + + const result = DashboardDateDisplay.format(date); + + expect(result.time).toBe('00:00'); + }); + + it('should handle end of day correctly', () => { + const date = new Date('2024-01-15T23:59:59'); + + const result = DashboardDateDisplay.format(date); + + expect(result.time).toBe('23:59'); + }); + + it('should handle different days of week', () => { + const date = new Date('2024-01-15'); // Monday + + const result = DashboardDateDisplay.format(date); + + expect(result.date).toContain('Mon'); + }); + + it('should handle different months', () => { + const date = new Date('2024-01-15'); + + const result = DashboardDateDisplay.format(date); + + expect(result.date).toContain('Jan'); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardLeaguePositionDisplay.test.ts b/apps/website/lib/display-objects/DashboardLeaguePositionDisplay.test.ts new file mode 100644 index 000000000..8011d12cf --- /dev/null +++ b/apps/website/lib/display-objects/DashboardLeaguePositionDisplay.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay'; + +describe('DashboardLeaguePositionDisplay', () => { + describe('happy paths', () => { + it('should format position correctly', () => { + expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1'); + expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5'); + expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100'); + }); + + it('should handle null values', () => { + expect(DashboardLeaguePositionDisplay.format(null)).toBe('-'); + }); + + it('should handle undefined values', () => { + expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-'); + }); + }); + + describe('edge cases', () => { + it('should handle position 0', () => { + expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0'); + }); + + it('should handle large positions', () => { + expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999'); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardRankDisplay.test.ts b/apps/website/lib/display-objects/DashboardRankDisplay.test.ts new file mode 100644 index 000000000..c048d8a7f --- /dev/null +++ b/apps/website/lib/display-objects/DashboardRankDisplay.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardRankDisplay } from './DashboardRankDisplay'; + +describe('DashboardRankDisplay', () => { + describe('happy paths', () => { + it('should format rank correctly', () => { + expect(DashboardRankDisplay.format(1)).toBe('1'); + expect(DashboardRankDisplay.format(42)).toBe('42'); + expect(DashboardRankDisplay.format(100)).toBe('100'); + }); + }); + + describe('edge cases', () => { + it('should handle rank 0', () => { + expect(DashboardRankDisplay.format(0)).toBe('0'); + }); + + it('should handle large ranks', () => { + expect(DashboardRankDisplay.format(999999)).toBe('999999'); + }); + }); +}); diff --git a/apps/website/lib/display-objects/DashboardViewDataConsistency.test.ts b/apps/website/lib/display-objects/DashboardViewDataConsistency.test.ts new file mode 100644 index 000000000..171cd2675 --- /dev/null +++ b/apps/website/lib/display-objects/DashboardViewDataConsistency.test.ts @@ -0,0 +1,369 @@ +import { describe, it, expect } from 'vitest'; +import { DashboardViewDataBuilder } from '../builders/view-data/DashboardViewDataBuilder'; +import { DashboardDateDisplay } from './DashboardDateDisplay'; +import { DashboardCountDisplay } from './DashboardCountDisplay'; +import { DashboardRankDisplay } from './DashboardRankDisplay'; +import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay'; +import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay'; +import { RatingDisplay } from './RatingDisplay'; +import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; + +describe('Dashboard View Data - Cross-Component Consistency', () => { + describe('common patterns', () => { + it('should all use consistent formatting for numeric values', () => { + const dashboardDTO: DashboardOverviewDTO = { + currentDriver: { + id: 'driver-123', + name: 'John Doe', + country: 'USA', + rating: 1234.56, + globalRank: 42, + totalRaces: 150, + wins: 25, + podiums: 60, + consistency: 85, + }, + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [], + activeLeaguesCount: 3, + nextRace: null, + recentResults: [], + leagueStandingsSummaries: [ + { + leagueId: 'league-1', + leagueName: 'Test League', + position: 5, + totalDrivers: 50, + points: 1250, + }, + ], + feedSummary: { + notificationCount: 0, + items: [], + }, + friends: [ + { id: 'friend-1', name: 'Alice', country: 'UK' }, + { id: 'friend-2', name: 'Bob', country: 'Germany' }, + ], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + // All numeric values should be formatted as strings + expect(typeof result.currentDriver.rating).toBe('string'); + expect(typeof result.currentDriver.rank).toBe('string'); + expect(typeof result.currentDriver.totalRaces).toBe('string'); + expect(typeof result.currentDriver.wins).toBe('string'); + expect(typeof result.currentDriver.podiums).toBe('string'); + expect(typeof result.currentDriver.consistency).toBe('string'); + expect(typeof result.activeLeaguesCount).toBe('string'); + expect(typeof result.friendCount).toBe('string'); + expect(typeof result.leagueStandings[0].position).toBe('string'); + expect(typeof result.leagueStandings[0].points).toBe('string'); + expect(typeof result.leagueStandings[0].totalDrivers).toBe('string'); + }); + + it('should all handle missing data gracefully', () => { + const dashboardDTO: DashboardOverviewDTO = { + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [], + activeLeaguesCount: 0, + nextRace: null, + recentResults: [], + leagueStandingsSummaries: [], + feedSummary: { + notificationCount: 0, + items: [], + }, + friends: [], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + // All fields should have safe defaults + expect(result.currentDriver.name).toBe(''); + expect(result.currentDriver.avatarUrl).toBe(''); + expect(result.currentDriver.country).toBe(''); + expect(result.currentDriver.rating).toBe('0.0'); + expect(result.currentDriver.rank).toBe('0'); + expect(result.currentDriver.totalRaces).toBe('0'); + expect(result.currentDriver.wins).toBe('0'); + expect(result.currentDriver.podiums).toBe('0'); + expect(result.currentDriver.consistency).toBe('0%'); + expect(result.nextRace).toBeNull(); + expect(result.upcomingRaces).toEqual([]); + expect(result.leagueStandings).toEqual([]); + expect(result.feedItems).toEqual([]); + expect(result.friends).toEqual([]); + expect(result.activeLeaguesCount).toBe('0'); + expect(result.friendCount).toBe('0'); + }); + + it('should all preserve ISO timestamps for serialization', () => { + const now = new Date(); + const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); + const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000); + + const dashboardDTO: DashboardOverviewDTO = { + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [], + activeLeaguesCount: 1, + nextRace: { + id: 'race-1', + track: 'Spa', + car: 'Porsche', + scheduledAt: futureDate.toISOString(), + status: 'scheduled', + isMyLeague: true, + }, + recentResults: [], + leagueStandingsSummaries: [], + feedSummary: { + notificationCount: 1, + items: [ + { + id: 'feed-1', + type: 'notification', + headline: 'Test', + timestamp: feedTimestamp.toISOString(), + }, + ], + }, + friends: [], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + // All timestamps should be preserved as ISO strings + expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString()); + expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString()); + }); + + it('should all handle boolean flags correctly', () => { + const dashboardDTO: DashboardOverviewDTO = { + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [ + { + id: 'race-1', + track: 'Spa', + car: 'Porsche', + scheduledAt: new Date().toISOString(), + status: 'scheduled', + isMyLeague: true, + }, + { + id: 'race-2', + track: 'Monza', + car: 'Ferrari', + scheduledAt: new Date().toISOString(), + status: 'scheduled', + isMyLeague: false, + }, + ], + activeLeaguesCount: 1, + nextRace: null, + recentResults: [], + leagueStandingsSummaries: [], + feedSummary: { + notificationCount: 0, + items: [], + }, + friends: [], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + expect(result.upcomingRaces[0].isMyLeague).toBe(true); + expect(result.upcomingRaces[1].isMyLeague).toBe(false); + }); + }); + + describe('data integrity', () => { + it('should maintain data consistency across transformations', () => { + const dashboardDTO: DashboardOverviewDTO = { + currentDriver: { + id: 'driver-123', + name: 'John Doe', + country: 'USA', + rating: 1234.56, + globalRank: 42, + totalRaces: 150, + wins: 25, + podiums: 60, + consistency: 85, + }, + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [], + activeLeaguesCount: 3, + nextRace: null, + recentResults: [], + leagueStandingsSummaries: [], + feedSummary: { + notificationCount: 5, + items: [], + }, + friends: [ + { id: 'friend-1', name: 'Alice', country: 'UK' }, + { id: 'friend-2', name: 'Bob', country: 'Germany' }, + ], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + // Verify derived fields match their source data + expect(result.friendCount).toBe(dashboardDTO.friends.length.toString()); + expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString()); + expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0); + expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0); + expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0); + expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0); + }); + + it('should handle complex real-world scenarios', () => { + const now = new Date(); + const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); + const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); + const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000); + + const dashboardDTO: DashboardOverviewDTO = { + currentDriver: { + id: 'driver-123', + name: 'John Doe', + country: 'USA', + avatarUrl: 'https://example.com/avatar.jpg', + rating: 2456.78, + globalRank: 15, + totalRaces: 250, + wins: 45, + podiums: 120, + consistency: 92.5, + }, + myUpcomingRaces: [], + otherUpcomingRaces: [], + upcomingRaces: [ + { + id: 'race-1', + leagueId: 'league-1', + leagueName: 'Pro League', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: race1Date.toISOString(), + status: 'scheduled', + isMyLeague: true, + }, + { + id: 'race-2', + track: 'Monza', + car: 'Ferrari 488 GT3', + scheduledAt: race2Date.toISOString(), + status: 'scheduled', + isMyLeague: false, + }, + ], + activeLeaguesCount: 2, + nextRace: { + id: 'race-1', + leagueId: 'league-1', + leagueName: 'Pro League', + track: 'Spa', + car: 'Porsche 911 GT3', + scheduledAt: race1Date.toISOString(), + status: 'scheduled', + isMyLeague: true, + }, + recentResults: [], + leagueStandingsSummaries: [ + { + leagueId: 'league-1', + leagueName: 'Pro League', + position: 3, + totalDrivers: 100, + points: 2450, + }, + { + leagueId: 'league-2', + leagueName: 'Rookie League', + position: 1, + totalDrivers: 50, + points: 1800, + }, + ], + feedSummary: { + notificationCount: 3, + items: [ + { + id: 'feed-1', + type: 'race_result', + headline: 'Race completed', + body: 'You finished 3rd in the Pro League race', + timestamp: feedTimestamp.toISOString(), + ctaLabel: 'View Results', + ctaHref: '/races/123', + }, + { + id: 'feed-2', + type: 'league_update', + headline: 'League standings updated', + body: 'You moved up 2 positions', + timestamp: feedTimestamp.toISOString(), + }, + ], + }, + friends: [ + { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' }, + { id: 'friend-2', name: 'Bob', country: 'Germany' }, + { id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' }, + ], + }; + + const result = DashboardViewDataBuilder.build(dashboardDTO); + + // Verify all transformations + expect(result.currentDriver.name).toBe('John Doe'); + expect(result.currentDriver.rating).toBe('2,457'); + expect(result.currentDriver.rank).toBe('15'); + expect(result.currentDriver.totalRaces).toBe('250'); + expect(result.currentDriver.wins).toBe('45'); + expect(result.currentDriver.podiums).toBe('120'); + expect(result.currentDriver.consistency).toBe('92.5%'); + + expect(result.nextRace).not.toBeNull(); + expect(result.nextRace?.id).toBe('race-1'); + expect(result.nextRace?.track).toBe('Spa'); + expect(result.nextRace?.isMyLeague).toBe(true); + + expect(result.upcomingRaces).toHaveLength(2); + expect(result.upcomingRaces[0].isMyLeague).toBe(true); + expect(result.upcomingRaces[1].isMyLeague).toBe(false); + + expect(result.leagueStandings).toHaveLength(2); + expect(result.leagueStandings[0].position).toBe('#3'); + expect(result.leagueStandings[0].points).toBe('2450'); + expect(result.leagueStandings[1].position).toBe('#1'); + expect(result.leagueStandings[1].points).toBe('1800'); + + expect(result.feedItems).toHaveLength(2); + expect(result.feedItems[0].type).toBe('race_result'); + expect(result.feedItems[0].ctaLabel).toBe('View Results'); + expect(result.feedItems[1].type).toBe('league_update'); + expect(result.feedItems[1].ctaLabel).toBeUndefined(); + + expect(result.friends).toHaveLength(3); + expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg'); + expect(result.friends[1].avatarUrl).toBe(''); + expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg'); + + expect(result.activeLeaguesCount).toBe('2'); + expect(result.friendCount).toBe('3'); + expect(result.hasUpcomingRaces).toBe(true); + expect(result.hasLeagueStandings).toBe(true); + expect(result.hasFeedItems).toBe(true); + expect(result.hasFriends).toBe(true); + }); + }); +}); diff --git a/apps/website/lib/display-objects/RatingDisplay.test.ts b/apps/website/lib/display-objects/RatingDisplay.test.ts new file mode 100644 index 000000000..1d83c9405 --- /dev/null +++ b/apps/website/lib/display-objects/RatingDisplay.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { RatingDisplay } from './RatingDisplay'; + +describe('RatingDisplay', () => { + describe('happy paths', () => { + it('should format rating correctly', () => { + expect(RatingDisplay.format(0)).toBe('0'); + expect(RatingDisplay.format(1234.56)).toBe('1,235'); + expect(RatingDisplay.format(9999.99)).toBe('10,000'); + }); + + it('should handle null values', () => { + expect(RatingDisplay.format(null)).toBe('—'); + }); + + it('should handle undefined values', () => { + expect(RatingDisplay.format(undefined)).toBe('—'); + }); + }); + + describe('edge cases', () => { + it('should round down correctly', () => { + expect(RatingDisplay.format(1234.4)).toBe('1,234'); + }); + + it('should round up correctly', () => { + expect(RatingDisplay.format(1234.6)).toBe('1,235'); + }); + + it('should handle decimal ratings', () => { + expect(RatingDisplay.format(1234.5)).toBe('1,235'); + }); + + it('should handle large ratings', () => { + expect(RatingDisplay.format(999999.99)).toBe('1,000,000'); + }); + }); +}); diff --git a/apps/website/tests/view-data/auth.test.ts b/apps/website/tests/view-data/auth.test.ts deleted file mode 100644 index 60a84684e..000000000 --- a/apps/website/tests/view-data/auth.test.ts +++ /dev/null @@ -1,1020 +0,0 @@ -/** - * View Data Layer Tests - Auth Functionality - * - * This test file covers the view data layer for auth functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Login form data transformation and validation - * - Signup form view models and field formatting - * - Forgot password flow data handling - * - Reset password token validation and UI state - * - Auth error message formatting and display - * - User session data mapping for UI components - * - Derived auth state fields (isAuthenticated, authStatus, etc.) - * - Default values and fallbacks for auth views - * - Auth-specific formatting (password strength, email validation, etc.) - */ - -import { LoginViewDataBuilder } from '@/lib/builders/view-data/LoginViewDataBuilder'; -import { SignupViewDataBuilder } from '@/lib/builders/view-data/SignupViewDataBuilder'; -import { ForgotPasswordViewDataBuilder } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder'; -import { ResetPasswordViewDataBuilder } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder'; -import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO'; -import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO'; -import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO'; -import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO'; - -describe('LoginViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform LoginPageDTO to LoginViewData correctly', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result).toEqual({ - returnTo: '/dashboard', - hasInsufficientPermissions: false, - showPassword: false, - showErrorDetails: false, - formState: { - fields: { - email: { value: '', error: undefined, touched: false, validating: false }, - password: { value: '', error: undefined, touched: false, validating: false }, - rememberMe: { value: false, error: undefined, touched: false, validating: false }, - }, - isValid: true, - isSubmitting: false, - submitError: undefined, - submitCount: 0, - }, - isSubmitting: false, - submitError: undefined, - }); - }); - - it('should handle insufficient permissions flag correctly', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/admin', - hasInsufficientPermissions: true, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.hasInsufficientPermissions).toBe(true); - expect(result.returnTo).toBe('/admin'); - }); - - it('should handle empty returnTo path', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.returnTo).toBe(''); - expect(result.hasInsufficientPermissions).toBe(false); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.returnTo).toBe(loginPageDTO.returnTo); - expect(result.hasInsufficientPermissions).toBe(loginPageDTO.hasInsufficientPermissions); - }); - - it('should not modify the input DTO', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const originalDTO = { ...loginPageDTO }; - LoginViewDataBuilder.build(loginPageDTO); - - expect(loginPageDTO).toEqual(originalDTO); - }); - - it('should initialize form fields with default values', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.formState.fields.email.value).toBe(''); - expect(result.formState.fields.email.error).toBeUndefined(); - expect(result.formState.fields.email.touched).toBe(false); - expect(result.formState.fields.email.validating).toBe(false); - - expect(result.formState.fields.password.value).toBe(''); - expect(result.formState.fields.password.error).toBeUndefined(); - expect(result.formState.fields.password.touched).toBe(false); - expect(result.formState.fields.password.validating).toBe(false); - - expect(result.formState.fields.rememberMe.value).toBe(false); - expect(result.formState.fields.rememberMe.error).toBeUndefined(); - expect(result.formState.fields.rememberMe.touched).toBe(false); - expect(result.formState.fields.rememberMe.validating).toBe(false); - }); - - it('should initialize form state with default values', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.formState.isValid).toBe(true); - expect(result.formState.isSubmitting).toBe(false); - expect(result.formState.submitError).toBeUndefined(); - expect(result.formState.submitCount).toBe(0); - }); - - it('should initialize UI state flags correctly', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.showPassword).toBe(false); - expect(result.showErrorDetails).toBe(false); - expect(result.isSubmitting).toBe(false); - expect(result.submitError).toBeUndefined(); - }); - }); - - describe('edge cases', () => { - it('should handle special characters in returnTo path', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard?param=value&other=test', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.returnTo).toBe('/dashboard?param=value&other=test'); - }); - - it('should handle returnTo with hash fragment', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard#section', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.returnTo).toBe('/dashboard#section'); - }); - - it('should handle returnTo with encoded characters', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard?redirect=%2Fadmin', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - }); - }); - - describe('form state structure', () => { - it('should have all required form fields', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - expect(result.formState.fields).toHaveProperty('email'); - expect(result.formState.fields).toHaveProperty('password'); - expect(result.formState.fields).toHaveProperty('rememberMe'); - }); - - it('should have consistent field state structure', () => { - const loginPageDTO: LoginPageDTO = { - returnTo: '/dashboard', - hasInsufficientPermissions: false, - }; - - const result = LoginViewDataBuilder.build(loginPageDTO); - - const fields = result.formState.fields; - Object.values(fields).forEach((field) => { - expect(field).toHaveProperty('value'); - expect(field).toHaveProperty('error'); - expect(field).toHaveProperty('touched'); - expect(field).toHaveProperty('validating'); - }); - }); - }); -}); - -describe('SignupViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform SignupPageDTO to SignupViewData correctly', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result).toEqual({ - returnTo: '/dashboard', - formState: { - fields: { - firstName: { value: '', error: undefined, touched: false, validating: false }, - lastName: { value: '', error: undefined, touched: false, validating: false }, - email: { value: '', error: undefined, touched: false, validating: false }, - password: { value: '', error: undefined, touched: false, validating: false }, - confirmPassword: { value: '', error: undefined, touched: false, validating: false }, - }, - isValid: true, - isSubmitting: false, - submitError: undefined, - submitCount: 0, - }, - isSubmitting: false, - submitError: undefined, - }); - }); - - it('should handle empty returnTo path', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.returnTo).toBe(''); - }); - - it('should handle returnTo with query parameters', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard?welcome=true', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.returnTo).toBe('/dashboard?welcome=true'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.returnTo).toBe(signupPageDTO.returnTo); - }); - - it('should not modify the input DTO', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const originalDTO = { ...signupPageDTO }; - SignupViewDataBuilder.build(signupPageDTO); - - expect(signupPageDTO).toEqual(originalDTO); - }); - - it('should initialize all signup form fields with default values', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.formState.fields.firstName.value).toBe(''); - expect(result.formState.fields.firstName.error).toBeUndefined(); - expect(result.formState.fields.firstName.touched).toBe(false); - expect(result.formState.fields.firstName.validating).toBe(false); - - expect(result.formState.fields.lastName.value).toBe(''); - expect(result.formState.fields.lastName.error).toBeUndefined(); - expect(result.formState.fields.lastName.touched).toBe(false); - expect(result.formState.fields.lastName.validating).toBe(false); - - expect(result.formState.fields.email.value).toBe(''); - expect(result.formState.fields.email.error).toBeUndefined(); - expect(result.formState.fields.email.touched).toBe(false); - expect(result.formState.fields.email.validating).toBe(false); - - expect(result.formState.fields.password.value).toBe(''); - expect(result.formState.fields.password.error).toBeUndefined(); - expect(result.formState.fields.password.touched).toBe(false); - expect(result.formState.fields.password.validating).toBe(false); - - expect(result.formState.fields.confirmPassword.value).toBe(''); - expect(result.formState.fields.confirmPassword.error).toBeUndefined(); - expect(result.formState.fields.confirmPassword.touched).toBe(false); - expect(result.formState.fields.confirmPassword.validating).toBe(false); - }); - - it('should initialize form state with default values', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.formState.isValid).toBe(true); - expect(result.formState.isSubmitting).toBe(false); - expect(result.formState.submitError).toBeUndefined(); - expect(result.formState.submitCount).toBe(0); - }); - - it('should initialize UI state flags correctly', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.isSubmitting).toBe(false); - expect(result.submitError).toBeUndefined(); - }); - }); - - describe('edge cases', () => { - it('should handle returnTo with encoded characters', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard?redirect=%2Fadmin', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - }); - - it('should handle returnTo with hash fragment', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard#section', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.returnTo).toBe('/dashboard#section'); - }); - }); - - describe('form state structure', () => { - it('should have all required form fields', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - expect(result.formState.fields).toHaveProperty('firstName'); - expect(result.formState.fields).toHaveProperty('lastName'); - expect(result.formState.fields).toHaveProperty('email'); - expect(result.formState.fields).toHaveProperty('password'); - expect(result.formState.fields).toHaveProperty('confirmPassword'); - }); - - it('should have consistent field state structure', () => { - const signupPageDTO: SignupPageDTO = { - returnTo: '/dashboard', - }; - - const result = SignupViewDataBuilder.build(signupPageDTO); - - const fields = result.formState.fields; - Object.values(fields).forEach((field) => { - expect(field).toHaveProperty('value'); - expect(field).toHaveProperty('error'); - expect(field).toHaveProperty('touched'); - expect(field).toHaveProperty('validating'); - }); - }); - }); -}); - -describe('ForgotPasswordViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform ForgotPasswordPageDTO to ForgotPasswordViewData correctly', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result).toEqual({ - returnTo: '/login', - showSuccess: false, - formState: { - fields: { - email: { value: '', error: undefined, touched: false, validating: false }, - }, - isValid: true, - isSubmitting: false, - submitError: undefined, - submitCount: 0, - }, - isSubmitting: false, - submitError: undefined, - }); - }); - - it('should handle empty returnTo path', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.returnTo).toBe(''); - }); - - it('should handle returnTo with query parameters', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login?error=expired', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.returnTo).toBe('/login?error=expired'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.returnTo).toBe(forgotPasswordPageDTO.returnTo); - }); - - it('should not modify the input DTO', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const originalDTO = { ...forgotPasswordPageDTO }; - ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(forgotPasswordPageDTO).toEqual(originalDTO); - }); - - it('should initialize form field with default values', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.formState.fields.email.value).toBe(''); - expect(result.formState.fields.email.error).toBeUndefined(); - expect(result.formState.fields.email.touched).toBe(false); - expect(result.formState.fields.email.validating).toBe(false); - }); - - it('should initialize form state with default values', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.formState.isValid).toBe(true); - expect(result.formState.isSubmitting).toBe(false); - expect(result.formState.submitError).toBeUndefined(); - expect(result.formState.submitCount).toBe(0); - }); - - it('should initialize UI state flags correctly', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.showSuccess).toBe(false); - expect(result.isSubmitting).toBe(false); - expect(result.submitError).toBeUndefined(); - }); - }); - - describe('edge cases', () => { - it('should handle returnTo with encoded characters', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login?redirect=%2Fdashboard', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.returnTo).toBe('/login?redirect=%2Fdashboard'); - }); - - it('should handle returnTo with hash fragment', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login#section', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.returnTo).toBe('/login#section'); - }); - }); - - describe('form state structure', () => { - it('should have email field', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - expect(result.formState.fields).toHaveProperty('email'); - }); - - it('should have consistent field state structure', () => { - const forgotPasswordPageDTO: ForgotPasswordPageDTO = { - returnTo: '/login', - }; - - const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO); - - const field = result.formState.fields.email; - expect(field).toHaveProperty('value'); - expect(field).toHaveProperty('error'); - expect(field).toHaveProperty('touched'); - expect(field).toHaveProperty('validating'); - }); - }); -}); - -describe('ResetPasswordViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform ResetPasswordPageDTO to ResetPasswordViewData correctly', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result).toEqual({ - token: 'abc123def456', - returnTo: '/login', - showSuccess: false, - formState: { - fields: { - newPassword: { value: '', error: undefined, touched: false, validating: false }, - confirmPassword: { value: '', error: undefined, touched: false, validating: false }, - }, - isValid: true, - isSubmitting: false, - submitError: undefined, - submitCount: 0, - }, - isSubmitting: false, - submitError: undefined, - }); - }); - - it('should handle empty returnTo path', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.returnTo).toBe(''); - }); - - it('should handle returnTo with query parameters', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login?success=true', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.returnTo).toBe('/login?success=true'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.token).toBe(resetPasswordPageDTO.token); - expect(result.returnTo).toBe(resetPasswordPageDTO.returnTo); - }); - - it('should not modify the input DTO', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const originalDTO = { ...resetPasswordPageDTO }; - ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(resetPasswordPageDTO).toEqual(originalDTO); - }); - - it('should initialize form fields with default values', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.formState.fields.newPassword.value).toBe(''); - expect(result.formState.fields.newPassword.error).toBeUndefined(); - expect(result.formState.fields.newPassword.touched).toBe(false); - expect(result.formState.fields.newPassword.validating).toBe(false); - - expect(result.formState.fields.confirmPassword.value).toBe(''); - expect(result.formState.fields.confirmPassword.error).toBeUndefined(); - expect(result.formState.fields.confirmPassword.touched).toBe(false); - expect(result.formState.fields.confirmPassword.validating).toBe(false); - }); - - it('should initialize form state with default values', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.formState.isValid).toBe(true); - expect(result.formState.isSubmitting).toBe(false); - expect(result.formState.submitError).toBeUndefined(); - expect(result.formState.submitCount).toBe(0); - }); - - it('should initialize UI state flags correctly', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.showSuccess).toBe(false); - expect(result.isSubmitting).toBe(false); - expect(result.submitError).toBeUndefined(); - }); - }); - - describe('edge cases', () => { - it('should handle token with special characters', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc-123_def.456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.token).toBe('abc-123_def.456'); - }); - - it('should handle token with URL-encoded characters', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc%20123%40def', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.token).toBe('abc%20123%40def'); - }); - - it('should handle returnTo with encoded characters', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login?redirect=%2Fdashboard', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.returnTo).toBe('/login?redirect=%2Fdashboard'); - }); - - it('should handle returnTo with hash fragment', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login#section', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.returnTo).toBe('/login#section'); - }); - }); - - describe('form state structure', () => { - it('should have all required form fields', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - expect(result.formState.fields).toHaveProperty('newPassword'); - expect(result.formState.fields).toHaveProperty('confirmPassword'); - }); - - it('should have consistent field state structure', () => { - const resetPasswordPageDTO: ResetPasswordPageDTO = { - token: 'abc123def456', - returnTo: '/login', - }; - - const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO); - - const fields = result.formState.fields; - Object.values(fields).forEach((field) => { - expect(field).toHaveProperty('value'); - expect(field).toHaveProperty('error'); - expect(field).toHaveProperty('touched'); - expect(field).toHaveProperty('validating'); - }); - }); - }); -}); - -describe('Auth View Data - Cross-Builder Consistency', () => { - describe('common patterns', () => { - it('should all initialize with isSubmitting false', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.isSubmitting).toBe(false); - expect(signupResult.isSubmitting).toBe(false); - expect(forgotPasswordResult.isSubmitting).toBe(false); - expect(resetPasswordResult.isSubmitting).toBe(false); - }); - - it('should all initialize with submitError undefined', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.submitError).toBeUndefined(); - expect(signupResult.submitError).toBeUndefined(); - expect(forgotPasswordResult.submitError).toBeUndefined(); - expect(resetPasswordResult.submitError).toBeUndefined(); - }); - - it('should all initialize formState.isValid as true', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.isValid).toBe(true); - expect(signupResult.formState.isValid).toBe(true); - expect(forgotPasswordResult.formState.isValid).toBe(true); - expect(resetPasswordResult.formState.isValid).toBe(true); - }); - - it('should all initialize formState.isSubmitting as false', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.isSubmitting).toBe(false); - expect(signupResult.formState.isSubmitting).toBe(false); - expect(forgotPasswordResult.formState.isSubmitting).toBe(false); - expect(resetPasswordResult.formState.isSubmitting).toBe(false); - }); - - it('should all initialize formState.submitError as undefined', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.submitError).toBeUndefined(); - expect(signupResult.formState.submitError).toBeUndefined(); - expect(forgotPasswordResult.formState.submitError).toBeUndefined(); - expect(resetPasswordResult.formState.submitError).toBeUndefined(); - }); - - it('should all initialize formState.submitCount as 0', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.submitCount).toBe(0); - expect(signupResult.formState.submitCount).toBe(0); - expect(forgotPasswordResult.formState.submitCount).toBe(0); - expect(resetPasswordResult.formState.submitCount).toBe(0); - }); - - it('should all initialize form fields with touched false', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.fields.email.touched).toBe(false); - expect(loginResult.formState.fields.password.touched).toBe(false); - expect(loginResult.formState.fields.rememberMe.touched).toBe(false); - - expect(signupResult.formState.fields.firstName.touched).toBe(false); - expect(signupResult.formState.fields.lastName.touched).toBe(false); - expect(signupResult.formState.fields.email.touched).toBe(false); - expect(signupResult.formState.fields.password.touched).toBe(false); - expect(signupResult.formState.fields.confirmPassword.touched).toBe(false); - - expect(forgotPasswordResult.formState.fields.email.touched).toBe(false); - - expect(resetPasswordResult.formState.fields.newPassword.touched).toBe(false); - expect(resetPasswordResult.formState.fields.confirmPassword.touched).toBe(false); - }); - - it('should all initialize form fields with validating false', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.fields.email.validating).toBe(false); - expect(loginResult.formState.fields.password.validating).toBe(false); - expect(loginResult.formState.fields.rememberMe.validating).toBe(false); - - expect(signupResult.formState.fields.firstName.validating).toBe(false); - expect(signupResult.formState.fields.lastName.validating).toBe(false); - expect(signupResult.formState.fields.email.validating).toBe(false); - expect(signupResult.formState.fields.password.validating).toBe(false); - expect(signupResult.formState.fields.confirmPassword.validating).toBe(false); - - expect(forgotPasswordResult.formState.fields.email.validating).toBe(false); - - expect(resetPasswordResult.formState.fields.newPassword.validating).toBe(false); - expect(resetPasswordResult.formState.fields.confirmPassword.validating).toBe(false); - }); - - it('should all initialize form fields with error undefined', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.formState.fields.email.error).toBeUndefined(); - expect(loginResult.formState.fields.password.error).toBeUndefined(); - expect(loginResult.formState.fields.rememberMe.error).toBeUndefined(); - - expect(signupResult.formState.fields.firstName.error).toBeUndefined(); - expect(signupResult.formState.fields.lastName.error).toBeUndefined(); - expect(signupResult.formState.fields.email.error).toBeUndefined(); - expect(signupResult.formState.fields.password.error).toBeUndefined(); - expect(signupResult.formState.fields.confirmPassword.error).toBeUndefined(); - - expect(forgotPasswordResult.formState.fields.email.error).toBeUndefined(); - - expect(resetPasswordResult.formState.fields.newPassword.error).toBeUndefined(); - expect(resetPasswordResult.formState.fields.confirmPassword.error).toBeUndefined(); - }); - }); - - describe('common returnTo handling', () => { - it('should all handle returnTo with query parameters', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard?welcome=true', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard?welcome=true' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?welcome=true' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?welcome=true' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.returnTo).toBe('/dashboard?welcome=true'); - expect(signupResult.returnTo).toBe('/dashboard?welcome=true'); - expect(forgotPasswordResult.returnTo).toBe('/dashboard?welcome=true'); - expect(resetPasswordResult.returnTo).toBe('/dashboard?welcome=true'); - }); - - it('should all handle returnTo with hash fragments', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard#section', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard#section' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard#section' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard#section' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.returnTo).toBe('/dashboard#section'); - expect(signupResult.returnTo).toBe('/dashboard#section'); - expect(forgotPasswordResult.returnTo).toBe('/dashboard#section'); - expect(resetPasswordResult.returnTo).toBe('/dashboard#section'); - }); - - it('should all handle returnTo with encoded characters', () => { - const loginDTO: LoginPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin', hasInsufficientPermissions: false }; - const signupDTO: SignupPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' }; - const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' }; - const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?redirect=%2Fadmin' }; - - const loginResult = LoginViewDataBuilder.build(loginDTO); - const signupResult = SignupViewDataBuilder.build(signupDTO); - const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO); - const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO); - - expect(loginResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - expect(signupResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - expect(forgotPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - expect(resetPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin'); - }); - }); -}); diff --git a/apps/website/tests/view-data/health.test.ts b/apps/website/tests/view-data/health.test.ts deleted file mode 100644 index d8657ea35..000000000 --- a/apps/website/tests/view-data/health.test.ts +++ /dev/null @@ -1,1065 +0,0 @@ -/** - * View Data Layer Tests - Health Functionality - * - * This test file covers the view data layer for health functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Health status data transformation and aggregation - * - System metrics and performance view models - * - Health check data formatting and validation - * - Derived health fields (status indicators, alerts, etc.) - * - Default values and fallbacks for health views - * - Health-specific formatting (uptime, response times, error rates, etc.) - * - Data grouping and categorization for health components - * - Real-time health monitoring data updates - * - Health alert and notification view models - */ - -import { HealthViewDataBuilder, HealthDTO } from '@/lib/builders/view-data/HealthViewDataBuilder'; -import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay'; -import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay'; -import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay'; -import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay'; - -describe('HealthViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform HealthDTO to HealthViewData correctly', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.95, - responseTime: 150, - errorRate: 0.05, - lastCheck: new Date().toISOString(), - checksPassed: 995, - checksFailed: 5, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: new Date().toISOString(), - responseTime: 50, - errorRate: 0.01, - }, - { - name: 'API', - status: 'ok', - lastCheck: new Date().toISOString(), - responseTime: 100, - errorRate: 0.02, - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'System Update', - message: 'System updated successfully', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe('ok'); - expect(result.overallStatus.statusLabel).toBe('Healthy'); - expect(result.overallStatus.statusColor).toBe('#10b981'); - expect(result.overallStatus.statusIcon).toBe('✓'); - expect(result.metrics.uptime).toBe('99.95%'); - expect(result.metrics.responseTime).toBe('150ms'); - expect(result.metrics.errorRate).toBe('0.05%'); - expect(result.metrics.checksPassed).toBe(995); - expect(result.metrics.checksFailed).toBe(5); - expect(result.metrics.totalChecks).toBe(1000); - expect(result.metrics.successRate).toBe('99.5%'); - expect(result.components).toHaveLength(2); - expect(result.components[0].name).toBe('Database'); - expect(result.components[0].status).toBe('ok'); - expect(result.components[0].statusLabel).toBe('Healthy'); - expect(result.alerts).toHaveLength(1); - expect(result.alerts[0].id).toBe('alert-1'); - expect(result.alerts[0].type).toBe('info'); - expect(result.hasAlerts).toBe(true); - expect(result.hasDegradedComponents).toBe(false); - expect(result.hasErrorComponents).toBe(false); - }); - - it('should handle missing optional fields gracefully', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe('ok'); - expect(result.metrics.uptime).toBe('N/A'); - expect(result.metrics.responseTime).toBe('N/A'); - expect(result.metrics.errorRate).toBe('N/A'); - expect(result.metrics.checksPassed).toBe(0); - expect(result.metrics.checksFailed).toBe(0); - expect(result.metrics.totalChecks).toBe(0); - expect(result.metrics.successRate).toBe('N/A'); - expect(result.components).toEqual([]); - expect(result.alerts).toEqual([]); - expect(result.hasAlerts).toBe(false); - expect(result.hasDegradedComponents).toBe(false); - expect(result.hasErrorComponents).toBe(false); - }); - - it('should handle degraded status correctly', () => { - const healthDTO: HealthDTO = { - status: 'degraded', - timestamp: new Date().toISOString(), - uptime: 95.5, - responseTime: 500, - errorRate: 4.5, - components: [ - { - name: 'Database', - status: 'degraded', - lastCheck: new Date().toISOString(), - responseTime: 200, - errorRate: 2.0, - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe('degraded'); - expect(result.overallStatus.statusLabel).toBe('Degraded'); - expect(result.overallStatus.statusColor).toBe('#f59e0b'); - expect(result.overallStatus.statusIcon).toBe('⚠'); - expect(result.metrics.uptime).toBe('95.50%'); - expect(result.metrics.responseTime).toBe('500ms'); - expect(result.metrics.errorRate).toBe('4.50%'); - expect(result.hasDegradedComponents).toBe(true); - }); - - it('should handle error status correctly', () => { - const healthDTO: HealthDTO = { - status: 'error', - timestamp: new Date().toISOString(), - uptime: 85.2, - responseTime: 2000, - errorRate: 14.8, - components: [ - { - name: 'Database', - status: 'error', - lastCheck: new Date().toISOString(), - responseTime: 1500, - errorRate: 10.0, - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe('error'); - expect(result.overallStatus.statusLabel).toBe('Error'); - expect(result.overallStatus.statusColor).toBe('#ef4444'); - expect(result.overallStatus.statusIcon).toBe('✕'); - expect(result.metrics.uptime).toBe('85.20%'); - expect(result.metrics.responseTime).toBe('2.00s'); - expect(result.metrics.errorRate).toBe('14.80%'); - expect(result.hasErrorComponents).toBe(true); - }); - - it('should handle multiple components with mixed statuses', () => { - const healthDTO: HealthDTO = { - status: 'degraded', - timestamp: new Date().toISOString(), - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - { - name: 'API', - status: 'degraded', - lastCheck: new Date().toISOString(), - }, - { - name: 'Cache', - status: 'error', - lastCheck: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.components).toHaveLength(3); - expect(result.hasDegradedComponents).toBe(true); - expect(result.hasErrorComponents).toBe(true); - expect(result.components[0].statusLabel).toBe('Healthy'); - expect(result.components[1].statusLabel).toBe('Degraded'); - expect(result.components[2].statusLabel).toBe('Error'); - }); - - it('should handle multiple alerts with different severities', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - alerts: [ - { - id: 'alert-1', - type: 'critical', - title: 'Critical Alert', - message: 'Critical issue detected', - timestamp: new Date().toISOString(), - }, - { - id: 'alert-2', - type: 'warning', - title: 'Warning Alert', - message: 'Warning message', - timestamp: new Date().toISOString(), - }, - { - id: 'alert-3', - type: 'info', - title: 'Info Alert', - message: 'Informational message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.alerts).toHaveLength(3); - expect(result.hasAlerts).toBe(true); - expect(result.alerts[0].severity).toBe('Critical'); - expect(result.alerts[0].severityColor).toBe('#ef4444'); - expect(result.alerts[1].severity).toBe('Warning'); - expect(result.alerts[1].severityColor).toBe('#f59e0b'); - expect(result.alerts[2].severity).toBe('Info'); - expect(result.alerts[2].severityColor).toBe('#3b82f6'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const now = new Date(); - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: now.toISOString(), - uptime: 99.99, - responseTime: 100, - errorRate: 0.01, - lastCheck: now.toISOString(), - checksPassed: 9999, - checksFailed: 1, - components: [ - { - name: 'Test Component', - status: 'ok', - lastCheck: now.toISOString(), - responseTime: 50, - errorRate: 0.005, - }, - ], - alerts: [ - { - id: 'test-alert', - type: 'info', - title: 'Test Alert', - message: 'Test message', - timestamp: now.toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe(healthDTO.status); - expect(result.overallStatus.timestamp).toBe(healthDTO.timestamp); - expect(result.metrics.uptime).toBe('99.99%'); - expect(result.metrics.responseTime).toBe('100ms'); - expect(result.metrics.errorRate).toBe('0.01%'); - expect(result.metrics.lastCheck).toBe(healthDTO.lastCheck); - expect(result.metrics.checksPassed).toBe(healthDTO.checksPassed); - expect(result.metrics.checksFailed).toBe(healthDTO.checksFailed); - expect(result.components[0].name).toBe(healthDTO.components![0].name); - expect(result.components[0].status).toBe(healthDTO.components![0].status); - expect(result.alerts[0].id).toBe(healthDTO.alerts![0].id); - expect(result.alerts[0].type).toBe(healthDTO.alerts![0].type); - }); - - it('should not modify the input DTO', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.95, - responseTime: 150, - errorRate: 0.05, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - ], - }; - - const originalDTO = JSON.parse(JSON.stringify(healthDTO)); - HealthViewDataBuilder.build(healthDTO); - - expect(healthDTO).toEqual(originalDTO); - }); - - it('should transform all numeric fields to formatted strings', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.95, - responseTime: 150, - errorRate: 0.05, - checksPassed: 995, - checksFailed: 5, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(typeof result.metrics.uptime).toBe('string'); - expect(typeof result.metrics.responseTime).toBe('string'); - expect(typeof result.metrics.errorRate).toBe('string'); - expect(typeof result.metrics.successRate).toBe('string'); - }); - - it('should handle large numbers correctly', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.999, - responseTime: 5000, - errorRate: 0.001, - checksPassed: 999999, - checksFailed: 1, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.uptime).toBe('100.00%'); - expect(result.metrics.responseTime).toBe('5.00s'); - expect(result.metrics.errorRate).toBe('0.00%'); - expect(result.metrics.successRate).toBe('100.0%'); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined numeric fields', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: null as any, - responseTime: undefined, - errorRate: null as any, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.uptime).toBe('N/A'); - expect(result.metrics.responseTime).toBe('N/A'); - expect(result.metrics.errorRate).toBe('N/A'); - }); - - it('should handle negative numeric values', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: -1, - responseTime: -100, - errorRate: -0.5, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.uptime).toBe('N/A'); - expect(result.metrics.responseTime).toBe('N/A'); - expect(result.metrics.errorRate).toBe('N/A'); - }); - - it('should handle empty components and alerts arrays', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - components: [], - alerts: [], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.components).toEqual([]); - expect(result.alerts).toEqual([]); - expect(result.hasAlerts).toBe(false); - expect(result.hasDegradedComponents).toBe(false); - expect(result.hasErrorComponents).toBe(false); - }); - - it('should handle component with missing optional fields', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - components: [ - { - name: 'Test Component', - status: 'ok', - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.components[0].lastCheck).toBeDefined(); - expect(result.components[0].formattedLastCheck).toBeDefined(); - expect(result.components[0].responseTime).toBe('N/A'); - expect(result.components[0].errorRate).toBe('N/A'); - }); - - it('should handle alert with missing optional fields', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test Alert', - message: 'Test message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.alerts[0].id).toBe('alert-1'); - expect(result.alerts[0].type).toBe('info'); - expect(result.alerts[0].title).toBe('Test Alert'); - expect(result.alerts[0].message).toBe('Test message'); - expect(result.alerts[0].timestamp).toBeDefined(); - expect(result.alerts[0].formattedTimestamp).toBeDefined(); - expect(result.alerts[0].relativeTime).toBeDefined(); - }); - - it('should handle unknown status', () => { - const healthDTO: HealthDTO = { - status: 'unknown', - timestamp: new Date().toISOString(), - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.overallStatus.status).toBe('unknown'); - expect(result.overallStatus.statusLabel).toBe('Unknown'); - expect(result.overallStatus.statusColor).toBe('#6b7280'); - expect(result.overallStatus.statusIcon).toBe('?'); - }); - }); - - describe('derived fields', () => { - it('should correctly calculate hasAlerts', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test', - message: 'Test message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.hasAlerts).toBe(true); - }); - - it('should correctly calculate hasDegradedComponents', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - components: [ - { - name: 'Component 1', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - { - name: 'Component 2', - status: 'degraded', - lastCheck: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.hasDegradedComponents).toBe(true); - }); - - it('should correctly calculate hasErrorComponents', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - components: [ - { - name: 'Component 1', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - { - name: 'Component 2', - status: 'error', - lastCheck: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.hasErrorComponents).toBe(true); - }); - - it('should correctly calculate totalChecks', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - checksPassed: 100, - checksFailed: 20, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.totalChecks).toBe(120); - }); - - it('should correctly calculate successRate', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - checksPassed: 90, - checksFailed: 10, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.successRate).toBe('90.0%'); - }); - - it('should handle zero checks correctly', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - checksPassed: 0, - checksFailed: 0, - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.metrics.totalChecks).toBe(0); - expect(result.metrics.successRate).toBe('N/A'); - }); - }); -}); - -describe('HealthStatusDisplay', () => { - describe('happy paths', () => { - it('should format status labels correctly', () => { - expect(HealthStatusDisplay.formatStatusLabel('ok')).toBe('Healthy'); - expect(HealthStatusDisplay.formatStatusLabel('degraded')).toBe('Degraded'); - expect(HealthStatusDisplay.formatStatusLabel('error')).toBe('Error'); - expect(HealthStatusDisplay.formatStatusLabel('unknown')).toBe('Unknown'); - }); - - it('should format status colors correctly', () => { - expect(HealthStatusDisplay.formatStatusColor('ok')).toBe('#10b981'); - expect(HealthStatusDisplay.formatStatusColor('degraded')).toBe('#f59e0b'); - expect(HealthStatusDisplay.formatStatusColor('error')).toBe('#ef4444'); - expect(HealthStatusDisplay.formatStatusColor('unknown')).toBe('#6b7280'); - }); - - it('should format status icons correctly', () => { - expect(HealthStatusDisplay.formatStatusIcon('ok')).toBe('✓'); - expect(HealthStatusDisplay.formatStatusIcon('degraded')).toBe('⚠'); - expect(HealthStatusDisplay.formatStatusIcon('error')).toBe('✕'); - expect(HealthStatusDisplay.formatStatusIcon('unknown')).toBe('?'); - }); - - it('should format timestamp correctly', () => { - const timestamp = '2024-01-15T10:30:45.123Z'; - const result = HealthStatusDisplay.formatTimestamp(timestamp); - expect(result).toMatch(/Jan 15, 2024, \d{1,2}:\d{2}:\d{2}/); - }); - - it('should format relative time correctly', () => { - const now = new Date(); - const oneMinuteAgo = new Date(now.getTime() - 60 * 1000); - const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - - expect(HealthStatusDisplay.formatRelativeTime(oneMinuteAgo.toISOString())).toBe('1m ago'); - expect(HealthStatusDisplay.formatRelativeTime(oneHourAgo.toISOString())).toBe('1h ago'); - expect(HealthStatusDisplay.formatRelativeTime(oneDayAgo.toISOString())).toBe('1d ago'); - }); - }); - - describe('edge cases', () => { - it('should handle unknown status', () => { - expect(HealthStatusDisplay.formatStatusLabel('unknown' as any)).toBe('Unknown'); - expect(HealthStatusDisplay.formatStatusColor('unknown' as any)).toBe('#6b7280'); - expect(HealthStatusDisplay.formatStatusIcon('unknown' as any)).toBe('?'); - }); - - it('should handle just now relative time', () => { - const now = new Date(); - const justNow = new Date(now.getTime() - 30 * 1000); - expect(HealthStatusDisplay.formatRelativeTime(justNow.toISOString())).toBe('Just now'); - }); - - it('should handle weeks ago relative time', () => { - const now = new Date(); - const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); - expect(HealthStatusDisplay.formatRelativeTime(twoWeeksAgo.toISOString())).toBe('2w ago'); - }); - }); -}); - -describe('HealthMetricDisplay', () => { - describe('happy paths', () => { - it('should format uptime correctly', () => { - expect(HealthMetricDisplay.formatUptime(99.95)).toBe('99.95%'); - expect(HealthMetricDisplay.formatUptime(100)).toBe('100.00%'); - expect(HealthMetricDisplay.formatUptime(0)).toBe('0.00%'); - }); - - it('should format response time correctly', () => { - expect(HealthMetricDisplay.formatResponseTime(150)).toBe('150ms'); - expect(HealthMetricDisplay.formatResponseTime(1500)).toBe('1.50s'); - expect(HealthMetricDisplay.formatResponseTime(90000)).toBe('1.50m'); - }); - - it('should format error rate correctly', () => { - expect(HealthMetricDisplay.formatErrorRate(0.05)).toBe('0.05%'); - expect(HealthMetricDisplay.formatErrorRate(5.5)).toBe('5.50%'); - expect(HealthMetricDisplay.formatErrorRate(100)).toBe('100.00%'); - }); - - it('should format timestamp correctly', () => { - const timestamp = '2024-01-15T10:30:45.123Z'; - const result = HealthMetricDisplay.formatTimestamp(timestamp); - expect(result).toMatch(/Jan 15, 2024, \d{1,2}:\d{2}:\d{2}/); - }); - - it('should format success rate correctly', () => { - expect(HealthMetricDisplay.formatSuccessRate(90, 10)).toBe('90.0%'); - expect(HealthMetricDisplay.formatSuccessRate(100, 0)).toBe('100.0%'); - expect(HealthMetricDisplay.formatSuccessRate(0, 100)).toBe('0.0%'); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined values', () => { - expect(HealthMetricDisplay.formatUptime(null as any)).toBe('N/A'); - expect(HealthMetricDisplay.formatUptime(undefined)).toBe('N/A'); - expect(HealthMetricDisplay.formatResponseTime(null as any)).toBe('N/A'); - expect(HealthMetricDisplay.formatResponseTime(undefined)).toBe('N/A'); - expect(HealthMetricDisplay.formatErrorRate(null as any)).toBe('N/A'); - expect(HealthMetricDisplay.formatErrorRate(undefined)).toBe('N/A'); - }); - - it('should handle negative values', () => { - expect(HealthMetricDisplay.formatUptime(-1)).toBe('N/A'); - expect(HealthMetricDisplay.formatResponseTime(-100)).toBe('N/A'); - expect(HealthMetricDisplay.formatErrorRate(-0.5)).toBe('N/A'); - }); - - it('should handle zero checks', () => { - expect(HealthMetricDisplay.formatSuccessRate(0, 0)).toBe('N/A'); - }); - - it('should handle decimal response times', () => { - expect(HealthMetricDisplay.formatResponseTime(1234.56)).toBe('1.23s'); - }); - }); -}); - -describe('HealthComponentDisplay', () => { - describe('happy paths', () => { - it('should format component status labels correctly', () => { - expect(HealthComponentDisplay.formatStatusLabel('ok')).toBe('Healthy'); - expect(HealthComponentDisplay.formatStatusLabel('degraded')).toBe('Degraded'); - expect(HealthComponentDisplay.formatStatusLabel('error')).toBe('Error'); - expect(HealthComponentDisplay.formatStatusLabel('unknown')).toBe('Unknown'); - }); - - it('should format component status colors correctly', () => { - expect(HealthComponentDisplay.formatStatusColor('ok')).toBe('#10b981'); - expect(HealthComponentDisplay.formatStatusColor('degraded')).toBe('#f59e0b'); - expect(HealthComponentDisplay.formatStatusColor('error')).toBe('#ef4444'); - expect(HealthComponentDisplay.formatStatusColor('unknown')).toBe('#6b7280'); - }); - - it('should format component status icons correctly', () => { - expect(HealthComponentDisplay.formatStatusIcon('ok')).toBe('✓'); - expect(HealthComponentDisplay.formatStatusIcon('degraded')).toBe('⚠'); - expect(HealthComponentDisplay.formatStatusIcon('error')).toBe('✕'); - expect(HealthComponentDisplay.formatStatusIcon('unknown')).toBe('?'); - }); - - it('should format timestamp correctly', () => { - const timestamp = '2024-01-15T10:30:45.123Z'; - const result = HealthComponentDisplay.formatTimestamp(timestamp); - expect(result).toMatch(/Jan 15, 2024, \d{1,2}:\d{2}:\d{2}/); - }); - }); - - describe('edge cases', () => { - it('should handle unknown status', () => { - expect(HealthComponentDisplay.formatStatusLabel('unknown' as any)).toBe('Unknown'); - expect(HealthComponentDisplay.formatStatusColor('unknown' as any)).toBe('#6b7280'); - expect(HealthComponentDisplay.formatStatusIcon('unknown' as any)).toBe('?'); - }); - }); -}); - -describe('HealthAlertDisplay', () => { - describe('happy paths', () => { - it('should format alert severities correctly', () => { - expect(HealthAlertDisplay.formatSeverity('critical')).toBe('Critical'); - expect(HealthAlertDisplay.formatSeverity('warning')).toBe('Warning'); - expect(HealthAlertDisplay.formatSeverity('info')).toBe('Info'); - }); - - it('should format alert severity colors correctly', () => { - expect(HealthAlertDisplay.formatSeverityColor('critical')).toBe('#ef4444'); - expect(HealthAlertDisplay.formatSeverityColor('warning')).toBe('#f59e0b'); - expect(HealthAlertDisplay.formatSeverityColor('info')).toBe('#3b82f6'); - }); - - it('should format timestamp correctly', () => { - const timestamp = '2024-01-15T10:30:45.123Z'; - const result = HealthAlertDisplay.formatTimestamp(timestamp); - expect(result).toMatch(/Jan 15, 2024, \d{1,2}:\d{2}:\d{2}/); - }); - - it('should format relative time correctly', () => { - const now = new Date(); - const oneMinuteAgo = new Date(now.getTime() - 60 * 1000); - const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - - expect(HealthAlertDisplay.formatRelativeTime(oneMinuteAgo.toISOString())).toBe('1m ago'); - expect(HealthAlertDisplay.formatRelativeTime(oneHourAgo.toISOString())).toBe('1h ago'); - expect(HealthAlertDisplay.formatRelativeTime(oneDayAgo.toISOString())).toBe('1d ago'); - }); - }); - - describe('edge cases', () => { - it('should handle unknown type', () => { - expect(HealthAlertDisplay.formatSeverity('unknown' as any)).toBe('Info'); - expect(HealthAlertDisplay.formatSeverityColor('unknown' as any)).toBe('#3b82f6'); - }); - - it('should handle just now relative time', () => { - const now = new Date(); - const justNow = new Date(now.getTime() - 30 * 1000); - expect(HealthAlertDisplay.formatRelativeTime(justNow.toISOString())).toBe('Just now'); - }); - - it('should handle weeks ago relative time', () => { - const now = new Date(); - const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); - expect(HealthAlertDisplay.formatRelativeTime(twoWeeksAgo.toISOString())).toBe('2w ago'); - }); - }); -}); - -describe('Health View Data - Cross-Component Consistency', () => { - describe('common patterns', () => { - it('should all use consistent formatting for numeric values', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.95, - responseTime: 150, - errorRate: 0.05, - checksPassed: 995, - checksFailed: 5, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: new Date().toISOString(), - responseTime: 50, - errorRate: 0.01, - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test', - message: 'Test message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - // All numeric values should be formatted as strings - expect(typeof result.metrics.uptime).toBe('string'); - expect(typeof result.metrics.responseTime).toBe('string'); - expect(typeof result.metrics.errorRate).toBe('string'); - expect(typeof result.metrics.successRate).toBe('string'); - expect(typeof result.components[0].responseTime).toBe('string'); - expect(typeof result.components[0].errorRate).toBe('string'); - }); - - it('should all handle missing data gracefully', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - // All fields should have safe defaults - expect(result.overallStatus.status).toBe('ok'); - expect(result.metrics.uptime).toBe('N/A'); - expect(result.metrics.responseTime).toBe('N/A'); - expect(result.metrics.errorRate).toBe('N/A'); - expect(result.metrics.successRate).toBe('N/A'); - expect(result.components).toEqual([]); - expect(result.alerts).toEqual([]); - expect(result.hasAlerts).toBe(false); - expect(result.hasDegradedComponents).toBe(false); - expect(result.hasErrorComponents).toBe(false); - }); - - it('should all preserve ISO timestamps for serialization', () => { - const now = new Date(); - const timestamp = now.toISOString(); - - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: timestamp, - lastCheck: timestamp, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: timestamp, - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test', - message: 'Test message', - timestamp: timestamp, - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - // All timestamps should be preserved as ISO strings - expect(result.overallStatus.timestamp).toBe(timestamp); - expect(result.metrics.lastCheck).toBe(timestamp); - expect(result.components[0].lastCheck).toBe(timestamp); - expect(result.alerts[0].timestamp).toBe(timestamp); - }); - - it('should all handle boolean flags correctly', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - components: [ - { - name: 'Component 1', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - { - name: 'Component 2', - status: 'degraded', - lastCheck: new Date().toISOString(), - }, - { - name: 'Component 3', - status: 'error', - lastCheck: new Date().toISOString(), - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test', - message: 'Test message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - expect(result.hasAlerts).toBe(true); - expect(result.hasDegradedComponents).toBe(true); - expect(result.hasErrorComponents).toBe(true); - }); - }); - - describe('data integrity', () => { - it('should maintain data consistency across transformations', () => { - const healthDTO: HealthDTO = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: 99.95, - responseTime: 150, - errorRate: 0.05, - checksPassed: 995, - checksFailed: 5, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: new Date().toISOString(), - }, - { - name: 'API', - status: 'degraded', - lastCheck: new Date().toISOString(), - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'info', - title: 'Test', - message: 'Test message', - timestamp: new Date().toISOString(), - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - // Verify derived fields match their source data - expect(result.hasAlerts).toBe(healthDTO.alerts!.length > 0); - expect(result.hasDegradedComponents).toBe( - healthDTO.components!.some((c) => c.status === 'degraded') - ); - expect(result.hasErrorComponents).toBe( - healthDTO.components!.some((c) => c.status === 'error') - ); - expect(result.metrics.totalChecks).toBe( - (healthDTO.checksPassed || 0) + (healthDTO.checksFailed || 0) - ); - }); - - it('should handle complex real-world scenarios', () => { - const now = new Date(); - const timestamp = now.toISOString(); - - const healthDTO: HealthDTO = { - status: 'degraded', - timestamp: timestamp, - uptime: 98.5, - responseTime: 350, - errorRate: 1.5, - lastCheck: timestamp, - checksPassed: 985, - checksFailed: 15, - components: [ - { - name: 'Database', - status: 'ok', - lastCheck: timestamp, - responseTime: 50, - errorRate: 0.01, - }, - { - name: 'API', - status: 'degraded', - lastCheck: timestamp, - responseTime: 200, - errorRate: 2.0, - }, - { - name: 'Cache', - status: 'error', - lastCheck: timestamp, - responseTime: 1000, - errorRate: 10.0, - }, - ], - alerts: [ - { - id: 'alert-1', - type: 'critical', - title: 'Cache Failure', - message: 'Cache service is down', - timestamp: timestamp, - }, - { - id: 'alert-2', - type: 'warning', - title: 'High Response Time', - message: 'API response time is elevated', - timestamp: timestamp, - }, - ], - }; - - const result = HealthViewDataBuilder.build(healthDTO); - - // Verify all transformations - expect(result.overallStatus.status).toBe('degraded'); - expect(result.overallStatus.statusLabel).toBe('Degraded'); - expect(result.metrics.uptime).toBe('98.50%'); - expect(result.metrics.responseTime).toBe('350ms'); - expect(result.metrics.errorRate).toBe('1.50%'); - expect(result.metrics.checksPassed).toBe(985); - expect(result.metrics.checksFailed).toBe(15); - expect(result.metrics.totalChecks).toBe(1000); - expect(result.metrics.successRate).toBe('98.5%'); - - expect(result.components).toHaveLength(3); - expect(result.components[0].statusLabel).toBe('Healthy'); - expect(result.components[1].statusLabel).toBe('Degraded'); - expect(result.components[2].statusLabel).toBe('Error'); - - expect(result.alerts).toHaveLength(2); - expect(result.alerts[0].severity).toBe('Critical'); - expect(result.alerts[0].severityColor).toBe('#ef4444'); - expect(result.alerts[1].severity).toBe('Warning'); - expect(result.alerts[1].severityColor).toBe('#f59e0b'); - - expect(result.hasAlerts).toBe(true); - expect(result.hasDegradedComponents).toBe(true); - expect(result.hasErrorComponents).toBe(true); - }); - }); -}); diff --git a/apps/website/tests/view-data/leaderboards.test.ts b/apps/website/tests/view-data/leaderboards.test.ts deleted file mode 100644 index cfa901c29..000000000 --- a/apps/website/tests/view-data/leaderboards.test.ts +++ /dev/null @@ -1,2053 +0,0 @@ -/** - * View Data Layer Tests - Leaderboards Functionality - * - * This test file covers the view data layer for leaderboards functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Leaderboard data transformation and ranking calculations - * - Driver leaderboard view models (overall, per-race, per-season) - * - Team leaderboard view models (constructor standings, team performance) - * - Leaderboard statistics and metrics formatting - * - Derived leaderboard fields (points, positions, gaps, intervals, etc.) - * - Default values and fallbacks for leaderboard views - * - Leaderboard-specific formatting (lap times, gaps, points, positions, etc.) - * - Data grouping and categorization for leaderboard components - * - Leaderboard sorting and filtering view models - * - Real-time leaderboard updates and state management - * - Historical leaderboard data transformation - * - Leaderboard comparison and trend analysis view models - */ - -import { LeaderboardsViewDataBuilder } from '@/lib/builders/view-data/LeaderboardsViewDataBuilder'; -import { DriverRankingsViewDataBuilder } from '@/lib/builders/view-data/DriverRankingsViewDataBuilder'; -import { TeamRankingsViewDataBuilder } from '@/lib/builders/view-data/TeamRankingsViewDataBuilder'; -import { WinRateDisplay } from '@/lib/display-objects/WinRateDisplay'; -import { MedalDisplay } from '@/lib/display-objects/MedalDisplay'; -import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; -import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; -import type { TeamLeaderboardItemDTO } from '@/lib/types/generated/TeamLeaderboardItemDTO'; -import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; - -describe('LeaderboardsViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform Leaderboards DTO to LeaderboardsViewData correctly', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar1.jpg', - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.0, - skillLevel: 'advanced', - nationality: 'Canada', - racesCompleted: 100, - wins: 15, - podiums: 40, - isActive: true, - rank: 2, - avatarUrl: 'https://example.com/avatar2.jpg', - }, - ], - totalRaces: 250, - totalWins: 40, - activeCount: 2, - }, - teams: { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - ], - recruitingCount: 5, - groupsBySkillLevel: 'pro,advanced,intermediate', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // Verify drivers - expect(result.drivers).toHaveLength(2); - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[0].name).toBe('John Doe'); - expect(result.drivers[0].rating).toBe(1234.56); - expect(result.drivers[0].skillLevel).toBe('pro'); - expect(result.drivers[0].nationality).toBe('USA'); - expect(result.drivers[0].wins).toBe(25); - expect(result.drivers[0].podiums).toBe(60); - expect(result.drivers[0].racesCompleted).toBe(150); - expect(result.drivers[0].rank).toBe(1); - expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); - expect(result.drivers[0].position).toBe(1); - - // Verify teams - expect(result.teams).toHaveLength(2); - expect(result.teams[0].id).toBe('team-1'); - expect(result.teams[0].name).toBe('Racing Team Alpha'); - expect(result.teams[0].tag).toBe('RTA'); - expect(result.teams[0].memberCount).toBe(15); - expect(result.teams[0].totalWins).toBe(50); - expect(result.teams[0].totalRaces).toBe(200); - expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg'); - expect(result.teams[0].position).toBe(1); - expect(result.teams[0].isRecruiting).toBe(false); - expect(result.teams[0].performanceLevel).toBe('elite'); - expect(result.teams[0].rating).toBe(1500); - expect(result.teams[0].category).toBeUndefined(); - }); - - it('should handle empty driver and team arrays', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers).toEqual([]); - expect(result.teams).toEqual([]); - }); - - it('should handle missing avatar URLs with empty string fallback', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].avatarUrl).toBe(''); - expect(result.teams[0].logoUrl).toBe(''); - }); - - it('should handle missing optional team fields with defaults', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.teams[0].rating).toBe(0); - expect(result.teams[0].logoUrl).toBe(''); - }); - - it('should calculate position based on index', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { id: 'driver-1', name: 'Driver 1', rating: 1000, skillLevel: 'pro', nationality: 'USA', racesCompleted: 100, wins: 10, podiums: 30, isActive: true, rank: 1 }, - { id: 'driver-2', name: 'Driver 2', rating: 900, skillLevel: 'advanced', nationality: 'Canada', racesCompleted: 80, wins: 8, podiums: 25, isActive: true, rank: 2 }, - { id: 'driver-3', name: 'Driver 3', rating: 800, skillLevel: 'intermediate', nationality: 'UK', racesCompleted: 60, wins: 5, podiums: 15, isActive: true, rank: 3 }, - ], - totalRaces: 240, - totalWins: 23, - activeCount: 3, - }, - teams: { - teams: [], - recruitingCount: 1, - groupsBySkillLevel: 'elite,advanced,intermediate', - topTeams: [ - { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, - { id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' }, - { id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].position).toBe(1); - expect(result.drivers[1].position).toBe(2); - expect(result.drivers[2].position).toBe(3); - - expect(result.teams[0].position).toBe(1); - expect(result.teams[1].position).toBe(2); - expect(result.teams[2].position).toBe(3); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-123', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 5, - groupsBySkillLevel: 'pro,advanced', - topTeams: [ - { - id: 'team-123', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].name).toBe(leaderboardsDTO.drivers.drivers[0].name); - expect(result.drivers[0].nationality).toBe(leaderboardsDTO.drivers.drivers[0].nationality); - expect(result.drivers[0].avatarUrl).toBe(leaderboardsDTO.drivers.drivers[0].avatarUrl); - expect(result.teams[0].name).toBe(leaderboardsDTO.teams.topTeams[0].name); - expect(result.teams[0].tag).toBe(leaderboardsDTO.teams.topTeams[0].tag); - expect(result.teams[0].logoUrl).toBe(leaderboardsDTO.teams.topTeams[0].logoUrl); - }); - - it('should not modify the input DTO', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-123', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 5, - groupsBySkillLevel: 'pro,advanced', - topTeams: [ - { - id: 'team-123', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const originalDTO = JSON.parse(JSON.stringify(leaderboardsDTO)); - LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(leaderboardsDTO).toEqual(originalDTO); - }); - - it('should handle large numbers correctly', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 999999.99, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 10000, - wins: 2500, - podiums: 5000, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ], - totalRaces: 10000, - totalWins: 2500, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 100, - rating: 999999, - totalWins: 5000, - totalRaces: 10000, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].rating).toBe(999999.99); - expect(result.drivers[0].wins).toBe(2500); - expect(result.drivers[0].podiums).toBe(5000); - expect(result.drivers[0].racesCompleted).toBe(10000); - expect(result.teams[0].rating).toBe(999999); - expect(result.teams[0].totalWins).toBe(5000); - expect(result.teams[0].totalRaces).toBe(10000); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined avatar URLs', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: null as any, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: undefined as any, - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].avatarUrl).toBe(''); - expect(result.teams[0].logoUrl).toBe(''); - }); - - it('should handle null/undefined rating', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: null as any, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - rating: null as any, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.drivers[0].rating).toBeNull(); - expect(result.teams[0].rating).toBe(0); - }); - - it('should handle null/undefined totalWins and totalRaces', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: null as any, - totalRaces: null as any, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.teams[0].totalWins).toBe(0); - expect(result.teams[0].totalRaces).toBe(0); - }); - - it('should handle empty performance level', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: '', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.teams[0].performanceLevel).toBe('N/A'); - }); - }); -}); - -describe('DriverRankingsViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform DriverLeaderboardItemDTO array to DriverRankingsViewData correctly', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar1.jpg', - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.0, - skillLevel: 'advanced', - nationality: 'Canada', - racesCompleted: 100, - wins: 15, - podiums: 40, - isActive: true, - rank: 2, - avatarUrl: 'https://example.com/avatar2.jpg', - }, - { - id: 'driver-3', - name: 'Bob Johnson', - rating: 950.0, - skillLevel: 'intermediate', - nationality: 'UK', - racesCompleted: 80, - wins: 10, - podiums: 30, - isActive: true, - rank: 3, - avatarUrl: 'https://example.com/avatar3.jpg', - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - // Verify drivers - expect(result.drivers).toHaveLength(3); - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[0].name).toBe('John Doe'); - expect(result.drivers[0].rating).toBe(1234.56); - expect(result.drivers[0].skillLevel).toBe('pro'); - expect(result.drivers[0].nationality).toBe('USA'); - expect(result.drivers[0].racesCompleted).toBe(150); - expect(result.drivers[0].wins).toBe(25); - expect(result.drivers[0].podiums).toBe(60); - expect(result.drivers[0].rank).toBe(1); - expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); - expect(result.drivers[0].winRate).toBe('16.7'); - expect(result.drivers[0].medalBg).toBe('bg-warning-amber'); - expect(result.drivers[0].medalColor).toBe('text-warning-amber'); - - // Verify podium (top 3 with special ordering: 2nd, 1st, 3rd) - expect(result.podium).toHaveLength(3); - expect(result.podium[0].id).toBe('driver-1'); - expect(result.podium[0].name).toBe('John Doe'); - expect(result.podium[0].rating).toBe(1234.56); - expect(result.podium[0].wins).toBe(25); - expect(result.podium[0].podiums).toBe(60); - expect(result.podium[0].avatarUrl).toBe('https://example.com/avatar1.jpg'); - expect(result.podium[0].position).toBe(2); // 2nd place - - expect(result.podium[1].id).toBe('driver-2'); - expect(result.podium[1].position).toBe(1); // 1st place - - expect(result.podium[2].id).toBe('driver-3'); - expect(result.podium[2].position).toBe(3); // 3rd place - - // Verify default values - expect(result.searchQuery).toBe(''); - expect(result.selectedSkill).toBe('all'); - expect(result.sortBy).toBe('rank'); - expect(result.showFilters).toBe(false); - }); - - it('should handle empty driver array', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = []; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers).toEqual([]); - expect(result.podium).toEqual([]); - expect(result.searchQuery).toBe(''); - expect(result.selectedSkill).toBe('all'); - expect(result.sortBy).toBe('rank'); - expect(result.showFilters).toBe(false); - }); - - it('should handle less than 3 drivers for podium', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar1.jpg', - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.0, - skillLevel: 'advanced', - nationality: 'Canada', - racesCompleted: 100, - wins: 15, - podiums: 40, - isActive: true, - rank: 2, - avatarUrl: 'https://example.com/avatar2.jpg', - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers).toHaveLength(2); - expect(result.podium).toHaveLength(2); - expect(result.podium[0].position).toBe(2); // 2nd place - expect(result.podium[1].position).toBe(1); // 1st place - }); - - it('should handle missing avatar URLs with empty string fallback', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].avatarUrl).toBe(''); - expect(result.podium[0].avatarUrl).toBe(''); - }); - - it('should calculate win rate correctly', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 100, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.0, - skillLevel: 'advanced', - nationality: 'Canada', - racesCompleted: 50, - wins: 10, - podiums: 25, - isActive: true, - rank: 2, - }, - { - id: 'driver-3', - name: 'Bob Johnson', - rating: 950.0, - skillLevel: 'intermediate', - nationality: 'UK', - racesCompleted: 0, - wins: 0, - podiums: 0, - isActive: true, - rank: 3, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].winRate).toBe('25.0'); - expect(result.drivers[1].winRate).toBe('20.0'); - expect(result.drivers[2].winRate).toBe('0.0'); - }); - - it('should assign correct medal colors based on position', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 1100.0, - skillLevel: 'advanced', - nationality: 'Canada', - racesCompleted: 100, - wins: 15, - podiums: 40, - isActive: true, - rank: 2, - }, - { - id: 'driver-3', - name: 'Bob Johnson', - rating: 950.0, - skillLevel: 'intermediate', - nationality: 'UK', - racesCompleted: 80, - wins: 10, - podiums: 30, - isActive: true, - rank: 3, - }, - { - id: 'driver-4', - name: 'Alice Brown', - rating: 800.0, - skillLevel: 'beginner', - nationality: 'Germany', - racesCompleted: 60, - wins: 5, - podiums: 15, - isActive: true, - rank: 4, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].medalBg).toBe('bg-warning-amber'); - expect(result.drivers[0].medalColor).toBe('text-warning-amber'); - expect(result.drivers[1].medalBg).toBe('bg-gray-300'); - expect(result.drivers[1].medalColor).toBe('text-gray-300'); - expect(result.drivers[2].medalBg).toBe('bg-orange-700'); - expect(result.drivers[2].medalColor).toBe('text-orange-700'); - expect(result.drivers[3].medalBg).toBe('bg-gray-800'); - expect(result.drivers[3].medalColor).toBe('text-gray-400'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-123', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].name).toBe(driverDTOs[0].name); - expect(result.drivers[0].nationality).toBe(driverDTOs[0].nationality); - expect(result.drivers[0].avatarUrl).toBe(driverDTOs[0].avatarUrl); - expect(result.drivers[0].skillLevel).toBe(driverDTOs[0].skillLevel); - }); - - it('should not modify the input DTO', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-123', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ]; - - const originalDTO = JSON.parse(JSON.stringify(driverDTOs)); - DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(driverDTOs).toEqual(originalDTO); - }); - - it('should handle large numbers correctly', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 999999.99, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 10000, - wins: 2500, - podiums: 5000, - isActive: true, - rank: 1, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].rating).toBe(999999.99); - expect(result.drivers[0].wins).toBe(2500); - expect(result.drivers[0].podiums).toBe(5000); - expect(result.drivers[0].racesCompleted).toBe(10000); - expect(result.drivers[0].winRate).toBe('25.0'); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined avatar URLs', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: null as any, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].avatarUrl).toBe(''); - expect(result.podium[0].avatarUrl).toBe(''); - }); - - it('should handle null/undefined rating', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: null as any, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].rating).toBeNull(); - expect(result.podium[0].rating).toBeNull(); - }); - - it('should handle zero races completed for win rate calculation', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 0, - wins: 0, - podiums: 0, - isActive: true, - rank: 1, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].winRate).toBe('0.0'); - }); - - it('should handle rank 0', () => { - const driverDTOs: DriverLeaderboardItemDTO[] = [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 0, - }, - ]; - - const result = DriverRankingsViewDataBuilder.build(driverDTOs); - - expect(result.drivers[0].rank).toBe(0); - expect(result.drivers[0].medalBg).toBe('bg-gray-800'); - expect(result.drivers[0].medalColor).toBe('text-gray-400'); - }); - }); -}); - -describe('TeamRankingsViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform GetTeamsLeaderboardOutputDTO to TeamRankingsViewData correctly', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - { - id: 'team-3', - name: 'Rookie Racers', - tag: 'RR', - logoUrl: 'https://example.com/logo3.jpg', - memberCount: 5, - rating: 800, - totalWins: 5, - totalRaces: 50, - performanceLevel: 'intermediate', - isRecruiting: false, - createdAt: '2023-09-01', - }, - ], - recruitingCount: 5, - groupsBySkillLevel: 'elite,advanced,intermediate', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - ], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - // Verify teams - expect(result.teams).toHaveLength(3); - expect(result.teams[0].id).toBe('team-1'); - expect(result.teams[0].name).toBe('Racing Team Alpha'); - expect(result.teams[0].tag).toBe('RTA'); - expect(result.teams[0].memberCount).toBe(15); - expect(result.teams[0].totalWins).toBe(50); - expect(result.teams[0].totalRaces).toBe(200); - expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg'); - expect(result.teams[0].position).toBe(1); - expect(result.teams[0].isRecruiting).toBe(false); - expect(result.teams[0].performanceLevel).toBe('elite'); - expect(result.teams[0].rating).toBe(1500); - expect(result.teams[0].category).toBeUndefined(); - - // Verify podium (top 3) - expect(result.podium).toHaveLength(3); - expect(result.podium[0].id).toBe('team-1'); - expect(result.podium[0].position).toBe(1); - expect(result.podium[1].id).toBe('team-2'); - expect(result.podium[1].position).toBe(2); - expect(result.podium[2].id).toBe('team-3'); - expect(result.podium[2].position).toBe(3); - - // Verify recruiting count - expect(result.recruitingCount).toBe(5); - }); - - it('should handle empty team array', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams).toEqual([]); - expect(result.podium).toEqual([]); - expect(result.recruitingCount).toBe(0); - }); - - it('should handle less than 3 teams for podium', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - ], - recruitingCount: 2, - groupsBySkillLevel: 'elite,advanced', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams).toHaveLength(2); - expect(result.podium).toHaveLength(2); - expect(result.podium[0].position).toBe(1); - expect(result.podium[1].position).toBe(2); - }); - - it('should handle missing avatar URLs with empty string fallback', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].logoUrl).toBe(''); - }); - - it('should calculate position based on index', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, - { id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' }, - { id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' }, - { id: 'team-4', name: 'Team 4', tag: 'T4', memberCount: 4, totalWins: 5, totalRaces: 40, performanceLevel: 'beginner', isRecruiting: true, createdAt: '2023-04-01' }, - ], - recruitingCount: 2, - groupsBySkillLevel: 'elite,advanced,intermediate,beginner', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].position).toBe(1); - expect(result.teams[1].position).toBe(2); - expect(result.teams[2].position).toBe(3); - expect(result.teams[3].position).toBe(4); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-123', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 5, - groupsBySkillLevel: 'elite,advanced', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].name).toBe(teamDTO.teams[0].name); - expect(result.teams[0].tag).toBe(teamDTO.teams[0].tag); - expect(result.teams[0].logoUrl).toBe(teamDTO.teams[0].logoUrl); - expect(result.teams[0].memberCount).toBe(teamDTO.teams[0].memberCount); - expect(result.teams[0].rating).toBe(teamDTO.teams[0].rating); - expect(result.teams[0].totalWins).toBe(teamDTO.teams[0].totalWins); - expect(result.teams[0].totalRaces).toBe(teamDTO.teams[0].totalRaces); - expect(result.teams[0].performanceLevel).toBe(teamDTO.teams[0].performanceLevel); - expect(result.teams[0].isRecruiting).toBe(teamDTO.teams[0].isRecruiting); - }); - - it('should not modify the input DTO', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-123', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 5, - groupsBySkillLevel: 'elite,advanced', - topTeams: [], - }; - - const originalDTO = JSON.parse(JSON.stringify(teamDTO)); - TeamRankingsViewDataBuilder.build(teamDTO); - - expect(teamDTO).toEqual(originalDTO); - }); - - it('should handle large numbers correctly', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 100, - rating: 999999, - totalWins: 5000, - totalRaces: 10000, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].rating).toBe(999999); - expect(result.teams[0].totalWins).toBe(5000); - expect(result.teams[0].totalRaces).toBe(10000); - }); - }); - - describe('edge cases', () => { - it('should handle null/undefined logo URLs', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: null as any, - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].logoUrl).toBe(''); - }); - - it('should handle null/undefined rating', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - rating: null as any, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].rating).toBe(0); - }); - - it('should handle null/undefined totalWins and totalRaces', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: null as any, - totalRaces: null as any, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].totalWins).toBe(0); - expect(result.teams[0].totalRaces).toBe(0); - }); - - it('should handle empty performance level', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - memberCount: 15, - totalWins: 50, - totalRaces: 200, - performanceLevel: '', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].performanceLevel).toBe('N/A'); - }); - - it('should handle position 0', () => { - const teamDTO: GetTeamsLeaderboardOutputDTO = { - teams: [ - { id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }; - - const result = TeamRankingsViewDataBuilder.build(teamDTO); - - expect(result.teams[0].position).toBe(1); - }); - }); -}); - -describe('WinRateDisplay', () => { - describe('happy paths', () => { - it('should calculate win rate correctly', () => { - expect(WinRateDisplay.calculate(100, 25)).toBe('25.0'); - expect(WinRateDisplay.calculate(50, 10)).toBe('20.0'); - expect(WinRateDisplay.calculate(200, 50)).toBe('25.0'); - }); - - it('should handle zero races completed', () => { - expect(WinRateDisplay.calculate(0, 0)).toBe('0.0'); - expect(WinRateDisplay.calculate(0, 10)).toBe('0.0'); - }); - - it('should handle zero wins', () => { - expect(WinRateDisplay.calculate(100, 0)).toBe('0.0'); - }); - - it('should format rate correctly', () => { - expect(WinRateDisplay.format(25.0)).toBe('25.0%'); - expect(WinRateDisplay.format(0)).toBe('0.0%'); - expect(WinRateDisplay.format(100)).toBe('100.0%'); - }); - }); - - describe('edge cases', () => { - it('should handle null rate in format', () => { - expect(WinRateDisplay.format(null)).toBe('0.0%'); - }); - - it('should handle undefined rate in format', () => { - expect(WinRateDisplay.format(undefined)).toBe('0.0%'); - }); - - it('should handle decimal win rates', () => { - expect(WinRateDisplay.calculate(100, 25)).toBe('25.0'); - expect(WinRateDisplay.calculate(100, 33)).toBe('33.0'); - expect(WinRateDisplay.calculate(100, 66)).toBe('66.0'); - }); - - it('should handle large numbers', () => { - expect(WinRateDisplay.calculate(10000, 2500)).toBe('25.0'); - expect(WinRateDisplay.calculate(10000, 5000)).toBe('50.0'); - }); - }); -}); - -describe('MedalDisplay', () => { - describe('happy paths', () => { - it('should return correct variant for positions', () => { - expect(MedalDisplay.getVariant(1)).toBe('warning'); - expect(MedalDisplay.getVariant(2)).toBe('high'); - expect(MedalDisplay.getVariant(3)).toBe('warning'); - expect(MedalDisplay.getVariant(4)).toBe('low'); - expect(MedalDisplay.getVariant(10)).toBe('low'); - }); - - it('should return correct medal icon for top 3 positions', () => { - expect(MedalDisplay.getMedalIcon(1)).toBe('🏆'); - expect(MedalDisplay.getMedalIcon(2)).toBe('🏆'); - expect(MedalDisplay.getMedalIcon(3)).toBe('🏆'); - }); - - it('should return null for positions outside top 3', () => { - expect(MedalDisplay.getMedalIcon(4)).toBeNull(); - expect(MedalDisplay.getMedalIcon(10)).toBeNull(); - expect(MedalDisplay.getMedalIcon(100)).toBeNull(); - }); - - it('should return correct background color for positions', () => { - expect(MedalDisplay.getBg(1)).toBe('bg-warning-amber'); - expect(MedalDisplay.getBg(2)).toBe('bg-gray-300'); - expect(MedalDisplay.getBg(3)).toBe('bg-orange-700'); - expect(MedalDisplay.getBg(4)).toBe('bg-gray-800'); - expect(MedalDisplay.getBg(10)).toBe('bg-gray-800'); - }); - - it('should return correct text color for positions', () => { - expect(MedalDisplay.getColor(1)).toBe('text-warning-amber'); - expect(MedalDisplay.getColor(2)).toBe('text-gray-300'); - expect(MedalDisplay.getColor(3)).toBe('text-orange-700'); - expect(MedalDisplay.getColor(4)).toBe('text-gray-400'); - expect(MedalDisplay.getColor(10)).toBe('text-gray-400'); - }); - }); - - describe('edge cases', () => { - it('should handle position 0', () => { - expect(MedalDisplay.getVariant(0)).toBe('low'); - expect(MedalDisplay.getMedalIcon(0)).toBe('🏆'); - expect(MedalDisplay.getBg(0)).toBe('bg-gray-800'); - expect(MedalDisplay.getColor(0)).toBe('text-gray-400'); - }); - - it('should handle large positions', () => { - expect(MedalDisplay.getVariant(999)).toBe('low'); - expect(MedalDisplay.getMedalIcon(999)).toBeNull(); - expect(MedalDisplay.getBg(999)).toBe('bg-gray-800'); - expect(MedalDisplay.getColor(999)).toBe('text-gray-400'); - }); - - it('should handle negative positions', () => { - expect(MedalDisplay.getVariant(-1)).toBe('low'); - expect(MedalDisplay.getMedalIcon(-1)).toBe('🏆'); - expect(MedalDisplay.getBg(-1)).toBe('bg-gray-800'); - expect(MedalDisplay.getColor(-1)).toBe('text-gray-400'); - }); - }); -}); - -describe('Leaderboards View Data - Cross-Component Consistency', () => { - describe('common patterns', () => { - it('should all use consistent formatting for numeric values', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [], - recruitingCount: 5, - groupsBySkillLevel: 'elite,advanced', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // All numeric values should be preserved as numbers (not formatted as strings) - expect(typeof result.drivers[0].rating).toBe('number'); - expect(typeof result.drivers[0].wins).toBe('number'); - expect(typeof result.drivers[0].podiums).toBe('number'); - expect(typeof result.drivers[0].racesCompleted).toBe('number'); - expect(typeof result.drivers[0].rank).toBe('number'); - expect(typeof result.teams[0].rating).toBe('number'); - expect(typeof result.teams[0].totalWins).toBe('number'); - expect(typeof result.teams[0].totalRaces).toBe('number'); - }); - - it('should all handle missing data gracefully', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // All fields should have safe defaults - expect(result.drivers).toEqual([]); - expect(result.teams).toEqual([]); - }); - - it('should all preserve ISO timestamps for serialization', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01T00:00:00Z', - }, - ], - recruitingCount: 0, - groupsBySkillLevel: '', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01T00:00:00Z', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // Verify that the view data model is correctly built - expect(result.teams).toHaveLength(1); - expect(result.teams[0].id).toBe('team-1'); - expect(result.teams[0].name).toBe('Racing Team Alpha'); - expect(result.teams[0].tag).toBe('RTA'); - expect(result.teams[0].logoUrl).toBe('https://example.com/logo.jpg'); - expect(result.teams[0].memberCount).toBe(15); - expect(result.teams[0].rating).toBe(1500); - expect(result.teams[0].totalWins).toBe(50); - expect(result.teams[0].totalRaces).toBe(200); - expect(result.teams[0].performanceLevel).toBe('elite'); - expect(result.teams[0].isRecruiting).toBe(false); - expect(result.teams[0].position).toBe(1); - }); - - it('should all handle boolean flags correctly', () => { - const leaderboardsDTO = { - drivers: { - drivers: [], - totalRaces: 0, - totalWins: 0, - activeCount: 0, - }, - teams: { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: true, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: false, - createdAt: '2023-06-01', - }, - ], - recruitingCount: 1, - groupsBySkillLevel: 'elite,advanced', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: true, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: false, - createdAt: '2023-06-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - expect(result.teams[0].isRecruiting).toBe(true); - expect(result.teams[1].isRecruiting).toBe(false); - }); - }); - - describe('data integrity', () => { - it('should maintain data consistency across transformations', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 1234.56, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 150, - wins: 25, - podiums: 60, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar.jpg', - }, - ], - totalRaces: 150, - totalWins: 25, - activeCount: 1, - }, - teams: { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - recruitingCount: 5, - groupsBySkillLevel: 'elite,advanced', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // Verify derived fields match their source data - expect(result.drivers[0].position).toBe(result.drivers[0].rank); - expect(result.teams[0].position).toBe(1); - }); - - it('should handle complex real-world scenarios', () => { - const leaderboardsDTO = { - drivers: { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 2456.78, - skillLevel: 'pro', - nationality: 'USA', - racesCompleted: 250, - wins: 45, - podiums: 120, - isActive: true, - rank: 1, - avatarUrl: 'https://example.com/avatar1.jpg', - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 2100.0, - skillLevel: 'pro', - nationality: 'Canada', - racesCompleted: 200, - wins: 35, - podiums: 100, - isActive: true, - rank: 2, - avatarUrl: 'https://example.com/avatar2.jpg', - }, - { - id: 'driver-3', - name: 'Bob Johnson', - rating: 1800.0, - skillLevel: 'advanced', - nationality: 'UK', - racesCompleted: 180, - wins: 25, - podiums: 80, - isActive: true, - rank: 3, - avatarUrl: 'https://example.com/avatar3.jpg', - }, - ], - totalRaces: 630, - totalWins: 105, - activeCount: 3, - }, - teams: { - teams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - { - id: 'team-3', - name: 'Rookie Racers', - tag: 'RR', - logoUrl: 'https://example.com/logo3.jpg', - memberCount: 5, - rating: 800, - totalWins: 5, - totalRaces: 50, - performanceLevel: 'intermediate', - isRecruiting: false, - createdAt: '2023-09-01', - }, - ], - recruitingCount: 1, - groupsBySkillLevel: 'elite,advanced,intermediate', - topTeams: [ - { - id: 'team-1', - name: 'Racing Team Alpha', - tag: 'RTA', - logoUrl: 'https://example.com/logo1.jpg', - memberCount: 15, - rating: 1500, - totalWins: 50, - totalRaces: 200, - performanceLevel: 'elite', - isRecruiting: false, - createdAt: '2023-01-01', - }, - { - id: 'team-2', - name: 'Speed Demons', - tag: 'SD', - logoUrl: 'https://example.com/logo2.jpg', - memberCount: 8, - rating: 1200, - totalWins: 20, - totalRaces: 150, - performanceLevel: 'advanced', - isRecruiting: true, - createdAt: '2023-06-01', - }, - { - id: 'team-3', - name: 'Rookie Racers', - tag: 'RR', - logoUrl: 'https://example.com/logo3.jpg', - memberCount: 5, - rating: 800, - totalWins: 5, - totalRaces: 50, - performanceLevel: 'intermediate', - isRecruiting: false, - createdAt: '2023-09-01', - }, - ], - }, - }; - - const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO); - - // Verify all transformations - expect(result.drivers).toHaveLength(3); - expect(result.drivers[0].name).toBe('John Doe'); - expect(result.drivers[0].rating).toBe(2456.78); - expect(result.drivers[0].rank).toBe(1); - expect(result.drivers[0].position).toBe(1); - - expect(result.teams).toHaveLength(3); - expect(result.teams[0].name).toBe('Racing Team Alpha'); - expect(result.teams[0].rating).toBe(1500); - expect(result.teams[0].position).toBe(1); - expect(result.teams[0].isRecruiting).toBe(false); - - expect(result.teams[1].isRecruiting).toBe(true); - expect(result.teams[2].isRecruiting).toBe(false); - }); - }); -}); diff --git a/apps/website/tests/view-data/leagues.test.ts b/apps/website/tests/view-data/leagues.test.ts deleted file mode 100644 index 8339163e0..000000000 --- a/apps/website/tests/view-data/leagues.test.ts +++ /dev/null @@ -1,1885 +0,0 @@ -/** - * View Data Layer Tests - Leagues Functionality - * - * This test file covers the view data layer for leagues functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - League list data transformation and sorting - * - Individual league profile view models - * - League roster data formatting and member management - * - League schedule and standings view models - * - League stewarding and protest handling data transformation - * - League wallet and sponsorship data formatting - * - League creation and migration data transformation - * - Derived league fields (member counts, status, permissions, etc.) - * - Default values and fallbacks for league views - * - League-specific formatting (dates, points, positions, race formats, etc.) - * - Data grouping and categorization for league components - * - League search and filtering view models - * - Real-time league data updates and state management - */ - -import { LeaguesViewDataBuilder } from '@/lib/builders/view-data/LeaguesViewDataBuilder'; -import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder'; -import { LeagueRosterAdminViewDataBuilder } from '@/lib/builders/view-data/LeagueRosterAdminViewDataBuilder'; -import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder'; -import { LeagueStandingsViewDataBuilder } from '@/lib/builders/view-data/LeagueStandingsViewDataBuilder'; -import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO'; -import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; -import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; -import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; -import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO'; -import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO'; -import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO'; -import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; -import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; -import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; - -describe('LeaguesViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform AllLeaguesWithCapacityAndScoringDTO to LeaguesViewData correctly', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Pro League', - description: 'A competitive league for experienced drivers', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo • 32 max', - }, - usedSlots: 25, - category: 'competitive', - scoring: { - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Weekly races on Sundays', - logoUrl: 'https://example.com/logo.png', - pendingJoinRequestsCount: 3, - pendingProtestsCount: 1, - walletBalance: 1000, - }, - { - id: 'league-2', - name: 'Rookie League', - description: null, - ownerId: 'owner-2', - createdAt: '2024-02-01T00:00:00.000Z', - settings: { - maxDrivers: 16, - qualifyingFormat: 'Solo • 16 max', - }, - usedSlots: 10, - category: 'rookie', - scoring: { - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-2', - scoringPresetName: 'Rookie', - dropPolicySummary: 'No drops', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Bi-weekly races', - logoUrl: null, - pendingJoinRequestsCount: 0, - pendingProtestsCount: 0, - walletBalance: 0, - }, - ], - totalCount: 2, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues).toHaveLength(2); - expect(result.leagues[0]).toEqual({ - id: 'league-1', - name: 'Pro League', - description: 'A competitive league for experienced drivers', - logoUrl: 'https://example.com/logo.png', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - maxDrivers: 32, - usedDriverSlots: 25, - activeDriversCount: undefined, - nextRaceAt: undefined, - maxTeams: undefined, - usedTeamSlots: undefined, - structureSummary: 'Solo • 32 max', - timingSummary: 'Weekly races on Sundays', - category: 'competitive', - scoring: { - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - }); - expect(result.leagues[1]).toEqual({ - id: 'league-2', - name: 'Rookie League', - description: null, - logoUrl: null, - ownerId: 'owner-2', - createdAt: '2024-02-01T00:00:00.000Z', - maxDrivers: 16, - usedDriverSlots: 10, - activeDriversCount: undefined, - nextRaceAt: undefined, - maxTeams: undefined, - usedTeamSlots: undefined, - structureSummary: 'Solo • 16 max', - timingSummary: 'Bi-weekly races', - category: 'rookie', - scoring: { - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-2', - scoringPresetName: 'Rookie', - dropPolicySummary: 'No drops', - scoringPatternSummary: 'Points based on finish position', - }, - }); - }); - - it('should handle empty leagues list', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [], - totalCount: 0, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues).toHaveLength(0); - }); - - it('should handle leagues with missing optional fields', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Minimal League', - description: '', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 20, - }, - usedSlots: 5, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].description).toBe(null); - expect(result.leagues[0].logoUrl).toBe(null); - expect(result.leagues[0].category).toBe(null); - expect(result.leagues[0].scoring).toBeUndefined(); - expect(result.leagues[0].timingSummary).toBe(''); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo • 32 max', - }, - usedSlots: 20, - category: 'test', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Test Type', - scoringPresetId: 'preset-1', - scoringPresetName: 'Test Preset', - dropPolicySummary: 'Test drop policy', - scoringPatternSummary: 'Test pattern', - }, - timingSummary: 'Test timing', - logoUrl: 'https://example.com/test.png', - pendingJoinRequestsCount: 5, - pendingProtestsCount: 2, - walletBalance: 500, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].id).toBe(leaguesDTO.leagues[0].id); - expect(result.leagues[0].name).toBe(leaguesDTO.leagues[0].name); - expect(result.leagues[0].description).toBe(leaguesDTO.leagues[0].description); - expect(result.leagues[0].logoUrl).toBe(leaguesDTO.leagues[0].logoUrl); - expect(result.leagues[0].ownerId).toBe(leaguesDTO.leagues[0].ownerId); - expect(result.leagues[0].createdAt).toBe(leaguesDTO.leagues[0].createdAt); - expect(result.leagues[0].maxDrivers).toBe(leaguesDTO.leagues[0].settings.maxDrivers); - expect(result.leagues[0].usedDriverSlots).toBe(leaguesDTO.leagues[0].usedSlots); - expect(result.leagues[0].structureSummary).toBe(leaguesDTO.leagues[0].settings.qualifyingFormat); - expect(result.leagues[0].timingSummary).toBe(leaguesDTO.leagues[0].timingSummary); - expect(result.leagues[0].category).toBe(leaguesDTO.leagues[0].category); - expect(result.leagues[0].scoring).toEqual(leaguesDTO.leagues[0].scoring); - }); - - it('should not modify the input DTO', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo • 32 max', - }, - usedSlots: 20, - category: 'test', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Test Type', - scoringPresetId: 'preset-1', - scoringPresetName: 'Test Preset', - dropPolicySummary: 'Test drop policy', - scoringPatternSummary: 'Test pattern', - }, - timingSummary: 'Test timing', - logoUrl: 'https://example.com/test.png', - pendingJoinRequestsCount: 5, - pendingProtestsCount: 2, - walletBalance: 500, - }, - ], - totalCount: 1, - }; - - const originalDTO = JSON.parse(JSON.stringify(leaguesDTO)); - LeaguesViewDataBuilder.build(leaguesDTO); - - expect(leaguesDTO).toEqual(originalDTO); - }); - }); - - describe('edge cases', () => { - it('should handle leagues with very long descriptions', () => { - const longDescription = 'A'.repeat(1000); - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Test League', - description: longDescription, - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 20, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].description).toBe(longDescription); - }); - - it('should handle leagues with special characters in name', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'League & Co. (2024)', - description: 'Test league', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 20, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].name).toBe('League & Co. (2024)'); - }); - - it('should handle leagues with zero used slots', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Empty League', - description: 'No members yet', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 0, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].usedDriverSlots).toBe(0); - }); - - it('should handle leagues with maximum capacity', () => { - const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = { - leagues: [ - { - id: 'league-1', - name: 'Full League', - description: 'At maximum capacity', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 32, - }, - ], - totalCount: 1, - }; - - const result = LeaguesViewDataBuilder.build(leaguesDTO); - - expect(result.leagues[0].usedDriverSlots).toBe(32); - expect(result.leagues[0].maxDrivers).toBe(32); - }); - }); -}); - -describe('LeagueDetailViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform league DTOs to LeagueDetailViewData correctly', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Pro League', - description: 'A competitive league for experienced drivers', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo • 32 max', - }, - usedSlots: 25, - category: 'competitive', - scoring: { - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - }, - timingSummary: 'Weekly races on Sundays', - logoUrl: 'https://example.com/logo.png', - pendingJoinRequestsCount: 3, - pendingProtestsCount: 1, - walletBalance: 1000, - }; - - const owner: GetDriverOutputDTO = { - id: 'owner-1', - name: 'John Doe', - iracingId: '12345', - country: 'USA', - bio: 'Experienced driver', - joinedAt: '2023-01-01T00:00:00.000Z', - avatarUrl: 'https://example.com/avatar.jpg', - }; - - const scoringConfig: LeagueScoringConfigDTO = { - id: 'config-1', - leagueId: 'league-1', - gameId: 'game-1', - gameName: 'iRacing', - primaryChampionshipType: 'Single Championship', - scoringPresetId: 'preset-1', - scoringPresetName: 'Standard', - dropPolicySummary: 'Drop 2 worst races', - scoringPatternSummary: 'Points based on finish position', - dropRaces: 2, - pointsPerRace: 100, - pointsForWin: 25, - pointsForPodium: [20, 15, 10], - }; - - const memberships: LeagueMembershipsDTO = { - members: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Bob', - iracingId: '22222', - country: 'Germany', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - role: 'steward', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - { - driverId: 'driver-3', - driver: { - id: 'driver-3', - name: 'Charlie', - iracingId: '33333', - country: 'France', - joinedAt: '2023-08-01T00:00:00.000Z', - }, - role: 'member', - joinedAt: '2023-08-01T00:00:00.000Z', - }, - ], - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-01-15T14:00:00.000Z', - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - strengthOfField: 1500, - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-01-22T14:00:00.000Z', - track: 'Monza', - car: 'Ferrari 488 GT3', - sessionType: 'race', - strengthOfField: 1600, - }, - ]; - - const sponsors: any[] = [ - { - id: 'sponsor-1', - name: 'Sponsor A', - tier: 'main', - logoUrl: 'https://example.com/sponsor-a.png', - websiteUrl: 'https://sponsor-a.com', - tagline: 'Premium racing gear', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner, - scoringConfig, - memberships, - races, - sponsors, - }); - - expect(result.leagueId).toBe('league-1'); - expect(result.name).toBe('Pro League'); - expect(result.description).toBe('A competitive league for experienced drivers'); - expect(result.logoUrl).toBe('https://example.com/logo.png'); - expect(result.info.name).toBe('Pro League'); - expect(result.info.description).toBe('A competitive league for experienced drivers'); - expect(result.info.membersCount).toBe(3); - expect(result.info.racesCount).toBe(2); - expect(result.info.avgSOF).toBe(1550); - expect(result.info.structure).toBe('Solo • 32 max'); - expect(result.info.scoring).toBe('preset-1'); - expect(result.info.createdAt).toBe('2024-01-01T00:00:00.000Z'); - expect(result.info.discordUrl).toBeUndefined(); - expect(result.info.youtubeUrl).toBeUndefined(); - expect(result.info.websiteUrl).toBeUndefined(); - expect(result.ownerSummary).not.toBeNull(); - expect(result.ownerSummary?.driverId).toBe('owner-1'); - expect(result.ownerSummary?.driverName).toBe('John Doe'); - expect(result.ownerSummary?.avatarUrl).toBe('https://example.com/avatar.jpg'); - expect(result.ownerSummary?.roleBadgeText).toBe('Owner'); - expect(result.adminSummaries).toHaveLength(1); - expect(result.adminSummaries[0].driverId).toBe('driver-1'); - expect(result.adminSummaries[0].driverName).toBe('Alice'); - expect(result.adminSummaries[0].roleBadgeText).toBe('Admin'); - expect(result.stewardSummaries).toHaveLength(1); - expect(result.stewardSummaries[0].driverId).toBe('driver-2'); - expect(result.stewardSummaries[0].driverName).toBe('Bob'); - expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward'); - expect(result.memberSummaries).toHaveLength(1); - expect(result.memberSummaries[0].driverId).toBe('driver-3'); - expect(result.memberSummaries[0].driverName).toBe('Charlie'); - expect(result.memberSummaries[0].roleBadgeText).toBe('Member'); - expect(result.sponsors).toHaveLength(1); - expect(result.sponsors[0].id).toBe('sponsor-1'); - expect(result.sponsors[0].name).toBe('Sponsor A'); - expect(result.sponsors[0].tier).toBe('main'); - expect(result.walletBalance).toBe(1000); - expect(result.pendingProtestsCount).toBe(1); - expect(result.pendingJoinRequestsCount).toBe(3); - }); - - it('should handle league with no owner', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(result.ownerSummary).toBeNull(); - }); - - it('should handle league with no scoring config', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(result.info.scoring).toBe('Standard'); - }); - - it('should handle league with no races', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(result.info.racesCount).toBe(0); - expect(result.info.avgSOF).toBeNull(); - expect(result.runningRaces).toEqual([]); - expect(result.nextRace).toBeUndefined(); - expect(result.seasonProgress).toEqual({ - completedRaces: 0, - totalRaces: 0, - percentage: 0, - }); - expect(result.recentResults).toEqual([]); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - qualifyingFormat: 'Solo • 32 max', - }, - usedSlots: 20, - category: 'test', - scoring: { - gameId: 'game-1', - gameName: 'Test Game', - primaryChampionshipType: 'Test Type', - scoringPresetId: 'preset-1', - scoringPresetName: 'Test Preset', - dropPolicySummary: 'Test drop policy', - scoringPatternSummary: 'Test pattern', - }, - timingSummary: 'Test timing', - logoUrl: 'https://example.com/test.png', - pendingJoinRequestsCount: 5, - pendingProtestsCount: 2, - walletBalance: 500, - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(result.leagueId).toBe(league.id); - expect(result.name).toBe(league.name); - expect(result.description).toBe(league.description); - expect(result.logoUrl).toBe(league.logoUrl); - expect(result.walletBalance).toBe(league.walletBalance); - expect(result.pendingProtestsCount).toBe(league.pendingProtestsCount); - expect(result.pendingJoinRequestsCount).toBe(league.pendingJoinRequestsCount); - }); - - it('should not modify the input DTOs', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 20, - }; - - const originalLeague = JSON.parse(JSON.stringify(league)); - LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(league).toEqual(originalLeague); - }); - }); - - describe('edge cases', () => { - it('should handle league with missing optional fields', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Minimal League', - description: '', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races: [], - sponsors: [], - }); - - expect(result.description).toBe(''); - expect(result.logoUrl).toBeUndefined(); - expect(result.info.description).toBe(''); - expect(result.info.discordUrl).toBeUndefined(); - expect(result.info.youtubeUrl).toBeUndefined(); - expect(result.info.websiteUrl).toBeUndefined(); - }); - - it('should handle races with missing strengthOfField', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-01-15T14:00:00.000Z', - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races, - sponsors: [], - }); - - expect(result.info.avgSOF).toBeNull(); - }); - - it('should handle races with zero strengthOfField', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-01-15T14:00:00.000Z', - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - strengthOfField: 0, - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races, - sponsors: [], - }); - - expect(result.info.avgSOF).toBeNull(); - }); - - it('should handle races with different dates for next race calculation', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now - - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const races: RaceDTO[] = [ - { - id: 'race-1', - name: 'Past Race', - date: pastDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - { - id: 'race-2', - name: 'Future Race', - date: futureDate.toISOString(), - track: 'Monza', - car: 'Ferrari 488 GT3', - sessionType: 'race', - }, - ]; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships: { members: [] }, - races, - sponsors: [], - }); - - expect(result.nextRace).toBeDefined(); - expect(result.nextRace?.id).toBe('race-2'); - expect(result.nextRace?.name).toBe('Future Race'); - expect(result.seasonProgress.completedRaces).toBe(1); - expect(result.seasonProgress.totalRaces).toBe(2); - expect(result.seasonProgress.percentage).toBe(50); - expect(result.recentResults).toHaveLength(1); - expect(result.recentResults[0].raceId).toBe('race-1'); - }); - - it('should handle members with different roles', () => { - const league: LeagueWithCapacityAndScoringDTO = { - id: 'league-1', - name: 'Test League', - description: 'Test description', - ownerId: 'owner-1', - createdAt: '2024-01-01T00:00:00.000Z', - settings: { - maxDrivers: 32, - }, - usedSlots: 10, - }; - - const memberships: LeagueMembershipsDTO = { - members: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Admin', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Steward', - iracingId: '22222', - country: 'Germany', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - role: 'steward', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - { - driverId: 'driver-3', - driver: { - id: 'driver-3', - name: 'Member', - iracingId: '33333', - country: 'France', - joinedAt: '2023-08-01T00:00:00.000Z', - }, - role: 'member', - joinedAt: '2023-08-01T00:00:00.000Z', - }, - ], - }; - - const result = LeagueDetailViewDataBuilder.build({ - league, - owner: null, - scoringConfig: null, - memberships, - races: [], - sponsors: [], - }); - - expect(result.adminSummaries).toHaveLength(1); - expect(result.stewardSummaries).toHaveLength(1); - expect(result.memberSummaries).toHaveLength(1); - expect(result.info.membersCount).toBe(3); - }); - }); -}); - -describe('LeagueRosterAdminViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform roster DTOs to LeagueRosterAdminViewData correctly', () => { - const members: LeagueRosterMemberDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Bob', - iracingId: '22222', - country: 'Germany', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - role: 'member', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - ]; - - const joinRequests: LeagueRosterJoinRequestDTO[] = [ - { - id: 'request-1', - leagueId: 'league-1', - driverId: 'driver-3', - requestedAt: '2024-01-15T10:00:00.000Z', - message: 'I would like to join this league', - driver: {}, - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members, - joinRequests, - }); - - expect(result.leagueId).toBe('league-1'); - expect(result.members).toHaveLength(2); - expect(result.members[0].driverId).toBe('driver-1'); - expect(result.members[0].driver.id).toBe('driver-1'); - expect(result.members[0].driver.name).toBe('Alice'); - expect(result.members[0].role).toBe('admin'); - expect(result.members[0].joinedAt).toBe('2023-06-01T00:00:00.000Z'); - expect(result.members[0].formattedJoinedAt).toBeDefined(); - expect(result.members[1].driverId).toBe('driver-2'); - expect(result.members[1].driver.id).toBe('driver-2'); - expect(result.members[1].driver.name).toBe('Bob'); - expect(result.members[1].role).toBe('member'); - expect(result.members[1].joinedAt).toBe('2023-07-01T00:00:00.000Z'); - expect(result.members[1].formattedJoinedAt).toBeDefined(); - expect(result.joinRequests).toHaveLength(1); - expect(result.joinRequests[0].id).toBe('request-1'); - expect(result.joinRequests[0].driver.id).toBe('driver-3'); - expect(result.joinRequests[0].driver.name).toBe('Unknown Driver'); - expect(result.joinRequests[0].requestedAt).toBe('2024-01-15T10:00:00.000Z'); - expect(result.joinRequests[0].formattedRequestedAt).toBeDefined(); - expect(result.joinRequests[0].message).toBe('I would like to join this league'); - }); - - it('should handle empty members and join requests', () => { - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members: [], - joinRequests: [], - }); - - expect(result.leagueId).toBe('league-1'); - expect(result.members).toHaveLength(0); - expect(result.joinRequests).toHaveLength(0); - }); - - it('should handle members without driver details', () => { - const members: LeagueRosterMemberDTO[] = [ - { - driverId: 'driver-1', - driver: undefined as any, - role: 'member', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members, - joinRequests: [], - }); - - expect(result.members[0].driver.name).toBe('Unknown Driver'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const members: LeagueRosterMemberDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - ]; - - const joinRequests: LeagueRosterJoinRequestDTO[] = [ - { - id: 'request-1', - leagueId: 'league-1', - driverId: 'driver-3', - requestedAt: '2024-01-15T10:00:00.000Z', - message: 'I would like to join this league', - driver: {}, - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members, - joinRequests, - }); - - expect(result.leagueId).toBe('league-1'); - expect(result.members[0].driverId).toBe(members[0].driverId); - expect(result.members[0].driver.id).toBe(members[0].driver.id); - expect(result.members[0].driver.name).toBe(members[0].driver.name); - expect(result.members[0].role).toBe(members[0].role); - expect(result.members[0].joinedAt).toBe(members[0].joinedAt); - expect(result.joinRequests[0].id).toBe(joinRequests[0].id); - expect(result.joinRequests[0].requestedAt).toBe(joinRequests[0].requestedAt); - expect(result.joinRequests[0].message).toBe(joinRequests[0].message); - }); - - it('should not modify the input DTOs', () => { - const members: LeagueRosterMemberDTO[] = [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - ]; - - const joinRequests: LeagueRosterJoinRequestDTO[] = [ - { - id: 'request-1', - leagueId: 'league-1', - driverId: 'driver-3', - requestedAt: '2024-01-15T10:00:00.000Z', - message: 'I would like to join this league', - driver: {}, - }, - ]; - - const originalMembers = JSON.parse(JSON.stringify(members)); - const originalRequests = JSON.parse(JSON.stringify(joinRequests)); - - LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members, - joinRequests, - }); - - expect(members).toEqual(originalMembers); - expect(joinRequests).toEqual(originalRequests); - }); - }); - - describe('edge cases', () => { - it('should handle members with missing driver field', () => { - const members: LeagueRosterMemberDTO[] = [ - { - driverId: 'driver-1', - driver: undefined as any, - role: 'member', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members, - joinRequests: [], - }); - - expect(result.members[0].driver.name).toBe('Unknown Driver'); - }); - - it('should handle join requests with missing driver field', () => { - const joinRequests: LeagueRosterJoinRequestDTO[] = [ - { - id: 'request-1', - leagueId: 'league-1', - driverId: 'driver-3', - requestedAt: '2024-01-15T10:00:00.000Z', - message: 'I would like to join this league', - driver: undefined, - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members: [], - joinRequests, - }); - - expect(result.joinRequests[0].driver.name).toBe('Unknown Driver'); - }); - - it('should handle join requests without message', () => { - const joinRequests: LeagueRosterJoinRequestDTO[] = [ - { - id: 'request-1', - leagueId: 'league-1', - driverId: 'driver-3', - requestedAt: '2024-01-15T10:00:00.000Z', - driver: {}, - }, - ]; - - const result = LeagueRosterAdminViewDataBuilder.build({ - leagueId: 'league-1', - members: [], - joinRequests, - }); - - expect(result.joinRequests[0].message).toBeUndefined(); - }); - }); -}); - -describe('LeagueScheduleViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform schedule DTO to LeagueScheduleViewData correctly', () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Past Race', - date: pastDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - { - id: 'race-2', - name: 'Future Race', - date: futureDate.toISOString(), - track: 'Monza', - car: 'Ferrari 488 GT3', - sessionType: 'race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', true); - - expect(result.leagueId).toBe('league-1'); - expect(result.races).toHaveLength(2); - expect(result.races[0].id).toBe('race-1'); - expect(result.races[0].name).toBe('Past Race'); - expect(result.races[0].scheduledAt).toBe(pastDate.toISOString()); - expect(result.races[0].track).toBe('Spa'); - expect(result.races[0].car).toBe('Porsche 911 GT3'); - expect(result.races[0].sessionType).toBe('race'); - expect(result.races[0].isPast).toBe(true); - expect(result.races[0].isUpcoming).toBe(false); - expect(result.races[0].status).toBe('completed'); - expect(result.races[0].isUserRegistered).toBe(false); - expect(result.races[0].canRegister).toBe(false); - expect(result.races[0].canEdit).toBe(true); - expect(result.races[0].canReschedule).toBe(true); - expect(result.races[1].id).toBe('race-2'); - expect(result.races[1].name).toBe('Future Race'); - expect(result.races[1].scheduledAt).toBe(futureDate.toISOString()); - expect(result.races[1].track).toBe('Monza'); - expect(result.races[1].car).toBe('Ferrari 488 GT3'); - expect(result.races[1].sessionType).toBe('race'); - expect(result.races[1].isPast).toBe(false); - expect(result.races[1].isUpcoming).toBe(true); - expect(result.races[1].status).toBe('scheduled'); - expect(result.races[1].isUserRegistered).toBe(false); - expect(result.races[1].canRegister).toBe(true); - expect(result.races[1].canEdit).toBe(true); - expect(result.races[1].canReschedule).toBe(true); - expect(result.currentDriverId).toBe('driver-1'); - expect(result.isAdmin).toBe(true); - }); - - it('should handle empty races list', () => { - const apiDto = { - leagueId: 'league-1', - races: [], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.leagueId).toBe('league-1'); - expect(result.races).toHaveLength(0); - }); - - it('should handle non-admin user', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Future Race', - date: futureDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', false); - - expect(result.races[0].canEdit).toBe(false); - expect(result.races[0].canReschedule).toBe(false); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Test Race', - date: futureDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.leagueId).toBe(apiDto.leagueId); - expect(result.races[0].id).toBe(apiDto.races[0].id); - expect(result.races[0].name).toBe(apiDto.races[0].name); - expect(result.races[0].scheduledAt).toBe(apiDto.races[0].date); - expect(result.races[0].track).toBe(apiDto.races[0].track); - expect(result.races[0].car).toBe(apiDto.races[0].car); - expect(result.races[0].sessionType).toBe(apiDto.races[0].sessionType); - }); - - it('should not modify the input DTO', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Test Race', - date: futureDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ], - }; - - const originalDto = JSON.parse(JSON.stringify(apiDto)); - LeagueScheduleViewDataBuilder.build(apiDto); - - expect(apiDto).toEqual(originalDto); - }); - }); - - describe('edge cases', () => { - it('should handle races with missing optional fields', () => { - const now = new Date(); - const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Test Race', - date: futureDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - expect(result.races[0].track).toBe('Spa'); - expect(result.races[0].car).toBe('Porsche 911 GT3'); - expect(result.races[0].sessionType).toBe('race'); - }); - - it('should handle races at exactly the current time', () => { - const now = new Date(); - const currentRaceDate = new Date(now.getTime()); - - const apiDto = { - leagueId: 'league-1', - races: [ - { - id: 'race-1', - name: 'Current Race', - date: currentRaceDate.toISOString(), - track: 'Spa', - car: 'Porsche 911 GT3', - sessionType: 'race', - }, - ], - }; - - const result = LeagueScheduleViewDataBuilder.build(apiDto); - - // Race at current time should be considered past - expect(result.races[0].isPast).toBe(true); - expect(result.races[0].isUpcoming).toBe(false); - expect(result.races[0].status).toBe('completed'); - }); - }); -}); - -describe('LeagueStandingsViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform standings DTOs to LeagueStandingsViewData correctly', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: ['race-1', 'race-2'], - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Bob', - iracingId: '22222', - country: 'Germany', - }, - points: 1100, - position: 2, - wins: 3, - podiums: 8, - races: 15, - positionChange: -1, - lastRacePoints: 15, - droppedRaceIds: [], - }, - ], - }; - - const membershipsDto = { - members: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'member', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - { - driverId: 'driver-2', - driver: { - id: 'driver-2', - name: 'Bob', - iracingId: '22222', - country: 'Germany', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - role: 'member', - joinedAt: '2023-07-01T00:00:00.000Z', - }, - ], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.leagueId).toBe('league-1'); - expect(result.isTeamChampionship).toBe(false); - expect(result.currentDriverId).toBeNull(); - expect(result.isAdmin).toBe(false); - expect(result.standings).toHaveLength(2); - expect(result.standings[0].driverId).toBe('driver-1'); - expect(result.standings[0].position).toBe(1); - expect(result.standings[0].totalPoints).toBe(1250); - expect(result.standings[0].racesFinished).toBe(15); - expect(result.standings[0].racesStarted).toBe(15); - expect(result.standings[0].avgFinish).toBeNull(); - expect(result.standings[0].penaltyPoints).toBe(0); - expect(result.standings[0].bonusPoints).toBe(0); - expect(result.standings[0].positionChange).toBe(2); - expect(result.standings[0].lastRacePoints).toBe(25); - expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']); - expect(result.standings[0].wins).toBe(5); - expect(result.standings[0].podiums).toBe(10); - expect(result.standings[1].driverId).toBe('driver-2'); - expect(result.standings[1].position).toBe(2); - expect(result.standings[1].totalPoints).toBe(1100); - expect(result.standings[1].racesFinished).toBe(15); - expect(result.standings[1].racesStarted).toBe(15); - expect(result.standings[1].avgFinish).toBeNull(); - expect(result.standings[1].penaltyPoints).toBe(0); - expect(result.standings[1].bonusPoints).toBe(0); - expect(result.standings[1].positionChange).toBe(-1); - expect(result.standings[1].lastRacePoints).toBe(15); - expect(result.standings[1].droppedRaceIds).toEqual([]); - expect(result.standings[1].wins).toBe(3); - expect(result.standings[1].podiums).toBe(8); - expect(result.drivers).toHaveLength(2); - expect(result.drivers[0].id).toBe('driver-1'); - expect(result.drivers[0].name).toBe('Alice'); - expect(result.drivers[0].iracingId).toBe('11111'); - expect(result.drivers[0].country).toBe('UK'); - expect(result.drivers[0].avatarUrl).toBeNull(); - expect(result.drivers[1].id).toBe('driver-2'); - expect(result.drivers[1].name).toBe('Bob'); - expect(result.drivers[1].iracingId).toBe('22222'); - expect(result.drivers[1].country).toBe('Germany'); - expect(result.drivers[1].avatarUrl).toBeNull(); - expect(result.memberships).toHaveLength(2); - expect(result.memberships[0].driverId).toBe('driver-1'); - expect(result.memberships[0].leagueId).toBe('league-1'); - expect(result.memberships[0].role).toBe('member'); - expect(result.memberships[0].joinedAt).toBe('2023-06-01T00:00:00.000Z'); - expect(result.memberships[0].status).toBe('active'); - expect(result.memberships[1].driverId).toBe('driver-2'); - expect(result.memberships[1].leagueId).toBe('league-1'); - expect(result.memberships[1].role).toBe('member'); - expect(result.memberships[1].joinedAt).toBe('2023-07-01T00:00:00.000Z'); - expect(result.memberships[1].status).toBe('active'); - }); - - it('should handle empty standings and memberships', () => { - const standingsDto = { - standings: [], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.standings).toHaveLength(0); - expect(result.drivers).toHaveLength(0); - expect(result.memberships).toHaveLength(0); - }); - - it('should handle team championship mode', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - true - ); - - expect(result.isTeamChampionship).toBe(true); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: ['race-1'], - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.standings[0].driverId).toBe(standingsDto.standings[0].driverId); - expect(result.standings[0].position).toBe(standingsDto.standings[0].position); - expect(result.standings[0].totalPoints).toBe(standingsDto.standings[0].points); - expect(result.standings[0].racesFinished).toBe(standingsDto.standings[0].races); - expect(result.standings[0].racesStarted).toBe(standingsDto.standings[0].races); - expect(result.standings[0].positionChange).toBe(standingsDto.standings[0].positionChange); - expect(result.standings[0].lastRacePoints).toBe(standingsDto.standings[0].lastRacePoints); - expect(result.standings[0].droppedRaceIds).toEqual(standingsDto.standings[0].droppedRaceIds); - expect(result.standings[0].wins).toBe(standingsDto.standings[0].wins); - expect(result.standings[0].podiums).toBe(standingsDto.standings[0].podiums); - expect(result.drivers[0].id).toBe(standingsDto.standings[0].driver.id); - expect(result.drivers[0].name).toBe(standingsDto.standings[0].driver.name); - expect(result.drivers[0].iracingId).toBe(standingsDto.standings[0].driver.iracingId); - expect(result.drivers[0].country).toBe(standingsDto.standings[0].driver.country); - }); - - it('should not modify the input DTOs', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: ['race-1'], - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const originalStandings = JSON.parse(JSON.stringify(standingsDto)); - const originalMemberships = JSON.parse(JSON.stringify(membershipsDto)); - - LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(standingsDto).toEqual(originalStandings); - expect(membershipsDto).toEqual(originalMemberships); - }); - }); - - describe('edge cases', () => { - it('should handle standings with missing optional fields', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.standings[0].positionChange).toBe(0); - expect(result.standings[0].lastRacePoints).toBe(0); - expect(result.standings[0].droppedRaceIds).toEqual([]); - }); - - it('should handle standings with missing driver field', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: undefined as any, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.drivers).toHaveLength(0); - }); - - it('should handle duplicate drivers in standings', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: [], - }, - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1100, - position: 2, - wins: 3, - podiums: 8, - races: 15, - positionChange: -1, - lastRacePoints: 15, - droppedRaceIds: [], - }, - ], - }; - - const membershipsDto = { - members: [], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - // Should only have one driver entry - expect(result.drivers).toHaveLength(1); - expect(result.drivers[0].id).toBe('driver-1'); - }); - - it('should handle members with different roles', () => { - const standingsDto = { - standings: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - }, - points: 1250, - position: 1, - wins: 5, - podiums: 10, - races: 15, - positionChange: 2, - lastRacePoints: 25, - droppedRaceIds: [], - }, - ], - }; - - const membershipsDto = { - members: [ - { - driverId: 'driver-1', - driver: { - id: 'driver-1', - name: 'Alice', - iracingId: '11111', - country: 'UK', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - role: 'admin', - joinedAt: '2023-06-01T00:00:00.000Z', - }, - ], - }; - - const result = LeagueStandingsViewDataBuilder.build( - standingsDto, - membershipsDto, - 'league-1', - false - ); - - expect(result.memberships[0].role).toBe('admin'); - }); - }); -}); diff --git a/apps/website/tests/view-data/media.test.ts b/apps/website/tests/view-data/media.test.ts deleted file mode 100644 index ad3c99fb5..000000000 --- a/apps/website/tests/view-data/media.test.ts +++ /dev/null @@ -1,1189 +0,0 @@ -/** - * View Data Layer Tests - Media Functionality - * - * This test file covers the view data layer for media functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Avatar page data transformation and display - * - Avatar route data handling for driver-specific avatars - * - Category icon data mapping and formatting - * - League cover and logo data transformation - * - Sponsor logo data handling and display - * - Team logo data mapping and validation - * - Track image data transformation and UI state - * - Media upload and validation view models - * - Media deletion confirmation and state management - * - Derived media fields (file size, format, dimensions, etc.) - * - Default values and fallbacks for media views - * - Media-specific formatting (image optimization, aspect ratios, etc.) - * - Media access control and permission view models - */ - -import { AvatarViewDataBuilder } from '@/lib/builders/view-data/AvatarViewDataBuilder'; -import { CategoryIconViewDataBuilder } from '@/lib/builders/view-data/CategoryIconViewDataBuilder'; -import { LeagueCoverViewDataBuilder } from '@/lib/builders/view-data/LeagueCoverViewDataBuilder'; -import { LeagueLogoViewDataBuilder } from '@/lib/builders/view-data/LeagueLogoViewDataBuilder'; -import { SponsorLogoViewDataBuilder } from '@/lib/builders/view-data/SponsorLogoViewDataBuilder'; -import { TeamLogoViewDataBuilder } from '@/lib/builders/view-data/TeamLogoViewDataBuilder'; -import { TrackImageViewDataBuilder } from '@/lib/builders/view-data/TrackImageViewDataBuilder'; -import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; - -describe('AvatarViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to AvatarViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle JPEG images', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle GIF images', () => { - const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/gif', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/gif'); - }); - - it('should handle SVG images', () => { - const buffer = new TextEncoder().encode(''); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/svg+xml', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/svg+xml'); - }); - - it('should handle WebP images', () => { - const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/webp', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/webp'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - AvatarViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle large buffer', () => { - const buffer = new Uint8Array(1024 * 1024); // 1MB - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with all zeros', () => { - const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with all ones', () => { - const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle different content types', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const contentTypes = [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/svg+xml', - 'image/bmp', - 'image/tiff', - ]; - - contentTypes.forEach((contentType) => { - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType, - }; - - const result = AvatarViewDataBuilder.build(mediaDto); - - expect(result.contentType).toBe(contentType); - }); - }); - }); -}); - -describe('CategoryIconViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle SVG icons', () => { - const buffer = new TextEncoder().encode(''); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/svg+xml', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/svg+xml'); - }); - - it('should handle small icon files', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - CategoryIconViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with special characters', () => { - const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = CategoryIconViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - }); -}); - -describe('LeagueCoverViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to LeagueCoverViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle JPEG cover images', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle WebP cover images', () => { - const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/webp', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/webp'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - LeagueCoverViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle large cover images', () => { - const buffer = new Uint8Array(2 * 1024 * 1024); // 2MB - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle buffer with all zeros', () => { - const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with all ones', () => { - const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueCoverViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - }); -}); - -describe('LeagueLogoViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to LeagueLogoViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle SVG league logos', () => { - const buffer = new TextEncoder().encode(''); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/svg+xml', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/svg+xml'); - }); - - it('should handle transparent PNG logos', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - LeagueLogoViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle small logo files', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with special characters', () => { - const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = LeagueLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - }); -}); - -describe('SponsorLogoViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to SponsorLogoViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle JPEG sponsor logos', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle SVG sponsor logos', () => { - const buffer = new TextEncoder().encode('Sponsor'); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/svg+xml', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/svg+xml'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - SponsorLogoViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle large sponsor logos', () => { - const buffer = new Uint8Array(3 * 1024 * 1024); // 3MB - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle buffer with all zeros', () => { - const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with all ones', () => { - const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle different content types', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const contentTypes = [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/svg+xml', - 'image/bmp', - 'image/tiff', - ]; - - contentTypes.forEach((contentType) => { - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType, - }; - - const result = SponsorLogoViewDataBuilder.build(mediaDto); - - expect(result.contentType).toBe(contentType); - }); - }); - }); -}); - -describe('TeamLogoViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to TeamLogoViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle JPEG team logos', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle SVG team logos', () => { - const buffer = new TextEncoder().encode(''); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/svg+xml', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/svg+xml'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - TeamLogoViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle small logo files', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with special characters', () => { - const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle different content types', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const contentTypes = [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/svg+xml', - 'image/bmp', - 'image/tiff', - ]; - - contentTypes.forEach((contentType) => { - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType, - }; - - const result = TeamLogoViewDataBuilder.build(mediaDto); - - expect(result.contentType).toBe(contentType); - }); - }); - }); -}); - -describe('TrackImageViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform MediaBinaryDTO to TrackImageViewData correctly', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle JPEG track images', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle WebP track images', () => { - const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/webp', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/webp'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBeDefined(); - expect(result.contentType).toBe(mediaDto.contentType); - }); - - it('should not modify the input DTO', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const originalDto = { ...mediaDto }; - TrackImageViewDataBuilder.build(mediaDto); - - expect(mediaDto).toEqual(originalDto); - }); - - it('should convert buffer to base64 string', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(typeof result.buffer).toBe('string'); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - }); - }); - - describe('edge cases', () => { - it('should handle empty buffer', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle large track images', () => { - const buffer = new Uint8Array(5 * 1024 * 1024); // 5MB - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - - it('should handle buffer with all zeros', () => { - const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle buffer with all ones', () => { - const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/png'); - }); - - it('should handle different content types', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); - const contentTypes = [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/svg+xml', - 'image/bmp', - 'image/tiff', - ]; - - contentTypes.forEach((contentType) => { - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType, - }; - - const result = TrackImageViewDataBuilder.build(mediaDto); - - expect(result.contentType).toBe(contentType); - }); - }); - }); -}); - -describe('Media View Data - Cross-Builder Consistency', () => { - describe('consistency across builders', () => { - it('should produce consistent output format across all builders', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const avatarResult = AvatarViewDataBuilder.build(mediaDto); - const categoryIconResult = CategoryIconViewDataBuilder.build(mediaDto); - const leagueCoverResult = LeagueCoverViewDataBuilder.build(mediaDto); - const leagueLogoResult = LeagueLogoViewDataBuilder.build(mediaDto); - const sponsorLogoResult = SponsorLogoViewDataBuilder.build(mediaDto); - const teamLogoResult = TeamLogoViewDataBuilder.build(mediaDto); - const trackImageResult = TrackImageViewDataBuilder.build(mediaDto); - - // All should have the same buffer format - expect(avatarResult.buffer).toBe(categoryIconResult.buffer); - expect(avatarResult.buffer).toBe(leagueCoverResult.buffer); - expect(avatarResult.buffer).toBe(leagueLogoResult.buffer); - expect(avatarResult.buffer).toBe(sponsorLogoResult.buffer); - expect(avatarResult.buffer).toBe(teamLogoResult.buffer); - expect(avatarResult.buffer).toBe(trackImageResult.buffer); - - // All should have the same content type - expect(avatarResult.contentType).toBe(categoryIconResult.contentType); - expect(avatarResult.contentType).toBe(leagueCoverResult.contentType); - expect(avatarResult.contentType).toBe(leagueLogoResult.contentType); - expect(avatarResult.contentType).toBe(sponsorLogoResult.contentType); - expect(avatarResult.contentType).toBe(teamLogoResult.contentType); - expect(avatarResult.contentType).toBe(trackImageResult.contentType); - }); - - it('should handle the same input consistently across all builders', () => { - const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/jpeg', - }; - - const builders = [ - AvatarViewDataBuilder, - CategoryIconViewDataBuilder, - LeagueCoverViewDataBuilder, - LeagueLogoViewDataBuilder, - SponsorLogoViewDataBuilder, - TeamLogoViewDataBuilder, - TrackImageViewDataBuilder, - ]; - - builders.forEach((Builder) => { - const result = Builder.build(mediaDto); - expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); - expect(result.contentType).toBe('image/jpeg'); - }); - }); - }); - - describe('base64 encoding consistency', () => { - it('should produce valid base64 strings for all builders', () => { - const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const builders = [ - { name: 'AvatarViewDataBuilder', builder: AvatarViewDataBuilder }, - { name: 'CategoryIconViewDataBuilder', builder: CategoryIconViewDataBuilder }, - { name: 'LeagueCoverViewDataBuilder', builder: LeagueCoverViewDataBuilder }, - { name: 'LeagueLogoViewDataBuilder', builder: LeagueLogoViewDataBuilder }, - { name: 'SponsorLogoViewDataBuilder', builder: SponsorLogoViewDataBuilder }, - { name: 'TeamLogoViewDataBuilder', builder: TeamLogoViewDataBuilder }, - { name: 'TrackImageViewDataBuilder', builder: TrackImageViewDataBuilder }, - ]; - - builders.forEach(({ name, builder }) => { - const result = builder.build(mediaDto); - - // Should be a valid base64 string - expect(() => Buffer.from(result.buffer, 'base64')).not.toThrow(); - - // Should decode back to original buffer - const decoded = Buffer.from(result.buffer, 'base64'); - expect(decoded.toString('hex')).toBe(Buffer.from(buffer).toString('hex')); - }); - }); - - it('should handle empty buffer consistently across all builders', () => { - const buffer = new Uint8Array([]); - const mediaDto: MediaBinaryDTO = { - buffer: buffer.buffer, - contentType: 'image/png', - }; - - const builders = [ - AvatarViewDataBuilder, - CategoryIconViewDataBuilder, - LeagueCoverViewDataBuilder, - LeagueLogoViewDataBuilder, - SponsorLogoViewDataBuilder, - TeamLogoViewDataBuilder, - TrackImageViewDataBuilder, - ]; - - builders.forEach((Builder) => { - const result = Builder.build(mediaDto); - expect(result.buffer).toBe(''); - expect(result.contentType).toBe('image/png'); - }); - }); - }); -}); diff --git a/apps/website/tests/view-data/onboarding.test.ts b/apps/website/tests/view-data/onboarding.test.ts deleted file mode 100644 index 02210c44a..000000000 --- a/apps/website/tests/view-data/onboarding.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -/** - * View Data Layer Tests - Onboarding Functionality - * - * This test file covers the view data layer for onboarding functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage includes: - * - Onboarding page data transformation and validation - * - Onboarding wizard view models and field formatting - * - Authentication and authorization checks for onboarding flow - * - Redirect logic based on onboarding status (already onboarded, not authenticated) - * - Onboarding-specific formatting and validation - * - Derived fields for onboarding UI components (progress, completion status, etc.) - * - Default values and fallbacks for onboarding views - * - Onboarding step data mapping and state management - * - Error handling and fallback UI states for onboarding flow - */ - -import { OnboardingViewDataBuilder } from '@/lib/builders/view-data/OnboardingViewDataBuilder'; -import { OnboardingPageViewDataBuilder } from '@/lib/builders/view-data/OnboardingPageViewDataBuilder'; -import { CompleteOnboardingViewDataBuilder } from '@/lib/builders/view-data/CompleteOnboardingViewDataBuilder'; -import { Result } from '@/lib/contracts/Result'; -import { PresentationError } from '@/lib/contracts/page-queries/PresentationError'; -import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; - -describe('OnboardingViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform successful onboarding check to ViewData correctly', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({ - isAlreadyOnboarded: false, - }); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isOk()).toBe(true); - expect(result.unwrap()).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle already onboarded user correctly', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({ - isAlreadyOnboarded: true, - }); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isOk()).toBe(true); - expect(result.unwrap()).toEqual({ - isAlreadyOnboarded: true, - }); - }); - - it('should handle missing isAlreadyOnboarded field with default false', () => { - const apiDto: Result<{ isAlreadyOnboarded?: boolean }, PresentationError> = Result.ok({}); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isOk()).toBe(true); - expect(result.unwrap()).toEqual({ - isAlreadyOnboarded: false, - }); - }); - }); - - describe('error handling', () => { - it('should propagate unauthorized error', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('unauthorized'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('unauthorized'); - }); - - it('should propagate notFound error', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('notFound'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('notFound'); - }); - - it('should propagate serverError', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('serverError'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('serverError'); - }); - - it('should propagate networkError', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('networkError'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('networkError'); - }); - - it('should propagate validationError', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('validationError'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('validationError'); - }); - - it('should propagate unknown error', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('unknown'); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isErr()).toBe(true); - expect(result.getError()).toBe('unknown'); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({ - isAlreadyOnboarded: false, - }); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.unwrap().isAlreadyOnboarded).toBe(false); - }); - - it('should not modify the input DTO', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({ - isAlreadyOnboarded: false, - }); - - const originalDto = { ...apiDto.unwrap() }; - OnboardingViewDataBuilder.build(apiDto); - - expect(apiDto.unwrap()).toEqual(originalDto); - }); - }); - - describe('edge cases', () => { - it('should handle null isAlreadyOnboarded as false', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean | null }, PresentationError> = Result.ok({ - isAlreadyOnboarded: null, - }); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isOk()).toBe(true); - expect(result.unwrap()).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle undefined isAlreadyOnboarded as false', () => { - const apiDto: Result<{ isAlreadyOnboarded: boolean | undefined }, PresentationError> = Result.ok({ - isAlreadyOnboarded: undefined, - }); - - const result = OnboardingViewDataBuilder.build(apiDto); - - expect(result.isOk()).toBe(true); - expect(result.unwrap()).toEqual({ - isAlreadyOnboarded: false, - }); - }); - }); -}); - -describe('OnboardingPageViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform driver data to ViewData correctly when driver exists', () => { - const apiDto = { id: 'driver-123', name: 'Test Driver' }; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: true, - }); - }); - - it('should handle empty object as driver data', () => { - const apiDto = {}; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: true, - }); - }); - - it('should handle null driver data', () => { - const apiDto = null; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle undefined driver data', () => { - const apiDto = undefined; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: false, - }); - }); - }); - - describe('data transformation', () => { - it('should preserve all driver data fields in the output', () => { - const apiDto = { - id: 'driver-123', - name: 'Test Driver', - email: 'test@example.com', - createdAt: '2024-01-01T00:00:00.000Z', - }; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result.isAlreadyOnboarded).toBe(true); - }); - - it('should not modify the input driver data', () => { - const apiDto = { id: 'driver-123', name: 'Test Driver' }; - const originalDto = { ...apiDto }; - - OnboardingPageViewDataBuilder.build(apiDto); - - expect(apiDto).toEqual(originalDto); - }); - }); - - describe('edge cases', () => { - it('should handle empty string as driver data', () => { - const apiDto = ''; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle zero as driver data', () => { - const apiDto = 0; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle false as driver data', () => { - const apiDto = false; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: false, - }); - }); - - it('should handle array as driver data', () => { - const apiDto = ['driver-123']; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: true, - }); - }); - - it('should handle function as driver data', () => { - const apiDto = () => {}; - - const result = OnboardingPageViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - isAlreadyOnboarded: true, - }); - }); - }); -}); - -describe('CompleteOnboardingViewDataBuilder', () => { - describe('happy paths', () => { - it('should transform successful onboarding completion DTO to ViewData correctly', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: 'driver-123', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - success: true, - driverId: 'driver-123', - errorMessage: undefined, - }); - }); - - it('should handle onboarding completion with error message', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: false, - driverId: undefined, - errorMessage: 'Failed to complete onboarding', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - success: false, - driverId: undefined, - errorMessage: 'Failed to complete onboarding', - }); - }); - - it('should handle onboarding completion with only success field', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result).toEqual({ - success: true, - driverId: undefined, - errorMessage: undefined, - }); - }); - }); - - describe('data transformation', () => { - it('should preserve all DTO fields in the output', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: 'driver-123', - errorMessage: undefined, - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.success).toBe(apiDto.success); - expect(result.driverId).toBe(apiDto.driverId); - expect(result.errorMessage).toBe(apiDto.errorMessage); - }); - - it('should not modify the input DTO', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: 'driver-123', - errorMessage: undefined, - }; - - const originalDto = { ...apiDto }; - CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(apiDto).toEqual(originalDto); - }); - }); - - describe('edge cases', () => { - it('should handle false success value', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: false, - driverId: undefined, - errorMessage: 'Error occurred', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.success).toBe(false); - expect(result.driverId).toBeUndefined(); - expect(result.errorMessage).toBe('Error occurred'); - }); - - it('should handle empty string error message', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: false, - driverId: undefined, - errorMessage: '', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.success).toBe(false); - expect(result.errorMessage).toBe(''); - }); - - it('should handle very long driverId', () => { - const longDriverId = 'driver-' + 'a'.repeat(1000); - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: longDriverId, - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.driverId).toBe(longDriverId); - }); - - it('should handle special characters in error message', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: false, - driverId: undefined, - errorMessage: 'Error: "Failed to create driver" (code: 500)', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.errorMessage).toBe('Error: "Failed to create driver" (code: 500)'); - }); - }); - - describe('derived fields calculation', () => { - it('should calculate isSuccessful derived field correctly', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: 'driver-123', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - // Note: The builder doesn't add derived fields, but we can verify the structure - expect(result.success).toBe(true); - expect(result.driverId).toBe('driver-123'); - }); - - it('should handle success with no driverId', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: true, - driverId: undefined, - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.success).toBe(true); - expect(result.driverId).toBeUndefined(); - }); - - it('should handle failure with driverId', () => { - const apiDto: CompleteOnboardingOutputDTO = { - success: false, - driverId: 'driver-123', - errorMessage: 'Partial failure', - }; - - const result = CompleteOnboardingViewDataBuilder.build(apiDto); - - expect(result.success).toBe(false); - expect(result.driverId).toBe('driver-123'); - expect(result.errorMessage).toBe('Partial failure'); - }); - }); -}); diff --git a/apps/website/tests/view-data/profile.test.ts b/apps/website/tests/view-data/profile.test.ts deleted file mode 100644 index b2217ba9e..000000000 --- a/apps/website/tests/view-data/profile.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * View Data Layer Tests - Profile Functionality - * - * This test file will cover the view data layer for profile functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage will include: - * - Driver profile data transformation and formatting - * - Profile statistics (rating, rank, race counts, finishes, consistency, etc.) - * - Team membership data mapping and role labeling - * - Extended profile data (timezone, racing style, favorite track/car, etc.) - * - Social handles formatting and URL generation - * - Achievement data transformation and icon mapping - * - Friends list data mapping and display formatting - * - Derived fields (percentile, consistency, looking for team, open to requests) - * - Default values and fallbacks for profile views - * - Profile-specific formatting (country flags, date labels, etc.) - */ diff --git a/apps/website/tests/view-data/races.test.ts b/apps/website/tests/view-data/races.test.ts deleted file mode 100644 index fabf30935..000000000 --- a/apps/website/tests/view-data/races.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * View Data Layer Tests - Races Functionality - * - * This test file will cover the view data layer for races functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage will include: - * - Race list data transformation and sorting - * - Individual race page view models (race details, schedule, participants) - * - Race results data formatting and ranking calculations - * - Stewarding data transformation (protests, penalties, incidents) - * - All races page data aggregation and filtering - * - Derived race fields (status, eligibility, availability, etc.) - * - Default values and fallbacks for race views - * - Race-specific formatting (lap times, gaps, points, positions, etc.) - * - Data grouping and categorization for race components (by series, date, type) - * - Race search and filtering view models - * - Real-time race updates and state management - * - Historical race data transformation - * - Race registration and withdrawal data handling - */ diff --git a/apps/website/tests/view-data/sponsor.test.ts b/apps/website/tests/view-data/sponsor.test.ts deleted file mode 100644 index 6244fa1fb..000000000 --- a/apps/website/tests/view-data/sponsor.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * View Data Layer Tests - Sponsor Functionality - * - * This test file will cover the view data layer for sponsor functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage will include: - * - Sponsor dashboard data transformation and metrics - * - Sponsor billing and payment view models - * - Campaign management data formatting and status tracking - * - League sponsorship data aggregation and tier calculations - * - Sponsor settings and configuration view models - * - Sponsor signup and onboarding data handling - * - Derived sponsor fields (engagement metrics, ROI calculations, etc.) - * - Default values and fallbacks for sponsor views - * - Sponsor-specific formatting (budgets, impressions, clicks, conversions) - * - Data grouping and categorization for sponsor components (by campaign, league, status) - * - Sponsor search and filtering view models - * - Real-time sponsor metrics and state management - * - Historical sponsor performance data transformation - */ diff --git a/apps/website/tests/view-data/teams.test.ts b/apps/website/tests/view-data/teams.test.ts deleted file mode 100644 index 097c011bf..000000000 --- a/apps/website/tests/view-data/teams.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * View Data Layer Tests - Teams Functionality - * - * This test file will cover the view data layer for teams functionality. - * - * The view data layer is responsible for: - * - DTO → UI model mapping - * - Formatting, sorting, and grouping - * - Derived fields and defaults - * - UI-specific semantics - * - * This layer isolates the UI from API churn by providing a stable interface - * between the API layer and the presentation layer. - * - * Test coverage will include: - * - Team list data transformation and sorting - * - Individual team profile view models - * - Team creation form data handling - * - Team leaderboard data transformation - * - Team statistics and metrics formatting - * - Derived team fields (performance ratings, rankings, etc.) - * - Default values and fallbacks for team views - * - Team-specific formatting (points, positions, member counts, etc.) - * - Data grouping and categorization for team components - * - Team search and filtering view models - * - Team member data transformation - * - Team comparison data transformation - */ \ No newline at end of file