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('');
+ 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('');
- 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