diff --git a/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.test.ts new file mode 100644 index 000000000..d9a34ad2b --- /dev/null +++ b/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect } from 'vitest'; +import { GenerateAvatarsViewDataBuilder } from './GenerateAvatarsViewDataBuilder'; +import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; + +describe('GenerateAvatarsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform RequestAvatarGenerationOutputDTO to GenerateAvatarsViewData correctly', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result).toEqual({ + success: true, + avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'], + errorMessage: null, + }); + }); + + it('should handle empty avatar URLs', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: [], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toHaveLength(0); + }); + + it('should handle single avatar URL', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toHaveLength(1); + expect(result.avatarUrls[0]).toBe('avatar-url-1'); + }); + + it('should handle multiple avatar URLs', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3', 'avatar-url-4', 'avatar-url-5'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toHaveLength(5); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1', 'avatar-url-2'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.success).toBe(requestAvatarGenerationOutputDto.success); + expect(result.avatarUrls).toEqual(requestAvatarGenerationOutputDto.avatarUrls); + expect(result.errorMessage).toBe(requestAvatarGenerationOutputDto.errorMessage); + }); + + it('should not modify the input DTO', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1'], + errorMessage: null, + }; + + const originalDto = { ...requestAvatarGenerationOutputDto }; + GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(requestAvatarGenerationOutputDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle success false', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: false, + avatarUrls: [], + errorMessage: 'Generation failed', + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.success).toBe(false); + }); + + it('should handle error message', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: false, + avatarUrls: [], + errorMessage: 'Invalid input data', + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.errorMessage).toBe('Invalid input data'); + }); + + it('should handle null error message', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.errorMessage).toBeNull(); + }); + + it('should handle undefined avatarUrls', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: undefined, + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toEqual([]); + }); + + it('should handle empty string avatar URLs', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['', 'avatar-url-1', ''], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toEqual(['', 'avatar-url-1', '']); + }); + + it('should handle special characters in avatar URLs', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: ['avatar-url-1?param=value', 'avatar-url-2#anchor', 'avatar-url-3?query=1&test=2'], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toEqual([ + 'avatar-url-1?param=value', + 'avatar-url-2#anchor', + 'avatar-url-3?query=1&test=2', + ]); + }); + + it('should handle very long avatar URLs', () => { + const longUrl = 'https://example.com/avatars/' + 'a'.repeat(1000) + '.png'; + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: [longUrl], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls[0]).toBe(longUrl); + }); + + it('should handle avatar URLs with special characters', () => { + const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = { + success: true, + avatarUrls: [ + 'avatar-url-1?name=John%20Doe', + 'avatar-url-2?email=test@example.com', + 'avatar-url-3?query=hello%20world', + ], + errorMessage: null, + }; + + const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto); + + expect(result.avatarUrls).toEqual([ + 'avatar-url-1?name=John%20Doe', + 'avatar-url-2?email=test@example.com', + 'avatar-url-3?query=hello%20world', + ]); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/HomeViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/HomeViewDataBuilder.test.ts new file mode 100644 index 000000000..2087f9edc --- /dev/null +++ b/apps/website/lib/builders/view-data/HomeViewDataBuilder.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect } from 'vitest'; +import { HomeViewDataBuilder } from './HomeViewDataBuilder'; +import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO'; + +describe('HomeViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform HomeDataDTO to HomeViewData correctly', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: true, + upcomingRaces: [ + { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + track: 'Test Track', + }, + ], + topLeagues: [ + { + id: 'league-1', + name: 'Test League', + description: 'Test Description', + }, + ], + teams: [ + { + id: 'team-1', + name: 'Test Team', + tag: 'TT', + }, + ], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result).toEqual({ + isAlpha: true, + upcomingRaces: [ + { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + track: 'Test Track', + }, + ], + topLeagues: [ + { + id: 'league-1', + name: 'Test League', + description: 'Test Description', + }, + ], + teams: [ + { + id: 'team-1', + name: 'Test Team', + tag: 'TT', + }, + ], + }); + }); + + it('should handle empty arrays correctly', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: false, + upcomingRaces: [], + topLeagues: [], + teams: [], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result).toEqual({ + isAlpha: false, + upcomingRaces: [], + topLeagues: [], + teams: [], + }); + }); + + it('should handle multiple items in arrays', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: true, + upcomingRaces: [ + { id: 'race-1', name: 'Race 1', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track 1' }, + { id: 'race-2', name: 'Race 2', scheduledAt: '2024-01-02T10:00:00Z', track: 'Track 2' }, + ], + topLeagues: [ + { id: 'league-1', name: 'League 1', description: 'Description 1' }, + { id: 'league-2', name: 'League 2', description: 'Description 2' }, + ], + teams: [ + { id: 'team-1', name: 'Team 1', tag: 'T1' }, + { id: 'team-2', name: 'Team 2', tag: 'T2' }, + ], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result.upcomingRaces).toHaveLength(2); + expect(result.topLeagues).toHaveLength(2); + expect(result.teams).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: true, + upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }], + topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }], + teams: [{ id: 'team-1', name: 'Team', tag: 'T' }], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result.isAlpha).toBe(homeDataDto.isAlpha); + expect(result.upcomingRaces).toEqual(homeDataDto.upcomingRaces); + expect(result.topLeagues).toEqual(homeDataDto.topLeagues); + expect(result.teams).toEqual(homeDataDto.teams); + }); + + it('should not modify the input DTO', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: true, + upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }], + topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }], + teams: [{ id: 'team-1', name: 'Team', tag: 'T' }], + }; + + const originalDto = { ...homeDataDto }; + HomeViewDataBuilder.build(homeDataDto); + + expect(homeDataDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle false isAlpha value', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: false, + upcomingRaces: [], + topLeagues: [], + teams: [], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result.isAlpha).toBe(false); + }); + + it('should handle null/undefined values in arrays', () => { + const homeDataDto: HomeDataDTO = { + isAlpha: true, + upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }], + topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }], + teams: [{ id: 'team-1', name: 'Team', tag: 'T' }], + }; + + const result = HomeViewDataBuilder.build(homeDataDto); + + expect(result.upcomingRaces[0].id).toBe('race-1'); + expect(result.topLeagues[0].id).toBe('league-1'); + expect(result.teams[0].id).toBe('team-1'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.test.ts new file mode 100644 index 000000000..4c93182f4 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueSettingsViewDataBuilder } from './LeagueSettingsViewDataBuilder'; +import type { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; + +describe('LeagueSettingsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform LeagueSettingsApiDto to LeagueSettingsViewData correctly', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-123', + league: { + id: 'league-123', + name: 'Test League', + description: 'Test Description', + }, + config: { + maxDrivers: 32, + qualifyingFormat: 'Open', + raceLength: 30, + }, + }; + + const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(result).toEqual({ + leagueId: 'league-123', + league: { + id: 'league-123', + name: 'Test League', + description: 'Test Description', + }, + config: { + maxDrivers: 32, + qualifyingFormat: 'Open', + raceLength: 30, + }, + }); + }); + + it('should handle minimal configuration', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-456', + league: { + id: 'league-456', + name: 'Minimal League', + description: '', + }, + config: { + maxDrivers: 16, + qualifyingFormat: 'Open', + raceLength: 20, + }, + }; + + const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(result.leagueId).toBe('league-456'); + expect(result.league.name).toBe('Minimal League'); + expect(result.config.maxDrivers).toBe(16); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-789', + league: { + id: 'league-789', + name: 'Full League', + description: 'Full Description', + }, + config: { + maxDrivers: 24, + qualifyingFormat: 'Open', + raceLength: 45, + }, + }; + + const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(result.leagueId).toBe(leagueSettingsApiDto.leagueId); + expect(result.league).toEqual(leagueSettingsApiDto.league); + expect(result.config).toEqual(leagueSettingsApiDto.config); + }); + + it('should not modify the input DTO', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-101', + league: { + id: 'league-101', + name: 'Test League', + description: 'Test', + }, + config: { + maxDrivers: 20, + qualifyingFormat: 'Open', + raceLength: 25, + }, + }; + + const originalDto = { ...leagueSettingsApiDto }; + LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(leagueSettingsApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle different qualifying formats', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-102', + league: { + id: 'league-102', + name: 'Test League', + description: 'Test', + }, + config: { + maxDrivers: 20, + qualifyingFormat: 'Closed', + raceLength: 30, + }, + }; + + const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(result.config.qualifyingFormat).toBe('Closed'); + }); + + it('should handle large driver counts', () => { + const leagueSettingsApiDto: LeagueSettingsApiDto = { + leagueId: 'league-103', + league: { + id: 'league-103', + name: 'Test League', + description: 'Test', + }, + config: { + maxDrivers: 100, + qualifyingFormat: 'Open', + raceLength: 60, + }, + }; + + const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto); + + expect(result.config.maxDrivers).toBe(100); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.test.ts new file mode 100644 index 000000000..55df9cbca --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueSponsorshipsViewDataBuilder } from './LeagueSponsorshipsViewDataBuilder'; +import type { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; + +describe('LeagueSponsorshipsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform LeagueSponsorshipsApiDto to LeagueSponsorshipsViewData correctly', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-123', + league: { + id: 'league-123', + name: 'Test League', + }, + sponsorshipSlots: [ + { + id: 'slot-1', + name: 'Primary Sponsor', + price: 1000, + status: 'available', + }, + ], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + }, + ], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result).toEqual({ + leagueId: 'league-123', + activeTab: 'overview', + onTabChange: expect.any(Function), + league: { + id: 'league-123', + name: 'Test League', + }, + sponsorshipSlots: [ + { + id: 'slot-1', + name: 'Primary Sponsor', + price: 1000, + status: 'available', + }, + ], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + formattedRequestedAt: expect.any(String), + statusLabel: expect.any(String), + }, + ], + }); + }); + + it('should handle empty sponsorship requests', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-456', + league: { + id: 'league-456', + name: 'Test League', + }, + sponsorshipSlots: [ + { + id: 'slot-1', + name: 'Primary Sponsor', + price: 1000, + status: 'available', + }, + ], + sponsorshipRequests: [], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result.sponsorshipRequests).toHaveLength(0); + }); + + it('should handle multiple sponsorship requests', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-789', + league: { + id: 'league-789', + name: 'Test League', + }, + sponsorshipSlots: [], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Sponsor 1', + sponsorLogo: 'logo-1', + message: 'Message 1', + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + }, + { + id: 'request-2', + sponsorId: 'sponsor-2', + sponsorName: 'Sponsor 2', + sponsorLogo: 'logo-2', + message: 'Message 2', + requestedAt: '2024-01-02T10:00:00Z', + status: 'approved', + }, + ], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result.sponsorshipRequests).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-101', + league: { + id: 'league-101', + name: 'Test League', + }, + sponsorshipSlots: [ + { + id: 'slot-1', + name: 'Primary Sponsor', + price: 1000, + status: 'available', + }, + ], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + }, + ], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result.leagueId).toBe(leagueSponsorshipsApiDto.leagueId); + expect(result.league).toEqual(leagueSponsorshipsApiDto.league); + expect(result.sponsorshipSlots).toEqual(leagueSponsorshipsApiDto.sponsorshipSlots); + }); + + it('should not modify the input DTO', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-102', + league: { + id: 'league-102', + name: 'Test League', + }, + sponsorshipSlots: [], + sponsorshipRequests: [], + }; + + const originalDto = { ...leagueSponsorshipsApiDto }; + LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(leagueSponsorshipsApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle requests without sponsor logo', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-103', + league: { + id: 'league-103', + name: 'Test League', + }, + sponsorshipSlots: [], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: null, + message: 'Test message', + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + }, + ], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result.sponsorshipRequests[0].sponsorLogoUrl).toBeNull(); + }); + + it('should handle requests without message', () => { + const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = { + leagueId: 'league-104', + league: { + id: 'league-104', + name: 'Test League', + }, + sponsorshipSlots: [], + sponsorshipRequests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: null, + requestedAt: '2024-01-01T10:00:00Z', + status: 'pending', + }, + ], + }; + + const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto); + + expect(result.sponsorshipRequests[0].message).toBeNull(); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.test.ts new file mode 100644 index 000000000..838f13fa6 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.test.ts @@ -0,0 +1,213 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueWalletViewDataBuilder } from './LeagueWalletViewDataBuilder'; +import type { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; + +describe('LeagueWalletViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform LeagueWalletApiDto to LeagueWalletViewData correctly', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-123', + balance: 5000, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: 1000, + status: 'completed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Sponsorship payment', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result).toEqual({ + leagueId: 'league-123', + balance: 5000, + formattedBalance: expect.any(String), + totalRevenue: 5000, + formattedTotalRevenue: expect.any(String), + totalFees: 0, + formattedTotalFees: expect.any(String), + pendingPayouts: 0, + formattedPendingPayouts: expect.any(String), + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: 1000, + status: 'completed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Sponsorship payment', + formattedAmount: expect.any(String), + amountColor: 'green', + formattedDate: expect.any(String), + statusColor: 'green', + typeColor: 'blue', + }, + ], + }); + }); + + it('should handle empty transactions', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-456', + balance: 0, + currency: 'USD', + transactions: [], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.transactions).toHaveLength(0); + expect(result.balance).toBe(0); + }); + + it('should handle multiple transactions', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-789', + balance: 10000, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: 5000, + status: 'completed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Sponsorship payment', + }, + { + id: 'txn-2', + amount: -1000, + status: 'completed', + createdAt: '2024-01-02T10:00:00Z', + description: 'Payout', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.transactions).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-101', + balance: 7500, + currency: 'EUR', + transactions: [ + { + id: 'txn-1', + amount: 2500, + status: 'completed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Test transaction', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.leagueId).toBe(leagueWalletApiDto.leagueId); + expect(result.balance).toBe(leagueWalletApiDto.balance); + expect(result.currency).toBe(leagueWalletApiDto.currency); + }); + + it('should not modify the input DTO', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-102', + balance: 5000, + currency: 'USD', + transactions: [], + }; + + const originalDto = { ...leagueWalletApiDto }; + LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(leagueWalletApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle negative balance', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-103', + balance: -500, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: -500, + status: 'completed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Overdraft', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.balance).toBe(-500); + expect(result.transactions[0].amountColor).toBe('red'); + }); + + it('should handle pending transactions', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-104', + balance: 1000, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: 500, + status: 'pending', + createdAt: '2024-01-01T10:00:00Z', + description: 'Pending payment', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.transactions[0].statusColor).toBe('yellow'); + }); + + it('should handle failed transactions', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-105', + balance: 1000, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + amount: 500, + status: 'failed', + createdAt: '2024-01-01T10:00:00Z', + description: 'Failed payment', + }, + ], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.transactions[0].statusColor).toBe('red'); + }); + + it('should handle different currencies', () => { + const leagueWalletApiDto: LeagueWalletApiDto = { + leagueId: 'league-106', + balance: 1000, + currency: 'EUR', + transactions: [], + }; + + const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto); + + expect(result.currency).toBe('EUR'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.test.ts new file mode 100644 index 000000000..96f06ba60 --- /dev/null +++ b/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.test.ts @@ -0,0 +1,243 @@ +import { describe, it, expect } from 'vitest'; +import { ProfileLeaguesViewDataBuilder } from './ProfileLeaguesViewDataBuilder'; + +describe('ProfileLeaguesViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform ProfileLeaguesPageDto to ProfileLeaguesViewData correctly', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Owned League', + description: 'Test Description', + membershipRole: 'owner' as const, + }, + ], + memberLeagues: [ + { + leagueId: 'league-2', + name: 'Member League', + description: 'Test Description', + membershipRole: 'member' as const, + }, + ], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result).toEqual({ + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Owned League', + description: 'Test Description', + membershipRole: 'owner', + }, + ], + memberLeagues: [ + { + leagueId: 'league-2', + name: 'Member League', + description: 'Test Description', + membershipRole: 'member', + }, + ], + }); + }); + + it('should handle empty owned leagues', () => { + const profileLeaguesPageDto = { + ownedLeagues: [], + memberLeagues: [ + { + leagueId: 'league-1', + name: 'Member League', + description: 'Test Description', + membershipRole: 'member' as const, + }, + ], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues).toHaveLength(0); + expect(result.memberLeagues).toHaveLength(1); + }); + + it('should handle empty member leagues', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Owned League', + description: 'Test Description', + membershipRole: 'owner' as const, + }, + ], + memberLeagues: [], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues).toHaveLength(1); + expect(result.memberLeagues).toHaveLength(0); + }); + + it('should handle multiple leagues in both arrays', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Owned League 1', + description: 'Description 1', + membershipRole: 'owner' as const, + }, + { + leagueId: 'league-2', + name: 'Owned League 2', + description: 'Description 2', + membershipRole: 'admin' as const, + }, + ], + memberLeagues: [ + { + leagueId: 'league-3', + name: 'Member League 1', + description: 'Description 3', + membershipRole: 'member' as const, + }, + { + leagueId: 'league-4', + name: 'Member League 2', + description: 'Description 4', + membershipRole: 'steward' as const, + }, + ], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues).toHaveLength(2); + expect(result.memberLeagues).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Test League', + description: 'Test Description', + membershipRole: 'owner' as const, + }, + ], + memberLeagues: [ + { + leagueId: 'league-2', + name: 'Test League 2', + description: 'Test Description 2', + membershipRole: 'member' as const, + }, + ], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues[0].leagueId).toBe(profileLeaguesPageDto.ownedLeagues[0].leagueId); + expect(result.ownedLeagues[0].name).toBe(profileLeaguesPageDto.ownedLeagues[0].name); + expect(result.ownedLeagues[0].description).toBe(profileLeaguesPageDto.ownedLeagues[0].description); + expect(result.ownedLeagues[0].membershipRole).toBe(profileLeaguesPageDto.ownedLeagues[0].membershipRole); + expect(result.memberLeagues[0].leagueId).toBe(profileLeaguesPageDto.memberLeagues[0].leagueId); + expect(result.memberLeagues[0].name).toBe(profileLeaguesPageDto.memberLeagues[0].name); + expect(result.memberLeagues[0].description).toBe(profileLeaguesPageDto.memberLeagues[0].description); + expect(result.memberLeagues[0].membershipRole).toBe(profileLeaguesPageDto.memberLeagues[0].membershipRole); + }); + + it('should not modify the input DTO', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Test League', + description: 'Test Description', + membershipRole: 'owner' as const, + }, + ], + memberLeagues: [ + { + leagueId: 'league-2', + name: 'Test League 2', + description: 'Test Description 2', + membershipRole: 'member' as const, + }, + ], + }; + + const originalDto = { ...profileLeaguesPageDto }; + ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(profileLeaguesPageDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle different membership roles', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Test League', + description: 'Test Description', + membershipRole: 'owner' as const, + }, + { + leagueId: 'league-2', + name: 'Test League 2', + description: 'Test Description 2', + membershipRole: 'admin' as const, + }, + { + leagueId: 'league-3', + name: 'Test League 3', + description: 'Test Description 3', + membershipRole: 'steward' as const, + }, + { + leagueId: 'league-4', + name: 'Test League 4', + description: 'Test Description 4', + membershipRole: 'member' as const, + }, + ], + memberLeagues: [], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues[0].membershipRole).toBe('owner'); + expect(result.ownedLeagues[1].membershipRole).toBe('admin'); + expect(result.ownedLeagues[2].membershipRole).toBe('steward'); + expect(result.ownedLeagues[3].membershipRole).toBe('member'); + }); + + it('should handle empty description', () => { + const profileLeaguesPageDto = { + ownedLeagues: [ + { + leagueId: 'league-1', + name: 'Test League', + description: '', + membershipRole: 'owner' as const, + }, + ], + memberLeagues: [], + }; + + const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto); + + expect(result.ownedLeagues[0].description).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/ProfileViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/ProfileViewDataBuilder.test.ts new file mode 100644 index 000000000..35a7e9831 --- /dev/null +++ b/apps/website/lib/builders/view-data/ProfileViewDataBuilder.test.ts @@ -0,0 +1,499 @@ +import { describe, it, expect } from 'vitest'; +import { ProfileViewDataBuilder } from './ProfileViewDataBuilder'; +import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; + +describe('ProfileViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform GetDriverProfileOutputDTO to ProfileViewData correctly', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.id).toBe('driver-123'); + expect(result.driver.name).toBe('Test Driver'); + expect(result.driver.countryCode).toBe('US'); + expect(result.driver.bio).toBe('Test bio'); + expect(result.driver.iracingId).toBe('12345'); + expect(result.stats).not.toBeNull(); + expect(result.stats?.ratingLabel).toBe('1500'); + expect(result.teamMemberships).toHaveLength(1); + expect(result.extendedProfile).not.toBeNull(); + expect(result.extendedProfile?.socialHandles).toHaveLength(1); + expect(result.extendedProfile?.achievements).toHaveLength(1); + }); + + it('should handle null driver (no profile)', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: null, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.id).toBe(''); + expect(result.driver.name).toBe(''); + expect(result.driver.countryCode).toBe(''); + expect(result.driver.bio).toBeNull(); + expect(result.driver.iracingId).toBeNull(); + expect(result.stats).toBeNull(); + expect(result.teamMemberships).toHaveLength(0); + expect(result.extendedProfile).toBeNull(); + }); + + it('should handle null stats', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.stats).toBeNull(); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.id).toBe(profileDto.currentDriver?.id); + expect(result.driver.name).toBe(profileDto.currentDriver?.name); + expect(result.driver.countryCode).toBe(profileDto.currentDriver?.country); + expect(result.driver.bio).toBe(profileDto.currentDriver?.bio); + expect(result.driver.iracingId).toBe(String(profileDto.currentDriver?.iracingId)); + expect(result.stats?.totalRacesLabel).toBe('50'); + expect(result.stats?.winsLabel).toBe('10'); + expect(result.teamMemberships).toHaveLength(1); + expect(result.extendedProfile?.socialHandles).toHaveLength(1); + expect(result.extendedProfile?.achievements).toHaveLength(1); + }); + + it('should not modify the input DTO', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const originalDto = { ...profileDto }; + ProfileViewDataBuilder.build(profileDto); + + expect(profileDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle driver without avatar', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: null, + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.avatarUrl).toContain('default'); + }); + + it('should handle driver without iracingId', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.iracingId).toBeNull(); + }); + + it('should handle driver without global rank', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.driver.globalRankLabel).toBe('—'); + }); + + it('should handle empty team memberships', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.teamMemberships).toHaveLength(0); + }); + + it('should handle empty friends list', () => { + const profileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = ProfileViewDataBuilder.build(profileDto); + + expect(result.extendedProfile?.friends).toHaveLength(0); + expect(result.extendedProfile?.friendsCountLabel).toBe('0'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.test.ts new file mode 100644 index 000000000..5050b7e8a --- /dev/null +++ b/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.test.ts @@ -0,0 +1,319 @@ +import { describe, it, expect } from 'vitest'; +import { ProtestDetailViewDataBuilder } from './ProtestDetailViewDataBuilder'; + +describe('ProtestDetailViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform ProtestDetailApiDto to ProtestDetailViewData correctly', () => { + const protestDetailApiDto = { + id: 'protest-123', + leagueId: 'league-456', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [ + { + type: 'time_penalty', + label: 'Time Penalty', + description: 'Add time to race result', + }, + ], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result).toEqual({ + protestId: 'protest-123', + leagueId: 'league-456', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [ + { + type: 'time_penalty', + label: 'Time Penalty', + description: 'Add time to race result', + }, + ], + }); + }); + + it('should handle resolved status', () => { + const protestDetailApiDto = { + id: 'protest-456', + leagueId: 'league-789', + status: 'resolved', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + protestingDriver: { + id: 'driver-3', + name: 'Driver 3', + }, + accusedDriver: { + id: 'driver-4', + name: 'Driver 4', + }, + race: { + id: 'race-2', + name: 'Test Race 2', + scheduledAt: '2024-01-02T10:00:00Z', + }, + penaltyTypes: [], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.status).toBe('resolved'); + }); + + it('should handle multiple penalty types', () => { + const protestDetailApiDto = { + id: 'protest-789', + leagueId: 'league-101', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 15, + description: 'Contact at turn 7', + }, + protestingDriver: { + id: 'driver-5', + name: 'Driver 5', + }, + accusedDriver: { + id: 'driver-6', + name: 'Driver 6', + }, + race: { + id: 'race-3', + name: 'Test Race 3', + scheduledAt: '2024-01-03T10:00:00Z', + }, + penaltyTypes: [ + { + type: 'time_penalty', + label: 'Time Penalty', + description: 'Add time to race result', + }, + { + type: 'grid_penalty', + label: 'Grid Penalty', + description: 'Drop grid positions', + }, + ], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.penaltyTypes).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const protestDetailApiDto = { + id: 'protest-101', + leagueId: 'league-102', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [ + { + type: 'time_penalty', + label: 'Time Penalty', + description: 'Add time to race result', + }, + ], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.protestId).toBe(protestDetailApiDto.id); + expect(result.leagueId).toBe(protestDetailApiDto.leagueId); + expect(result.status).toBe(protestDetailApiDto.status); + expect(result.submittedAt).toBe(protestDetailApiDto.submittedAt); + expect(result.incident).toEqual(protestDetailApiDto.incident); + expect(result.protestingDriver).toEqual(protestDetailApiDto.protestingDriver); + expect(result.accusedDriver).toEqual(protestDetailApiDto.accusedDriver); + expect(result.race).toEqual(protestDetailApiDto.race); + expect(result.penaltyTypes).toEqual(protestDetailApiDto.penaltyTypes); + }); + + it('should not modify the input DTO', () => { + const protestDetailApiDto = { + id: 'protest-102', + leagueId: 'league-103', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [], + }; + + const originalDto = { ...protestDetailApiDto }; + ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(protestDetailApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle different status values', () => { + const protestDetailApiDto = { + id: 'protest-103', + leagueId: 'league-104', + status: 'rejected', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.status).toBe('rejected'); + }); + + it('should handle lap 0', () => { + const protestDetailApiDto = { + id: 'protest-104', + leagueId: 'league-105', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 0, + description: 'Contact at start', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.incident.lap).toBe(0); + }); + + it('should handle empty description', () => { + const protestDetailApiDto = { + id: 'protest-105', + leagueId: 'league-106', + status: 'pending', + submittedAt: '2024-01-01T10:00:00Z', + incident: { + lap: 5, + description: '', + }, + protestingDriver: { + id: 'driver-1', + name: 'Driver 1', + }, + accusedDriver: { + id: 'driver-2', + name: 'Driver 2', + }, + race: { + id: 'race-1', + name: 'Test Race', + scheduledAt: '2024-01-01T10:00:00Z', + }, + penaltyTypes: [], + }; + + const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto); + + expect(result.incident.description).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.test.ts new file mode 100644 index 000000000..3fde61ef3 --- /dev/null +++ b/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.test.ts @@ -0,0 +1,393 @@ +import { describe, it, expect } from 'vitest'; +import { RaceDetailViewDataBuilder } from './RaceDetailViewDataBuilder'; + +describe('RaceDetailViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform API DTO to RaceDetailViewData correctly', () => { + const apiDto = { + race: { + id: 'race-123', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + league: { + id: 'league-456', + name: 'Test League', + description: 'Test Description', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Open', + }, + }, + entryList: [ + { + id: 'driver-1', + name: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + rating: 1500, + isCurrentUser: false, + }, + ], + registration: { + isUserRegistered: false, + canRegister: true, + }, + userResult: { + position: 5, + startPosition: 10, + positionChange: 5, + incidents: 2, + isClean: false, + isPodium: false, + ratingChange: 10, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + race: { + id: 'race-123', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + league: { + id: 'league-456', + name: 'Test League', + description: 'Test Description', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Open', + }, + }, + entryList: [ + { + id: 'driver-1', + name: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + rating: 1500, + isCurrentUser: false, + }, + ], + registration: { + isUserRegistered: false, + canRegister: true, + }, + userResult: { + position: 5, + startPosition: 10, + positionChange: 5, + incidents: 2, + isClean: false, + isPodium: false, + ratingChange: 10, + }, + canReopenRace: false, + }); + }); + + it('should handle race without league', () => { + const apiDto = { + race: { + id: 'race-456', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.league).toBeUndefined(); + }); + + it('should handle race without user result', () => { + const apiDto = { + race: { + id: 'race-789', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.userResult).toBeUndefined(); + }); + + it('should handle multiple entries in entry list', () => { + const apiDto = { + race: { + id: 'race-101', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + entryList: [ + { + id: 'driver-1', + name: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + rating: 1500, + isCurrentUser: false, + }, + { + id: 'driver-2', + name: 'Driver 2', + avatarUrl: 'avatar-url', + country: 'UK', + rating: 1600, + isCurrentUser: true, + }, + ], + registration: { + isUserRegistered: true, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.entryList).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto = { + race: { + id: 'race-102', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + league: { + id: 'league-103', + name: 'Test League', + description: 'Test Description', + settings: { + maxDrivers: 32, + qualifyingFormat: 'Open', + }, + }, + entryList: [ + { + id: 'driver-1', + name: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + rating: 1500, + isCurrentUser: false, + }, + ], + registration: { + isUserRegistered: false, + canRegister: true, + }, + userResult: { + position: 5, + startPosition: 10, + positionChange: 5, + incidents: 2, + isClean: false, + isPodium: false, + ratingChange: 10, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.race.id).toBe(apiDto.race.id); + expect(result.race.track).toBe(apiDto.race.track); + expect(result.race.car).toBe(apiDto.race.car); + expect(result.race.scheduledAt).toBe(apiDto.race.scheduledAt); + expect(result.race.status).toBe(apiDto.race.status); + expect(result.race.sessionType).toBe(apiDto.race.sessionType); + expect(result.league?.id).toBe(apiDto.league.id); + expect(result.league?.name).toBe(apiDto.league.name); + expect(result.registration.isUserRegistered).toBe(apiDto.registration.isUserRegistered); + expect(result.registration.canRegister).toBe(apiDto.registration.canRegister); + expect(result.canReopenRace).toBe(apiDto.canReopenRace); + }); + + it('should not modify the input DTO', () => { + const apiDto = { + race: { + id: 'race-104', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const originalDto = { ...apiDto }; + RaceDetailViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle null API DTO', () => { + const result = RaceDetailViewDataBuilder.build(null); + + expect(result.race.id).toBe(''); + expect(result.race.track).toBe(''); + expect(result.race.car).toBe(''); + expect(result.race.scheduledAt).toBe(''); + expect(result.race.status).toBe('scheduled'); + expect(result.race.sessionType).toBe('race'); + expect(result.entryList).toHaveLength(0); + expect(result.registration.isUserRegistered).toBe(false); + expect(result.registration.canRegister).toBe(false); + expect(result.canReopenRace).toBe(false); + }); + + it('should handle undefined API DTO', () => { + const result = RaceDetailViewDataBuilder.build(undefined); + + expect(result.race.id).toBe(''); + expect(result.race.track).toBe(''); + expect(result.race.car).toBe(''); + expect(result.race.scheduledAt).toBe(''); + expect(result.race.status).toBe('scheduled'); + expect(result.race.sessionType).toBe('race'); + expect(result.entryList).toHaveLength(0); + expect(result.registration.isUserRegistered).toBe(false); + expect(result.registration.canRegister).toBe(false); + expect(result.canReopenRace).toBe(false); + }); + + it('should handle race without entry list', () => { + const apiDto = { + race: { + id: 'race-105', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'race', + }, + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.entryList).toHaveLength(0); + }); + + it('should handle different race statuses', () => { + const apiDto = { + race: { + id: 'race-106', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'running', + sessionType: 'race', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.race.status).toBe('running'); + }); + + it('should handle different session types', () => { + const apiDto = { + race: { + id: 'race-107', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + sessionType: 'qualifying', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: false, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.race.sessionType).toBe('qualifying'); + }); + + it('should handle canReopenRace true', () => { + const apiDto = { + race: { + id: 'race-108', + track: 'Test Track', + car: 'Test Car', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'completed', + sessionType: 'race', + }, + entryList: [], + registration: { + isUserRegistered: false, + canRegister: false, + }, + canReopenRace: true, + }; + + const result = RaceDetailViewDataBuilder.build(apiDto); + + expect(result.canReopenRace).toBe(true); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.test.ts new file mode 100644 index 000000000..6f09ff8f8 --- /dev/null +++ b/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.test.ts @@ -0,0 +1,775 @@ +import { describe, it, expect } from 'vitest'; +import { RaceResultsViewDataBuilder } from './RaceResultsViewDataBuilder'; + +describe('RaceResultsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform API DTO to RaceResultsViewData correctly', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [ + { + driverId: 'driver-2', + driverName: 'Driver 2', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + pointsSystem: { + 1: 25, + 2: 18, + 3: 15, + }, + fastestLapTime: 120000, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + raceTrack: 'Test Track', + raceScheduledAt: '2024-01-01T10:00:00Z', + totalDrivers: 20, + leagueName: 'Test League', + raceSOF: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + driverAvatar: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [ + { + driverId: 'driver-2', + driverName: 'Driver 2', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + pointsSystem: { + 1: 25, + 2: 18, + 3: 15, + }, + fastestLapTime: 120000, + }); + }); + + it('should handle empty results and penalties', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 0, + }, + league: { + name: 'Test League', + }, + strengthOfField: null, + results: [], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.raceSOF).toBeNull(); + }); + + it('should handle multiple results and penalties', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + { + position: 2, + driverId: 'driver-2', + driverName: 'Driver 2', + avatarUrl: 'avatar-url', + country: 'UK', + car: 'Test Car', + laps: 30, + time: '1:24.000', + fastestLap: '1:21.000', + points: 18, + incidents: 1, + isCurrentUser: true, + }, + ], + penalties: [ + { + driverId: 'driver-3', + driverName: 'Driver 3', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + { + driverId: 'driver-4', + driverName: 'Driver 4', + type: 'grid_penalty', + value: 3, + reason: 'Qualifying infringement', + notes: null, + }, + ], + pointsSystem: { + 1: 25, + 2: 18, + 3: 15, + }, + fastestLapTime: 120000, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results).toHaveLength(2); + expect(result.penalties).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: { + 1: 25, + 2: 18, + 3: 15, + }, + fastestLapTime: 120000, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.raceTrack).toBe(apiDto.race.track); + expect(result.raceScheduledAt).toBe(apiDto.race.scheduledAt); + expect(result.totalDrivers).toBe(apiDto.stats.totalDrivers); + expect(result.leagueName).toBe(apiDto.league.name); + expect(result.raceSOF).toBe(apiDto.strengthOfField); + expect(result.pointsSystem).toEqual(apiDto.pointsSystem); + expect(result.fastestLapTime).toBe(apiDto.fastestLapTime); + }); + + it('should not modify the input DTO', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const originalDto = { ...apiDto }; + RaceResultsViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle null API DTO', () => { + const result = RaceResultsViewDataBuilder.build(null); + + expect(result.raceSOF).toBeNull(); + expect(result.results).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.pointsSystem).toEqual({}); + expect(result.fastestLapTime).toBe(0); + }); + + it('should handle undefined API DTO', () => { + const result = RaceResultsViewDataBuilder.build(undefined); + + expect(result.raceSOF).toBeNull(); + expect(result.results).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.pointsSystem).toEqual({}); + expect(result.fastestLapTime).toBe(0); + }); + + it('should handle results without country', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: null, + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].country).toBe('US'); + }); + + it('should handle results without car', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: null, + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].car).toBe('Unknown'); + }); + + it('should handle results without laps', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: null, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].laps).toBe(0); + }); + + it('should handle results without time', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: null, + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].time).toBe('0:00.00'); + }); + + it('should handle results without fastest lap', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: null, + points: 25, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].fastestLap).toBe('0.00'); + }); + + it('should handle results without points', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: null, + incidents: 0, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].points).toBe(0); + }); + + it('should handle results without incidents', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: null, + isCurrentUser: false, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].incidents).toBe(0); + }); + + it('should handle results without isCurrentUser', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [ + { + position: 1, + driverId: 'driver-1', + driverName: 'Driver 1', + avatarUrl: 'avatar-url', + country: 'US', + car: 'Test Car', + laps: 30, + time: '1:23.456', + fastestLap: '1:20.000', + points: 25, + incidents: 0, + isCurrentUser: null, + }, + ], + penalties: [], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.results[0].isCurrentUser).toBe(false); + }); + + it('should handle penalties without driver name', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [], + penalties: [ + { + driverId: 'driver-1', + driverName: null, + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.penalties[0].driverName).toBe('Unknown'); + }); + + it('should handle penalties without value', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [], + penalties: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + type: 'time_penalty', + value: null, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.penalties[0].value).toBe(0); + }); + + it('should handle penalties without reason', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [], + penalties: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + type: 'time_penalty', + value: 5, + reason: null, + notes: 'Warning issued', + }, + ], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.penalties[0].reason).toBe('Penalty applied'); + }); + + it('should handle different penalty types', () => { + const apiDto = { + race: { + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + stats: { + totalDrivers: 20, + }, + league: { + name: 'Test League', + }, + strengthOfField: 1500, + results: [], + penalties: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + type: 'grid_penalty', + value: 3, + reason: 'Qualifying infringement', + notes: null, + }, + { + driverId: 'driver-2', + driverName: 'Driver 2', + type: 'points_deduction', + value: 10, + reason: 'Dangerous driving', + notes: null, + }, + { + driverId: 'driver-3', + driverName: 'Driver 3', + type: 'disqualification', + value: 0, + reason: 'Technical infringement', + notes: null, + }, + { + driverId: 'driver-4', + driverName: 'Driver 4', + type: 'warning', + value: 0, + reason: 'Minor infraction', + notes: null, + }, + { + driverId: 'driver-5', + driverName: 'Driver 5', + type: 'license_points', + value: 2, + reason: 'Multiple incidents', + notes: null, + }, + ], + pointsSystem: {}, + fastestLapTime: 0, + }; + + const result = RaceResultsViewDataBuilder.build(apiDto); + + expect(result.penalties[0].type).toBe('grid_penalty'); + expect(result.penalties[1].type).toBe('points_deduction'); + expect(result.penalties[2].type).toBe('disqualification'); + expect(result.penalties[3].type).toBe('warning'); + expect(result.penalties[4].type).toBe('license_points'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.test.ts new file mode 100644 index 000000000..c3292c5f3 --- /dev/null +++ b/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.test.ts @@ -0,0 +1,841 @@ +import { describe, it, expect } from 'vitest'; +import { RaceStewardingViewDataBuilder } from './RaceStewardingViewDataBuilder'; + +describe('RaceStewardingViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform API DTO to RaceStewardingViewData correctly', () => { + const apiDto = { + race: { + id: 'race-123', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-456', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [ + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-4', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: 'Penalty applied', + }, + ], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-5', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + }, + pendingCount: 1, + resolvedCount: 1, + penaltiesCount: 1, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + race: { + id: 'race-123', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-456', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [ + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-4', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: 'Penalty applied', + }, + ], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-5', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + }, + pendingCount: 1, + resolvedCount: 1, + penaltiesCount: 1, + }); + }); + + it('should handle empty protests and penalties', () => { + const apiDto = { + race: { + id: 'race-456', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-789', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [], + driverMap: {}, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingProtests).toHaveLength(0); + expect(result.resolvedProtests).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.pendingCount).toBe(0); + expect(result.resolvedCount).toBe(0); + expect(result.penaltiesCount).toBe(0); + }); + + it('should handle multiple protests and penalties', () => { + const apiDto = { + race: { + id: 'race-789', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-101', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-4', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [ + { + id: 'protest-3', + protestingDriverId: 'driver-5', + accusedDriverId: 'driver-6', + incident: { + lap: 15, + description: 'Contact at turn 7', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: 'Penalty applied', + }, + ], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-7', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + { + id: 'penalty-2', + driverId: 'driver-8', + type: 'grid_penalty', + value: 3, + reason: 'Qualifying infringement', + notes: null, + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + 'driver-6': { id: 'driver-6', name: 'Driver 6' }, + 'driver-7': { id: 'driver-7', name: 'Driver 7' }, + 'driver-8': { id: 'driver-8', name: 'Driver 8' }, + }, + pendingCount: 2, + resolvedCount: 1, + penaltiesCount: 2, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingProtests).toHaveLength(2); + expect(result.resolvedProtests).toHaveLength(1); + expect(result.penalties).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto = { + race: { + id: 'race-102', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-103', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [], + penalties: [], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + }, + pendingCount: 1, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.race?.id).toBe(apiDto.race.id); + expect(result.race?.track).toBe(apiDto.race.track); + expect(result.race?.scheduledAt).toBe(apiDto.race.scheduledAt); + expect(result.league?.id).toBe(apiDto.league.id); + expect(result.pendingCount).toBe(apiDto.pendingCount); + expect(result.resolvedCount).toBe(apiDto.resolvedCount); + expect(result.penaltiesCount).toBe(apiDto.penaltiesCount); + }); + + it('should not modify the input DTO', () => { + const apiDto = { + race: { + id: 'race-104', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-105', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [], + driverMap: {}, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const originalDto = { ...apiDto }; + RaceStewardingViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle null API DTO', () => { + const result = RaceStewardingViewDataBuilder.build(null); + + expect(result.race).toBeNull(); + expect(result.league).toBeNull(); + expect(result.pendingProtests).toHaveLength(0); + expect(result.resolvedProtests).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.driverMap).toEqual({}); + expect(result.pendingCount).toBe(0); + expect(result.resolvedCount).toBe(0); + expect(result.penaltiesCount).toBe(0); + }); + + it('should handle undefined API DTO', () => { + const result = RaceStewardingViewDataBuilder.build(undefined); + + expect(result.race).toBeNull(); + expect(result.league).toBeNull(); + expect(result.pendingProtests).toHaveLength(0); + expect(result.resolvedProtests).toHaveLength(0); + expect(result.penalties).toHaveLength(0); + expect(result.driverMap).toEqual({}); + expect(result.pendingCount).toBe(0); + expect(result.resolvedCount).toBe(0); + expect(result.penaltiesCount).toBe(0); + }); + + it('should handle race without league', () => { + const apiDto = { + race: { + id: 'race-106', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [], + driverMap: {}, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.league).toBeNull(); + }); + + it('should handle protests without proof video', () => { + const apiDto = { + race: { + id: 'race-107', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-108', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: null, + decisionNotes: null, + }, + ], + resolvedProtests: [], + penalties: [], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + }, + pendingCount: 1, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingProtests[0].proofVideoUrl).toBeNull(); + }); + + it('should handle protests without decision notes', () => { + const apiDto = { + race: { + id: 'race-109', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-110', + }, + pendingProtests: [], + resolvedProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + penalties: [], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + }, + pendingCount: 0, + resolvedCount: 1, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.resolvedProtests[0].decisionNotes).toBeNull(); + }); + + it('should handle penalties without notes', () => { + const apiDto = { + race: { + id: 'race-111', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-112', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: null, + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + }, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 1, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.penalties[0].notes).toBeNull(); + }); + + it('should handle penalties without value', () => { + const apiDto = { + race: { + id: 'race-113', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-114', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'disqualification', + value: null, + reason: 'Technical infringement', + notes: null, + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + }, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 1, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.penalties[0].value).toBe(0); + }); + + it('should handle penalties without reason', () => { + const apiDto = { + race: { + id: 'race-115', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-116', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'warning', + value: 0, + reason: null, + notes: null, + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + }, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 1, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.penalties[0].reason).toBe(''); + }); + + it('should handle different protest statuses', () => { + const apiDto = { + race: { + id: 'race-117', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-118', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [ + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-4', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: 'Penalty applied', + }, + { + id: 'protest-3', + protestingDriverId: 'driver-5', + accusedDriverId: 'driver-6', + incident: { + lap: 15, + description: 'Contact at turn 7', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'rejected', + proofVideoUrl: 'video-url', + decisionNotes: 'Insufficient evidence', + }, + ], + penalties: [], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + 'driver-6': { id: 'driver-6', name: 'Driver 6' }, + }, + pendingCount: 1, + resolvedCount: 2, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingProtests[0].status).toBe('pending'); + expect(result.resolvedProtests[0].status).toBe('resolved'); + expect(result.resolvedProtests[1].status).toBe('rejected'); + }); + + it('should handle different penalty types', () => { + const apiDto = { + race: { + id: 'race-119', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-120', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + { + id: 'penalty-2', + driverId: 'driver-2', + type: 'grid_penalty', + value: 3, + reason: 'Qualifying infringement', + notes: null, + }, + { + id: 'penalty-3', + driverId: 'driver-3', + type: 'points_deduction', + value: 10, + reason: 'Dangerous driving', + notes: null, + }, + { + id: 'penalty-4', + driverId: 'driver-4', + type: 'disqualification', + value: 0, + reason: 'Technical infringement', + notes: null, + }, + { + id: 'penalty-5', + driverId: 'driver-5', + type: 'warning', + value: 0, + reason: 'Minor infraction', + notes: null, + }, + { + id: 'penalty-6', + driverId: 'driver-6', + type: 'license_points', + value: 2, + reason: 'Multiple incidents', + notes: null, + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + 'driver-6': { id: 'driver-6', name: 'Driver 6' }, + }, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 6, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.penalties[0].type).toBe('time_penalty'); + expect(result.penalties[1].type).toBe('grid_penalty'); + expect(result.penalties[2].type).toBe('points_deduction'); + expect(result.penalties[3].type).toBe('disqualification'); + expect(result.penalties[4].type).toBe('warning'); + expect(result.penalties[5].type).toBe('license_points'); + }); + + it('should handle empty driver map', () => { + const apiDto = { + race: { + id: 'race-121', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-122', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [], + driverMap: {}, + pendingCount: 0, + resolvedCount: 0, + penaltiesCount: 0, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.driverMap).toEqual({}); + }); + + it('should handle count values from DTO', () => { + const apiDto = { + race: { + id: 'race-123', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-124', + }, + pendingProtests: [], + resolvedProtests: [], + penalties: [], + driverMap: {}, + pendingCount: 5, + resolvedCount: 10, + penaltiesCount: 3, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingCount).toBe(5); + expect(result.resolvedCount).toBe(10); + expect(result.penaltiesCount).toBe(3); + }); + + it('should calculate counts from arrays when not provided', () => { + const apiDto = { + race: { + id: 'race-125', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + }, + league: { + id: 'league-126', + }, + pendingProtests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'pending', + proofVideoUrl: 'video-url', + decisionNotes: null, + }, + ], + resolvedProtests: [ + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-4', + incident: { + lap: 10, + description: 'Contact at turn 5', + }, + filedAt: '2024-01-01T10:00:00Z', + status: 'resolved', + proofVideoUrl: 'video-url', + decisionNotes: 'Penalty applied', + }, + ], + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-5', + type: 'time_penalty', + value: 5, + reason: 'Track limits', + notes: 'Warning issued', + }, + ], + driverMap: { + 'driver-1': { id: 'driver-1', name: 'Driver 1' }, + 'driver-2': { id: 'driver-2', name: 'Driver 2' }, + 'driver-3': { id: 'driver-3', name: 'Driver 3' }, + 'driver-4': { id: 'driver-4', name: 'Driver 4' }, + 'driver-5': { id: 'driver-5', name: 'Driver 5' }, + }, + }; + + const result = RaceStewardingViewDataBuilder.build(apiDto); + + expect(result.pendingCount).toBe(1); + expect(result.resolvedCount).toBe(1); + expect(result.penaltiesCount).toBe(1); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/RulebookViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/RulebookViewDataBuilder.test.ts new file mode 100644 index 000000000..b408ded07 --- /dev/null +++ b/apps/website/lib/builders/view-data/RulebookViewDataBuilder.test.ts @@ -0,0 +1,407 @@ +import { describe, it, expect } from 'vitest'; +import { RulebookViewDataBuilder } from './RulebookViewDataBuilder'; +import type { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto'; + +describe('RulebookViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform RulebookApiDto to RulebookViewData correctly', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-123', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + { sessionType: 'race', position: 2, points: 18 }, + { sessionType: 'race', position: 3, points: 15 }, + ], + bonusSummary: [ + { type: 'fastest_lap', points: 5, description: 'Fastest lap' }, + ], + }, + ], + dropPolicySummary: 'Drop 2 worst results', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result).toEqual({ + leagueId: 'league-123', + gameName: 'iRacing', + scoringPresetName: 'Standard', + championshipsCount: 1, + sessionTypes: 'race', + dropPolicySummary: 'Drop 2 worst results', + hasActiveDropPolicy: true, + positionPoints: [ + { position: 1, points: 25 }, + { position: 2, points: 18 }, + { position: 3, points: 15 }, + ], + bonusPoints: [ + { type: 'fastest_lap', points: 5, description: 'Fastest lap' }, + ], + hasBonusPoints: true, + }); + }); + + it('should handle championship without driver type', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-456', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'team', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]); + }); + + it('should handle multiple championships', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-789', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + { + type: 'team', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.championshipsCount).toBe(2); + }); + + it('should handle empty bonus points', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-101', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.bonusPoints).toEqual([]); + expect(result.hasBonusPoints).toBe(false); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-102', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [ + { type: 'fastest_lap', points: 5, description: 'Fastest lap' }, + ], + }, + ], + dropPolicySummary: 'Drop 2 worst results', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.leagueId).toBe(rulebookApiDto.leagueId); + expect(result.gameName).toBe(rulebookApiDto.scoringConfig.gameName); + expect(result.scoringPresetName).toBe(rulebookApiDto.scoringConfig.scoringPresetName); + expect(result.dropPolicySummary).toBe(rulebookApiDto.scoringConfig.dropPolicySummary); + }); + + it('should not modify the input DTO', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-103', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const originalDto = { ...rulebookApiDto }; + RulebookViewDataBuilder.build(rulebookApiDto); + + expect(rulebookApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle empty drop policy', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-104', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: '', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.hasActiveDropPolicy).toBe(false); + }); + + it('should handle drop policy with "All" keyword', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-105', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'Drop all results', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.hasActiveDropPolicy).toBe(false); + }); + + it('should handle multiple session types', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-106', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race', 'qualifying', 'practice'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.sessionTypes).toBe('race, qualifying, practice'); + }); + + it('should handle single session type', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-107', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.sessionTypes).toBe('race'); + }); + + it('should handle empty points preview', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-108', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.positionPoints).toEqual([]); + }); + + it('should handle points preview with different session types', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-109', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + { sessionType: 'qualifying', position: 1, points: 10 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]); + }); + + it('should handle points preview with non-sequential positions', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-110', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + { sessionType: 'race', position: 3, points: 15 }, + { sessionType: 'race', position: 2, points: 18 }, + ], + bonusSummary: [], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.positionPoints).toEqual([ + { position: 1, points: 25 }, + { position: 2, points: 18 }, + { position: 3, points: 15 }, + ]); + }); + + it('should handle multiple bonus points', () => { + const rulebookApiDto: RulebookApiDto = { + leagueId: 'league-111', + scoringConfig: { + gameName: 'iRacing', + scoringPresetName: 'Standard', + championships: [ + { + type: 'driver', + sessionTypes: ['race'], + pointsPreview: [ + { sessionType: 'race', position: 1, points: 25 }, + ], + bonusSummary: [ + { type: 'fastest_lap', points: 5, description: 'Fastest lap' }, + { type: 'pole_position', points: 3, description: 'Pole position' }, + { type: 'clean_race', points: 2, description: 'Clean race' }, + ], + }, + ], + dropPolicySummary: 'No drops', + }, + }; + + const result = RulebookViewDataBuilder.build(rulebookApiDto); + + expect(result.bonusPoints).toHaveLength(3); + expect(result.hasBonusPoints).toBe(true); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.test.ts new file mode 100644 index 000000000..2ab2ed476 --- /dev/null +++ b/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.test.ts @@ -0,0 +1,223 @@ +import { describe, it, expect } from 'vitest'; +import { SponsorshipRequestsPageViewDataBuilder } from './SponsorshipRequestsPageViewDataBuilder'; +import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO'; + +describe('SponsorshipRequestsPageViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-123', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result).toEqual({ + sections: [ + { + entityType: 'driver', + entityId: 'driver-123', + entityName: 'driver', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogoUrl: 'logo-url', + message: 'Test message', + createdAtIso: '2024-01-01T10:00:00Z', + }, + ], + }, + ], + }); + }); + + it('should handle empty requests', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-456', + requests: [], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections).toHaveLength(1); + expect(result.sections[0].requests).toHaveLength(0); + }); + + it('should handle multiple requests', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'season', + entityId: 'season-789', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Sponsor 1', + sponsorLogo: 'logo-1', + message: 'Message 1', + createdAt: '2024-01-01T10:00:00Z', + }, + { + id: 'request-2', + sponsorId: 'sponsor-2', + sponsorName: 'Sponsor 2', + sponsorLogo: 'logo-2', + message: 'Message 2', + createdAt: '2024-01-02T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].requests).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-101', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].entityType).toBe(sponsorshipRequestsPageDto.entityType); + expect(result.sections[0].entityId).toBe(sponsorshipRequestsPageDto.entityId); + expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsPageDto.requests[0].id); + expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsPageDto.requests[0].sponsorId); + expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsPageDto.requests[0].sponsorName); + expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsPageDto.requests[0].sponsorLogo); + expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsPageDto.requests[0].message); + expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsPageDto.requests[0].createdAt); + }); + + it('should not modify the input DTO', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-102', + requests: [], + }; + + const originalDto = { ...sponsorshipRequestsPageDto }; + SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(sponsorshipRequestsPageDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle requests without sponsor logo', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-103', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: null, + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull(); + }); + + it('should handle requests without message', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-104', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: null, + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].requests[0].message).toBeNull(); + }); + + it('should handle different entity types', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-105', + requests: [], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].entityType).toBe('team'); + }); + + it('should handle entity name for driver type', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-106', + requests: [], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].entityName).toBe('driver'); + }); + + it('should handle entity name for team type', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-107', + requests: [], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].entityName).toBe('team'); + }); + + it('should handle entity name for season type', () => { + const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'season', + entityId: 'season-108', + requests: [], + }; + + const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto); + + expect(result.sections[0].entityName).toBe('season'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.test.ts new file mode 100644 index 000000000..e0818a5b5 --- /dev/null +++ b/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.test.ts @@ -0,0 +1,223 @@ +import { describe, it, expect } from 'vitest'; +import { SponsorshipRequestsViewDataBuilder } from './SponsorshipRequestsViewDataBuilder'; +import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO'; + +describe('SponsorshipRequestsViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-123', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result).toEqual({ + sections: [ + { + entityType: 'driver', + entityId: 'driver-123', + entityName: 'Driver', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogoUrl: 'logo-url', + message: 'Test message', + createdAtIso: '2024-01-01T10:00:00Z', + }, + ], + }, + ], + }); + }); + + it('should handle empty requests', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-456', + requests: [], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections).toHaveLength(1); + expect(result.sections[0].requests).toHaveLength(0); + }); + + it('should handle multiple requests', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'season', + entityId: 'season-789', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Sponsor 1', + sponsorLogo: 'logo-1', + message: 'Message 1', + createdAt: '2024-01-01T10:00:00Z', + }, + { + id: 'request-2', + sponsorId: 'sponsor-2', + sponsorName: 'Sponsor 2', + sponsorLogo: 'logo-2', + message: 'Message 2', + createdAt: '2024-01-02T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].requests).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-101', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].entityType).toBe(sponsorshipRequestsDto.entityType); + expect(result.sections[0].entityId).toBe(sponsorshipRequestsDto.entityId); + expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsDto.requests[0].id); + expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsDto.requests[0].sponsorId); + expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsDto.requests[0].sponsorName); + expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsDto.requests[0].sponsorLogo); + expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsDto.requests[0].message); + expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsDto.requests[0].createdAt); + }); + + it('should not modify the input DTO', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-102', + requests: [], + }; + + const originalDto = { ...sponsorshipRequestsDto }; + SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(sponsorshipRequestsDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle requests without sponsor logo', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-103', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: null, + message: 'Test message', + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull(); + }); + + it('should handle requests without message', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-104', + requests: [ + { + id: 'request-1', + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorLogo: 'logo-url', + message: null, + createdAt: '2024-01-01T10:00:00Z', + }, + ], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].requests[0].message).toBeNull(); + }); + + it('should handle different entity types', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-105', + requests: [], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].entityType).toBe('team'); + }); + + it('should handle entity name for driver type', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'driver', + entityId: 'driver-106', + requests: [], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].entityName).toBe('Driver'); + }); + + it('should handle entity name for team type', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'team', + entityId: 'team-107', + requests: [], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].entityName).toBe('team'); + }); + + it('should handle entity name for season type', () => { + const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = { + entityType: 'season', + entityId: 'season-108', + requests: [], + }; + + const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto); + + expect(result.sections[0].entityName).toBe('season'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/StewardingViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/StewardingViewDataBuilder.test.ts new file mode 100644 index 000000000..a7c59ab8d --- /dev/null +++ b/apps/website/lib/builders/view-data/StewardingViewDataBuilder.test.ts @@ -0,0 +1,349 @@ +import { describe, it, expect } from 'vitest'; +import { StewardingViewDataBuilder } from './StewardingViewDataBuilder'; +import type { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto'; + +describe('StewardingViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform StewardingApiDto to StewardingViewData correctly', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-123', + totalPending: 5, + totalResolved: 10, + totalPenalties: 3, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1', 'protest-2'], + resolvedProtests: ['protest-3'], + penalties: ['penalty-1'], + }, + ], + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + }, + ], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result).toEqual({ + leagueId: 'league-123', + totalPending: 5, + totalResolved: 10, + totalPenalties: 3, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1', 'protest-2'], + resolvedProtests: ['protest-3'], + penalties: ['penalty-1'], + }, + ], + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + }, + ], + }); + }); + + it('should handle empty races and drivers', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-456', + totalPending: 0, + totalResolved: 0, + totalPenalties: 0, + races: [], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races).toHaveLength(0); + expect(result.drivers).toHaveLength(0); + }); + + it('should handle multiple races and drivers', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-789', + totalPending: 10, + totalResolved: 20, + totalPenalties: 5, + races: [ + { + id: 'race-1', + track: 'Test Track 1', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1'], + resolvedProtests: ['protest-2'], + penalties: ['penalty-1'], + }, + { + id: 'race-2', + track: 'Test Track 2', + scheduledAt: '2024-01-02T10:00:00Z', + pendingProtests: ['protest-3'], + resolvedProtests: ['protest-4'], + penalties: ['penalty-2'], + }, + ], + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + }, + { + id: 'driver-2', + name: 'Driver 2', + }, + ], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races).toHaveLength(2); + expect(result.drivers).toHaveLength(2); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-101', + totalPending: 5, + totalResolved: 10, + totalPenalties: 3, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1'], + resolvedProtests: ['protest-2'], + penalties: ['penalty-1'], + }, + ], + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + }, + ], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.leagueId).toBe(stewardingApiDto.leagueId); + expect(result.totalPending).toBe(stewardingApiDto.totalPending); + expect(result.totalResolved).toBe(stewardingApiDto.totalResolved); + expect(result.totalPenalties).toBe(stewardingApiDto.totalPenalties); + expect(result.races).toEqual(stewardingApiDto.races); + expect(result.drivers).toEqual(stewardingApiDto.drivers); + }); + + it('should not modify the input DTO', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-102', + totalPending: 0, + totalResolved: 0, + totalPenalties: 0, + races: [], + drivers: [], + }; + + const originalDto = { ...stewardingApiDto }; + StewardingViewDataBuilder.build(stewardingApiDto); + + expect(stewardingApiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle null API DTO', () => { + const result = StewardingViewDataBuilder.build(null); + + expect(result.leagueId).toBeUndefined(); + expect(result.totalPending).toBe(0); + expect(result.totalResolved).toBe(0); + expect(result.totalPenalties).toBe(0); + expect(result.races).toHaveLength(0); + expect(result.drivers).toHaveLength(0); + }); + + it('should handle undefined API DTO', () => { + const result = StewardingViewDataBuilder.build(undefined); + + expect(result.leagueId).toBeUndefined(); + expect(result.totalPending).toBe(0); + expect(result.totalResolved).toBe(0); + expect(result.totalPenalties).toBe(0); + expect(result.races).toHaveLength(0); + expect(result.drivers).toHaveLength(0); + }); + + it('should handle races without pending protests', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-103', + totalPending: 0, + totalResolved: 5, + totalPenalties: 2, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: [], + resolvedProtests: ['protest-1'], + penalties: ['penalty-1'], + }, + ], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races[0].pendingProtests).toHaveLength(0); + }); + + it('should handle races without resolved protests', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-104', + totalPending: 5, + totalResolved: 0, + totalPenalties: 2, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1'], + resolvedProtests: [], + penalties: ['penalty-1'], + }, + ], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races[0].resolvedProtests).toHaveLength(0); + }); + + it('should handle races without penalties', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-105', + totalPending: 5, + totalResolved: 10, + totalPenalties: 0, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1'], + resolvedProtests: ['protest-2'], + penalties: [], + }, + ], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races[0].penalties).toHaveLength(0); + }); + + it('should handle races with empty arrays', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-106', + totalPending: 0, + totalResolved: 0, + totalPenalties: 0, + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: [], + resolvedProtests: [], + penalties: [], + }, + ], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.races[0].pendingProtests).toHaveLength(0); + expect(result.races[0].resolvedProtests).toHaveLength(0); + expect(result.races[0].penalties).toHaveLength(0); + }); + + it('should handle drivers without name', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-107', + totalPending: 0, + totalResolved: 0, + totalPenalties: 0, + races: [], + drivers: [ + { + id: 'driver-1', + name: null, + }, + ], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.drivers[0].name).toBeNull(); + }); + + it('should handle count values from DTO', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-108', + totalPending: 15, + totalResolved: 25, + totalPenalties: 8, + races: [], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.totalPending).toBe(15); + expect(result.totalResolved).toBe(25); + expect(result.totalPenalties).toBe(8); + }); + + it('should calculate counts from arrays when not provided', () => { + const stewardingApiDto: StewardingApiDto = { + leagueId: 'league-109', + races: [ + { + id: 'race-1', + track: 'Test Track', + scheduledAt: '2024-01-01T10:00:00Z', + pendingProtests: ['protest-1', 'protest-2'], + resolvedProtests: ['protest-3', 'protest-4', 'protest-5'], + penalties: ['penalty-1', 'penalty-2'], + }, + ], + drivers: [], + }; + + const result = StewardingViewDataBuilder.build(stewardingApiDto); + + expect(result.totalPending).toBe(2); + expect(result.totalResolved).toBe(3); + expect(result.totalPenalties).toBe(2); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.test.ts new file mode 100644 index 000000000..552679de3 --- /dev/null +++ b/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.test.ts @@ -0,0 +1,1042 @@ +import { describe, it, expect } from 'vitest'; +import { TeamDetailViewDataBuilder } from './TeamDetailViewDataBuilder'; +import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery'; + +describe('TeamDetailViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform TeamDetailPageDto to TeamDetailViewData correctly', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-123', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1', 'league-2'], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English', 'German'], + category: 'Professional', + membership: 'open', + canManage: true, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result).toEqual({ + team: { + id: 'team-123', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1', 'league-2'], + createdAt: '2024-01-01', + foundedDateLabel: 'January 2024', + specialization: 'Racing', + region: 'EU', + languages: ['English', 'German'], + category: 'Professional', + membership: 'open', + canManage: true, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + joinedAtLabel: 'Jan 1, 2024', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + isAdmin: true, + teamMetrics: [ + { + icon: 'users', + label: 'Members', + value: '1', + color: 'text-primary-blue', + }, + { + icon: 'zap', + label: 'Est. Reach', + value: '15', + color: 'text-purple-400', + }, + { + icon: 'calendar', + label: 'Races', + value: '2', + color: 'text-neon-aqua', + }, + { + icon: 'users', + label: 'Engagement', + value: '82%', + color: 'text-performance-green', + }, + ], + tabs: [ + { id: 'overview', label: 'Overview', visible: true }, + { id: 'roster', label: 'Roster', visible: true }, + { id: 'standings', label: 'Standings', visible: true }, + { id: 'admin', label: 'Admin', visible: true }, + ], + memberCountLabel: '1', + leagueCountLabel: '2', + }); + }); + + it('should handle team without leagues', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-456', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'member', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.leagues).toHaveLength(0); + expect(result.teamMetrics[2].value).toBe('0'); + expect(result.leagueCountLabel).toBe('0'); + }); + + it('should handle team without members', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-789', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1'], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships).toHaveLength(0); + expect(result.teamMetrics[0].value).toBe('0'); + expect(result.teamMetrics[1].value).toBe('0'); + expect(result.memberCountLabel).toBe('0'); + }); + + it('should handle multiple members', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-101', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1'], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-2', + driverName: 'Driver 2', + role: 'manager', + joinedAt: '2024-01-02', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-3', + driverName: 'Driver 3', + role: 'member', + joinedAt: '2024-01-03', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships).toHaveLength(3); + expect(result.teamMetrics[0].value).toBe('3'); + expect(result.teamMetrics[1].value).toBe('45'); + expect(result.memberCountLabel).toBe('3'); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-102', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1'], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: true, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.id).toBe(teamDetailPageDto.team.id); + expect(result.team.name).toBe(teamDetailPageDto.team.name); + expect(result.team.tag).toBe(teamDetailPageDto.team.tag); + expect(result.team.description).toBe(teamDetailPageDto.team.description); + expect(result.team.ownerId).toBe(teamDetailPageDto.team.ownerId); + expect(result.team.leagues).toEqual(teamDetailPageDto.team.leagues); + expect(result.team.createdAt).toBe(teamDetailPageDto.team.createdAt); + expect(result.team.specialization).toBe(teamDetailPageDto.team.specialization); + expect(result.team.region).toBe(teamDetailPageDto.team.region); + expect(result.team.languages).toEqual(teamDetailPageDto.team.languages); + expect(result.team.category).toBe(teamDetailPageDto.team.category); + expect(result.team.membership).toBe(teamDetailPageDto.team.membership); + expect(result.team.canManage).toBe(teamDetailPageDto.team.canManage); + expect(result.currentDriverId).toBe(teamDetailPageDto.currentDriverId); + }); + + it('should not modify the input DTO', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-103', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const originalDto = { ...teamDetailPageDto }; + TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(teamDetailPageDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle team without createdAt', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-104', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: null, + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.foundedDateLabel).toBe('Unknown'); + }); + + it('should handle team without description', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-105', + name: 'Test Team', + tag: 'TT', + description: null, + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.description).toBeNull(); + }); + + it('should handle team without tag', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-106', + name: 'Test Team', + tag: null, + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.tag).toBeNull(); + }); + + it('should handle team without specialization', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-107', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: null, + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.specialization).toBeNull(); + }); + + it('should handle team without region', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-108', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: null, + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.region).toBeNull(); + }); + + it('should handle team without languages', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-109', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: null, + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.languages).toBeNull(); + }); + + it('should handle team without category', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-110', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: null, + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.category).toBeNull(); + }); + + it('should handle team without membership', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-111', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: null, + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.membership).toBeNull(); + }); + + it('should handle member without avatar', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-112', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: null, + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships[0].avatarUrl).toBeNull(); + }); + + it('should handle member without role', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-113', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: null, + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships[0].role).toBeNull(); + }); + + it('should handle member without isActive', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-114', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: null, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships[0].isActive).toBeNull(); + }); + + it('should handle current driver not in members', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-115', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-2', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.isAdmin).toBe(false); + expect(result.tabs.find(t => t.id === 'admin')?.visible).toBe(false); + }); + + it('should handle current driver as manager', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-116', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'manager', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.isAdmin).toBe(true); + expect(result.tabs.find(t => t.id === 'admin')?.visible).toBe(true); + }); + + it('should handle current driver as member', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-117', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'member', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.isAdmin).toBe(false); + expect(result.tabs.find(t => t.id === 'admin')?.visible).toBe(false); + }); + + it('should handle current driver as steward', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-118', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'steward', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.isAdmin).toBe(false); + expect(result.tabs.find(t => t.id === 'admin')?.visible).toBe(false); + }); + + it('should handle different membership types', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-119', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'closed', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.membership).toBe('closed'); + }); + + it('should handle different categories', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-120', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Amateur', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.category).toBe('Amateur'); + }); + + it('should handle different specializations', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-121', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Endurance', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.specialization).toBe('Endurance'); + }); + + it('should handle different regions', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-122', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'NA', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.region).toBe('NA'); + }); + + it('should handle multiple languages', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-123', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English', 'German', 'French'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.languages).toEqual(['English', 'German', 'French']); + }); + + it('should handle empty languages array', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-124', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: [], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.languages).toEqual([]); + }); + + it('should handle empty leagues array', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-125', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.leagues).toEqual([]); + }); + + it('should handle large number of leagues', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-126', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: ['league-1', 'league-2', 'league-3', 'league-4', 'league-5'], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.team.leagues).toHaveLength(5); + expect(result.teamMetrics[2].value).toBe('5'); + expect(result.leagueCountLabel).toBe('5'); + }); + + it('should handle large number of members', () => { + const teamDetailPageDto: TeamDetailPageDto = { + team: { + id: 'team-127', + name: 'Test Team', + tag: 'TT', + description: 'Test Description', + ownerId: 'owner-1', + leagues: [], + createdAt: '2024-01-01', + specialization: 'Racing', + region: 'EU', + languages: ['English'], + category: 'Professional', + membership: 'open', + canManage: false, + }, + memberships: [ + { + driverId: 'driver-1', + driverName: 'Driver 1', + role: 'owner', + joinedAt: '2024-01-01', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-2', + driverName: 'Driver 2', + role: 'member', + joinedAt: '2024-01-02', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-3', + driverName: 'Driver 3', + role: 'member', + joinedAt: '2024-01-03', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-4', + driverName: 'Driver 4', + role: 'member', + joinedAt: '2024-01-04', + isActive: true, + avatarUrl: 'avatar-url', + }, + { + driverId: 'driver-5', + driverName: 'Driver 5', + role: 'member', + joinedAt: '2024-01-05', + isActive: true, + avatarUrl: 'avatar-url', + }, + ], + currentDriverId: 'driver-1', + }; + + const result = TeamDetailViewDataBuilder.build(teamDetailPageDto); + + expect(result.memberships).toHaveLength(5); + expect(result.teamMetrics[0].value).toBe('5'); + expect(result.teamMetrics[1].value).toBe('75'); + expect(result.memberCountLabel).toBe('5'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.test.ts b/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.test.ts new file mode 100644 index 000000000..d410eb4c4 --- /dev/null +++ b/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.test.ts @@ -0,0 +1,1304 @@ +import { describe, it, expect } from 'vitest'; +import { DriverProfileViewModelBuilder } from './DriverProfileViewModelBuilder'; +import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; + +describe('DriverProfileViewModelBuilder', () => { + describe('happy paths', () => { + it('should transform GetDriverProfileOutputDTO to DriverProfileViewModel correctly', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver).not.toBeNull(); + expect(result.currentDriver?.id).toBe('driver-123'); + expect(result.currentDriver?.name).toBe('Test Driver'); + expect(result.currentDriver?.country).toBe('US'); + expect(result.currentDriver?.avatarUrl).toBe('avatar-url'); + expect(result.currentDriver?.iracingId).toBe(12345); + expect(result.currentDriver?.joinedAt).toBe('2024-01-01'); + expect(result.currentDriver?.rating).toBe(1500); + expect(result.currentDriver?.globalRank).toBe(100); + expect(result.currentDriver?.consistency).toBe(85); + expect(result.currentDriver?.bio).toBe('Test bio'); + expect(result.currentDriver?.totalDrivers).toBeNull(); + expect(result.stats).not.toBeNull(); + expect(result.stats?.totalRaces).toBe(50); + expect(result.stats?.wins).toBe(10); + expect(result.stats?.podiums).toBe(20); + expect(result.stats?.dnfs).toBe(5); + expect(result.stats?.avgFinish).toBe(5.5); + expect(result.stats?.bestFinish).toBe(1); + expect(result.stats?.worstFinish).toBe(20); + expect(result.stats?.finishRate).toBe(90); + expect(result.stats?.winRate).toBe(20); + expect(result.stats?.podiumRate).toBe(40); + expect(result.stats?.percentile).toBe(95); + expect(result.stats?.rating).toBe(1500); + expect(result.stats?.consistency).toBe(85); + expect(result.stats?.overallRank).toBe(100); + expect(result.finishDistribution).not.toBeNull(); + expect(result.finishDistribution?.totalRaces).toBe(50); + expect(result.finishDistribution?.wins).toBe(10); + expect(result.finishDistribution?.podiums).toBe(20); + expect(result.finishDistribution?.topTen).toBe(30); + expect(result.finishDistribution?.dnfs).toBe(5); + expect(result.finishDistribution?.other).toBe(15); + expect(result.teamMemberships).toHaveLength(1); + expect(result.teamMemberships[0].teamId).toBe('team-1'); + expect(result.teamMemberships[0].teamName).toBe('Test Team'); + expect(result.teamMemberships[0].teamTag).toBe('TT'); + expect(result.teamMemberships[0].role).toBe('driver'); + expect(result.teamMemberships[0].joinedAt).toBe('2024-01-01'); + expect(result.teamMemberships[0].isCurrent).toBe(true); + expect(result.socialSummary).not.toBeNull(); + expect(result.socialSummary?.friendsCount).toBe(10); + expect(result.socialSummary?.friends).toHaveLength(1); + expect(result.socialSummary?.friends[0].id).toBe('friend-1'); + expect(result.socialSummary?.friends[0].name).toBe('Friend 1'); + expect(result.socialSummary?.friends[0].country).toBe('US'); + expect(result.socialSummary?.friends[0].avatarUrl).toBe('avatar-url'); + expect(result.extendedProfile).not.toBeNull(); + expect(result.extendedProfile?.socialHandles).toHaveLength(1); + expect(result.extendedProfile?.socialHandles[0].platform).toBe('twitter'); + expect(result.extendedProfile?.socialHandles[0].handle).toBe('@test'); + expect(result.extendedProfile?.socialHandles[0].url).toBe('https://twitter.com/test'); + expect(result.extendedProfile?.achievements).toHaveLength(1); + expect(result.extendedProfile?.achievements[0].id).toBe('ach-1'); + expect(result.extendedProfile?.achievements[0].title).toBe('Achievement'); + expect(result.extendedProfile?.achievements[0].description).toBe('Test achievement'); + expect(result.extendedProfile?.achievements[0].icon).toBe('trophy'); + expect(result.extendedProfile?.achievements[0].rarity).toBe('rare'); + expect(result.extendedProfile?.achievements[0].earnedAt).toBe('2024-01-01'); + expect(result.extendedProfile?.racingStyle).toBe('Aggressive'); + expect(result.extendedProfile?.favoriteTrack).toBe('Test Track'); + expect(result.extendedProfile?.favoriteCar).toBe('Test Car'); + expect(result.extendedProfile?.timezone).toBe('UTC'); + expect(result.extendedProfile?.availableHours).toBe(10); + expect(result.extendedProfile?.lookingForTeam).toBe(true); + expect(result.extendedProfile?.openToRequests).toBe(true); + }); + + it('should handle null driver (no profile)', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: null, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver).toBeNull(); + expect(result.stats).toBeNull(); + expect(result.finishDistribution).toBeNull(); + expect(result.teamMemberships).toHaveLength(0); + expect(result.socialSummary).not.toBeNull(); + expect(result.socialSummary?.friendsCount).toBe(0); + expect(result.socialSummary?.friends).toHaveLength(0); + expect(result.extendedProfile).toBeNull(); + }); + + it('should handle null stats', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.stats).toBeNull(); + }); + + it('should handle null finish distribution', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.finishDistribution).toBeNull(); + }); + + it('should handle null extended profile', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile).toBeNull(); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.id).toBe(driverProfileDto.currentDriver?.id); + expect(result.currentDriver?.name).toBe(driverProfileDto.currentDriver?.name); + expect(result.currentDriver?.country).toBe(driverProfileDto.currentDriver?.country); + expect(result.currentDriver?.avatarUrl).toBe(driverProfileDto.currentDriver?.avatarUrl); + expect(result.currentDriver?.iracingId).toBe(driverProfileDto.currentDriver?.iracingId); + expect(result.currentDriver?.joinedAt).toBe(driverProfileDto.currentDriver?.joinedAt); + expect(result.currentDriver?.rating).toBe(driverProfileDto.currentDriver?.rating); + expect(result.currentDriver?.globalRank).toBe(driverProfileDto.currentDriver?.globalRank); + expect(result.currentDriver?.consistency).toBe(driverProfileDto.currentDriver?.consistency); + expect(result.currentDriver?.bio).toBe(driverProfileDto.currentDriver?.bio); + expect(result.stats?.totalRaces).toBe(driverProfileDto.stats?.totalRaces); + expect(result.stats?.wins).toBe(driverProfileDto.stats?.wins); + expect(result.stats?.podiums).toBe(driverProfileDto.stats?.podiums); + expect(result.stats?.dnfs).toBe(driverProfileDto.stats?.dnfs); + expect(result.stats?.avgFinish).toBe(driverProfileDto.stats?.avgFinish); + expect(result.stats?.bestFinish).toBe(driverProfileDto.stats?.bestFinish); + expect(result.stats?.worstFinish).toBe(driverProfileDto.stats?.worstFinish); + expect(result.stats?.finishRate).toBe(driverProfileDto.stats?.finishRate); + expect(result.stats?.winRate).toBe(driverProfileDto.stats?.winRate); + expect(result.stats?.podiumRate).toBe(driverProfileDto.stats?.podiumRate); + expect(result.stats?.percentile).toBe(driverProfileDto.stats?.percentile); + expect(result.stats?.rating).toBe(driverProfileDto.stats?.rating); + expect(result.stats?.consistency).toBe(driverProfileDto.stats?.consistency); + expect(result.stats?.overallRank).toBe(driverProfileDto.stats?.overallRank); + expect(result.finishDistribution?.totalRaces).toBe(driverProfileDto.finishDistribution?.totalRaces); + expect(result.finishDistribution?.wins).toBe(driverProfileDto.finishDistribution?.wins); + expect(result.finishDistribution?.podiums).toBe(driverProfileDto.finishDistribution?.podiums); + expect(result.finishDistribution?.topTen).toBe(driverProfileDto.finishDistribution?.topTen); + expect(result.finishDistribution?.dnfs).toBe(driverProfileDto.finishDistribution?.dnfs); + expect(result.finishDistribution?.other).toBe(driverProfileDto.finishDistribution?.other); + expect(result.teamMemberships).toHaveLength(1); + expect(result.teamMemberships[0].teamId).toBe(driverProfileDto.teamMemberships[0].teamId); + expect(result.teamMemberships[0].teamName).toBe(driverProfileDto.teamMemberships[0].teamName); + expect(result.teamMemberships[0].teamTag).toBe(driverProfileDto.teamMemberships[0].teamTag); + expect(result.teamMemberships[0].role).toBe(driverProfileDto.teamMemberships[0].role); + expect(result.teamMemberships[0].joinedAt).toBe(driverProfileDto.teamMemberships[0].joinedAt); + expect(result.teamMemberships[0].isCurrent).toBe(driverProfileDto.teamMemberships[0].isCurrent); + expect(result.socialSummary?.friendsCount).toBe(driverProfileDto.socialSummary.friendsCount); + expect(result.socialSummary?.friends).toHaveLength(1); + expect(result.socialSummary?.friends[0].id).toBe(driverProfileDto.socialSummary.friends[0].id); + expect(result.socialSummary?.friends[0].name).toBe(driverProfileDto.socialSummary.friends[0].name); + expect(result.socialSummary?.friends[0].country).toBe(driverProfileDto.socialSummary.friends[0].country); + expect(result.socialSummary?.friends[0].avatarUrl).toBe(driverProfileDto.socialSummary.friends[0].avatarUrl); + expect(result.extendedProfile?.socialHandles).toHaveLength(1); + expect(result.extendedProfile?.socialHandles[0].platform).toBe(driverProfileDto.extendedProfile?.socialHandles[0].platform); + expect(result.extendedProfile?.socialHandles[0].handle).toBe(driverProfileDto.extendedProfile?.socialHandles[0].handle); + expect(result.extendedProfile?.socialHandles[0].url).toBe(driverProfileDto.extendedProfile?.socialHandles[0].url); + expect(result.extendedProfile?.achievements).toHaveLength(1); + expect(result.extendedProfile?.achievements[0].id).toBe(driverProfileDto.extendedProfile?.achievements[0].id); + expect(result.extendedProfile?.achievements[0].title).toBe(driverProfileDto.extendedProfile?.achievements[0].title); + expect(result.extendedProfile?.achievements[0].description).toBe(driverProfileDto.extendedProfile?.achievements[0].description); + expect(result.extendedProfile?.achievements[0].icon).toBe(driverProfileDto.extendedProfile?.achievements[0].icon); + expect(result.extendedProfile?.achievements[0].rarity).toBe(driverProfileDto.extendedProfile?.achievements[0].rarity); + expect(result.extendedProfile?.achievements[0].earnedAt).toBe(driverProfileDto.extendedProfile?.achievements[0].earnedAt); + expect(result.extendedProfile?.racingStyle).toBe(driverProfileDto.extendedProfile?.racingStyle); + expect(result.extendedProfile?.favoriteTrack).toBe(driverProfileDto.extendedProfile?.favoriteTrack); + expect(result.extendedProfile?.favoriteCar).toBe(driverProfileDto.extendedProfile?.favoriteCar); + expect(result.extendedProfile?.timezone).toBe(driverProfileDto.extendedProfile?.timezone); + expect(result.extendedProfile?.availableHours).toBe(driverProfileDto.extendedProfile?.availableHours); + expect(result.extendedProfile?.lookingForTeam).toBe(driverProfileDto.extendedProfile?.lookingForTeam); + expect(result.extendedProfile?.openToRequests).toBe(driverProfileDto.extendedProfile?.openToRequests); + }); + + it('should not modify the input DTO', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: 'Test bio', + iracingId: 12345, + joinedAt: '2024-01-01', + globalRank: 100, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: 5.5, + bestFinish: 1, + worstFinish: 20, + finishRate: 90, + winRate: 20, + podiumRate: 40, + percentile: 95, + rating: 1500, + consistency: 85, + overallRank: 100, + }, + finishDistribution: { + totalRaces: 50, + wins: 10, + podiums: 20, + topTen: 30, + dnfs: 5, + other: 15, + }, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: 'TT', + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 10, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: 'avatar-url', + }, + ], + }, + extendedProfile: { + socialHandles: [ + { + platform: 'twitter', + handle: '@test', + url: 'https://twitter.com/test', + }, + ], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: 'Aggressive', + favoriteTrack: 'Test Track', + favoriteCar: 'Test Car', + timezone: 'UTC', + availableHours: 10, + lookingForTeam: true, + openToRequests: true, + }, + }; + + const originalDto = { ...driverProfileDto }; + DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(driverProfileDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle driver without avatar', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: null, + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.avatarUrl).toBe(''); + }); + + it('should handle driver without iracingId', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.iracingId).toBeNull(); + }); + + it('should handle driver without global rank', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.globalRank).toBeNull(); + }); + + it('should handle driver without rating', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + rating: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.rating).toBeNull(); + }); + + it('should handle driver without consistency', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + consistency: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.consistency).toBeNull(); + }); + + it('should handle driver without bio', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.currentDriver?.bio).toBeNull(); + }); + + it('should handle stats without avgFinish', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: { + totalRaces: 50, + wins: 10, + podiums: 20, + dnfs: 5, + avgFinish: null, + bestFinish: null, + worstFinish: null, + finishRate: null, + winRate: null, + podiumRate: null, + percentile: null, + rating: null, + consistency: null, + overallRank: null, + }, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.stats?.avgFinish).toBeNull(); + expect(result.stats?.bestFinish).toBeNull(); + expect(result.stats?.worstFinish).toBeNull(); + 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?.consistency).toBeNull(); + expect(result.stats?.overallRank).toBeNull(); + }); + + it('should handle empty team memberships', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.teamMemberships).toHaveLength(0); + }); + + it('should handle team membership without teamTag', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [ + { + teamId: 'team-1', + teamName: 'Test Team', + teamTag: null, + role: 'driver', + joinedAt: '2024-01-01', + isCurrent: true, + }, + ], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.teamMemberships[0].teamTag).toBeNull(); + }); + + it('should handle empty friends list', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.socialSummary?.friends).toHaveLength(0); + expect(result.socialSummary?.friendsCount).toBe(0); + }); + + it('should handle friend without avatar', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 1, + friends: [ + { + id: 'friend-1', + name: 'Friend 1', + country: 'US', + avatarUrl: null, + }, + ], + }, + extendedProfile: null, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.socialSummary?.friends[0].avatarUrl).toBe(''); + }); + + it('should handle empty social handles', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.socialHandles).toHaveLength(0); + }); + + it('should handle empty achievements', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.achievements).toHaveLength(0); + }); + + it('should handle achievement without icon', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: null, + rarity: 'rare', + earnedAt: '2024-01-01', + }, + ], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.achievements[0].icon).toBeNull(); + }); + + it('should handle achievement without rarity', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [ + { + id: 'ach-1', + title: 'Achievement', + description: 'Test achievement', + icon: 'trophy', + rarity: null, + earnedAt: '2024-01-01', + }, + ], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.achievements[0].rarity).toBeNull(); + }); + + it('should handle extended profile without racingStyle', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.racingStyle).toBeNull(); + }); + + it('should handle extended profile without favoriteTrack', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.favoriteTrack).toBeNull(); + }); + + it('should handle extended profile without favoriteCar', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.favoriteCar).toBeNull(); + }); + + it('should handle extended profile without timezone', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.timezone).toBeNull(); + }); + + it('should handle extended profile without availableHours', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.availableHours).toBeNull(); + }); + + it('should handle extended profile with lookingForTeam false', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.lookingForTeam).toBe(false); + }); + + it('should handle extended profile with openToRequests false', () => { + const driverProfileDto: GetDriverProfileOutputDTO = { + currentDriver: { + id: 'driver-123', + name: 'Test Driver', + country: 'US', + avatarUrl: 'avatar-url', + bio: null, + iracingId: null, + joinedAt: '2024-01-01', + globalRank: null, + }, + stats: null, + finishDistribution: null, + teamMemberships: [], + socialSummary: { + friendsCount: 0, + friends: [], + }, + extendedProfile: { + socialHandles: [], + achievements: [], + racingStyle: null, + favoriteTrack: null, + favoriteCar: null, + timezone: null, + availableHours: null, + lookingForTeam: false, + openToRequests: false, + }, + }; + + const result = DriverProfileViewModelBuilder.build(driverProfileDto); + + expect(result.extendedProfile?.openToRequests).toBe(false); + }); + }); +}); diff --git a/apps/website/lib/builders/view-models/DriversViewModelBuilder.test.ts b/apps/website/lib/builders/view-models/DriversViewModelBuilder.test.ts new file mode 100644 index 000000000..b65e917cf --- /dev/null +++ b/apps/website/lib/builders/view-models/DriversViewModelBuilder.test.ts @@ -0,0 +1,449 @@ +import { describe, it, expect } from 'vitest'; +import { DriversViewModelBuilder } from './DriversViewModelBuilder'; +import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; + +describe('DriversViewModelBuilder', () => { + describe('happy paths', () => { + it('should transform DriversLeaderboardDTO to DriverLeaderboardViewModel correctly', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers).toHaveLength(2); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[0].name).toBe('Driver 1'); + expect(result.drivers[0].country).toBe('US'); + expect(result.drivers[0].avatarUrl).toBe('avatar-url'); + expect(result.drivers[0].rating).toBe(1500); + expect(result.drivers[0].globalRank).toBe(1); + expect(result.drivers[0].consistency).toBe(95); + expect(result.drivers[1].id).toBe('driver-2'); + expect(result.drivers[1].name).toBe('Driver 2'); + expect(result.drivers[1].country).toBe('UK'); + expect(result.drivers[1].avatarUrl).toBe('avatar-url'); + expect(result.drivers[1].rating).toBe(1450); + expect(result.drivers[1].globalRank).toBe(2); + expect(result.drivers[1].consistency).toBe(90); + }); + + it('should handle empty drivers array', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers).toHaveLength(0); + }); + + it('should handle single driver', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers).toHaveLength(1); + }); + + it('should handle multiple drivers', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + { + id: 'driver-3', + name: 'Driver 3', + country: 'DE', + avatarUrl: 'avatar-url', + rating: 1400, + globalRank: 3, + consistency: 85, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers).toHaveLength(3); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].id).toBe(driversLeaderboardDto.drivers[0].id); + expect(result.drivers[0].name).toBe(driversLeaderboardDto.drivers[0].name); + expect(result.drivers[0].country).toBe(driversLeaderboardDto.drivers[0].country); + expect(result.drivers[0].avatarUrl).toBe(driversLeaderboardDto.drivers[0].avatarUrl); + expect(result.drivers[0].rating).toBe(driversLeaderboardDto.drivers[0].rating); + expect(result.drivers[0].globalRank).toBe(driversLeaderboardDto.drivers[0].globalRank); + expect(result.drivers[0].consistency).toBe(driversLeaderboardDto.drivers[0].consistency); + }); + + it('should not modify the input DTO', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const originalDto = { ...driversLeaderboardDto }; + DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(driversLeaderboardDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle driver without avatar', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: null, + rating: 1500, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].avatarUrl).toBeNull(); + }); + + it('should handle driver without country', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: null, + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].country).toBeNull(); + }); + + it('should handle driver without rating', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: null, + globalRank: 1, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].rating).toBeNull(); + }); + + it('should handle driver without global rank', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: null, + consistency: 95, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].globalRank).toBeNull(); + }); + + it('should handle driver without consistency', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: null, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].consistency).toBeNull(); + }); + + it('should handle different countries', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + { + id: 'driver-3', + name: 'Driver 3', + country: 'DE', + avatarUrl: 'avatar-url', + rating: 1400, + globalRank: 3, + consistency: 85, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].country).toBe('US'); + expect(result.drivers[1].country).toBe('UK'); + expect(result.drivers[2].country).toBe('DE'); + }); + + it('should handle different ratings', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + { + id: 'driver-3', + name: 'Driver 3', + country: 'DE', + avatarUrl: 'avatar-url', + rating: 1400, + globalRank: 3, + consistency: 85, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].rating).toBe(1500); + expect(result.drivers[1].rating).toBe(1450); + expect(result.drivers[2].rating).toBe(1400); + }); + + it('should handle different global ranks', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + { + id: 'driver-3', + name: 'Driver 3', + country: 'DE', + avatarUrl: 'avatar-url', + rating: 1400, + globalRank: 3, + consistency: 85, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].globalRank).toBe(1); + expect(result.drivers[1].globalRank).toBe(2); + expect(result.drivers[2].globalRank).toBe(3); + }); + + it('should handle different consistency values', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: [ + { + id: 'driver-1', + name: 'Driver 1', + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500, + globalRank: 1, + consistency: 95, + }, + { + id: 'driver-2', + name: 'Driver 2', + country: 'UK', + avatarUrl: 'avatar-url', + rating: 1450, + globalRank: 2, + consistency: 90, + }, + { + id: 'driver-3', + name: 'Driver 3', + country: 'DE', + avatarUrl: 'avatar-url', + rating: 1400, + globalRank: 3, + consistency: 85, + }, + ], + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers[0].consistency).toBe(95); + expect(result.drivers[1].consistency).toBe(90); + expect(result.drivers[2].consistency).toBe(85); + }); + + it('should handle large number of drivers', () => { + const driversLeaderboardDto: DriversLeaderboardDTO = { + drivers: Array.from({ length: 100 }, (_, i) => ({ + id: `driver-${i + 1}`, + name: `Driver ${i + 1}`, + country: 'US', + avatarUrl: 'avatar-url', + rating: 1500 - i, + globalRank: i + 1, + consistency: 95 - i * 0.1, + })), + }; + + const result = DriversViewModelBuilder.build(driversLeaderboardDto); + + expect(result.drivers).toHaveLength(100); + expect(result.drivers[0].id).toBe('driver-1'); + expect(result.drivers[99].id).toBe('driver-100'); + }); + }); +});