diff --git a/apps/api/src/domain/admin/presenters/DashboardStatsPresenter.test.ts b/apps/api/src/domain/admin/presenters/DashboardStatsPresenter.test.ts new file mode 100644 index 000000000..b8a04bbc2 --- /dev/null +++ b/apps/api/src/domain/admin/presenters/DashboardStatsPresenter.test.ts @@ -0,0 +1,127 @@ +import { DashboardStatsPresenter } from './DashboardStatsPresenter'; + +describe('DashboardStatsPresenter', () => { + let presenter: DashboardStatsPresenter; + + beforeEach(() => { + presenter = new DashboardStatsPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result = { + totalUsers: 100, + activeUsers: 80, + suspendedUsers: 10, + deletedUsers: 10, + systemAdmins: 5, + recentLogins: 20, + newUsersToday: 5, + userGrowth: [ + { label: 'Day 1', value: 10, color: '#000' }, + { label: 'Day 2', value: 15, color: '#000' }, + ], + roleDistribution: [ + { label: 'Admin', value: 5, color: '#000' }, + { label: 'User', value: 95, color: '#000' }, + ], + statusDistribution: { + active: 80, + suspended: 10, + deleted: 10, + }, + activityTimeline: [ + { date: '2024-01-01', newUsers: 5, logins: 20 }, + ], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + totalUsers: 100, + activeUsers: 80, + suspendedUsers: 10, + deletedUsers: 10, + systemAdmins: 5, + recentLogins: 20, + newUsersToday: 5, + userGrowth: [ + { label: 'Day 1', value: 10, color: '#000' }, + { label: 'Day 2', value: 15, color: '#000' }, + ], + roleDistribution: [ + { label: 'Admin', value: 5, color: '#000' }, + { label: 'User', value: 95, color: '#000' }, + ], + statusDistribution: { + active: 80, + suspended: 10, + deleted: 10, + }, + activityTimeline: [ + { date: '2024-01-01', newUsers: 5, logins: 20 }, + ], + }); + }); + + it('should handle empty arrays', () => { + const result = { + totalUsers: 0, + activeUsers: 0, + suspendedUsers: 0, + deletedUsers: 0, + systemAdmins: 0, + recentLogins: 0, + newUsersToday: 0, + userGrowth: [], + roleDistribution: [], + statusDistribution: { + active: 0, + suspended: 0, + deleted: 0, + }, + activityTimeline: [], + }; + + presenter.present(result); + + expect(presenter.responseModel.userGrowth).toEqual([]); + expect(presenter.responseModel.roleDistribution).toEqual([]); + expect(presenter.responseModel.activityTimeline).toEqual([]); + }); + }); + + describe('reset', () => { + it('should clear the response model', () => { + const result = { + totalUsers: 100, + activeUsers: 80, + suspendedUsers: 10, + deletedUsers: 10, + systemAdmins: 5, + recentLogins: 20, + newUsersToday: 5, + userGrowth: [], + roleDistribution: [], + statusDistribution: { + active: 80, + suspended: 10, + deleted: 10, + }, + activityTimeline: [], + }; + + presenter.present(result); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('No response model available. Call present() first.'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('No response model available. Call present() first.'); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/auth/presenters/ForgotPasswordPresenter.test.ts b/apps/api/src/domain/auth/presenters/ForgotPasswordPresenter.test.ts new file mode 100644 index 000000000..cea095633 --- /dev/null +++ b/apps/api/src/domain/auth/presenters/ForgotPasswordPresenter.test.ts @@ -0,0 +1,43 @@ +import { ForgotPasswordPresenter } from './ForgotPasswordPresenter'; + +describe('ForgotPasswordPresenter', () => { + let presenter: ForgotPasswordPresenter; + + beforeEach(() => { + presenter = new ForgotPasswordPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result = { message: 'Password reset link generated successfully', magicLink: 'http://example.com/reset?token=abc123' }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ message: 'Password reset link generated successfully', magicLink: 'http://example.com/reset?token=abc123' }); + }); + + it('should handle result without magicLink', () => { + const result = { message: 'If an account exists with this email, a password reset link will be sent', magicLink: null }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ message: 'If an account exists with this email, a password reset link will be sent', magicLink: null }); + }); + }); + + describe('reset', () => { + it('should clear the response model', () => { + presenter.present({ message: 'Password reset link generated successfully', magicLink: 'http://example.com' }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('ForgotPasswordPresenter: No response model available'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('ForgotPasswordPresenter: No response model available'); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/auth/presenters/ResetPasswordPresenter.test.ts b/apps/api/src/domain/auth/presenters/ResetPasswordPresenter.test.ts new file mode 100644 index 000000000..baf634ae2 --- /dev/null +++ b/apps/api/src/domain/auth/presenters/ResetPasswordPresenter.test.ts @@ -0,0 +1,43 @@ +import { ResetPasswordPresenter } from './ResetPasswordPresenter'; + +describe('ResetPasswordPresenter', () => { + let presenter: ResetPasswordPresenter; + + beforeEach(() => { + presenter = new ResetPasswordPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result = { message: 'Password reset successfully. You can now log in with your new password.' }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ message: 'Password reset successfully. You can now log in with your new password.' }); + }); + + it('should handle different message', () => { + const result = { message: 'Password updated' }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ message: 'Password updated' }); + }); + }); + + describe('reset', () => { + it('should clear the response model', () => { + presenter.present({ message: 'Password reset successfully' }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('ResetPasswordPresenter: No response model available'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('ResetPasswordPresenter: No response model available'); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts new file mode 100644 index 000000000..45cfd7c6c --- /dev/null +++ b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts @@ -0,0 +1,55 @@ +import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter'; + +describe('CompleteOnboardingPresenter', () => { + let presenter: CompleteOnboardingPresenter; + + beforeEach(() => { + presenter = new CompleteOnboardingPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result = { + driver: { + id: 'driver-123', + } as any, + }; + + presenter.present(result); + + expect(presenter.getResponseModel()).toEqual({ + success: true, + driverId: 'driver-123', + }); + }); + + it('should handle different driver IDs', () => { + const result = { + driver: { + id: 'driver-456', + } as any, + }; + + presenter.present(result); + + expect(presenter.getResponseModel()).toEqual({ + success: true, + driverId: 'driver-456', + }); + }); + }); + + describe('getResponseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); + }); + + it('should return model after present()', () => { + presenter.present({ driver: { id: 'driver-123' } } as any); + expect(presenter.getResponseModel()).toEqual({ + success: true, + driverId: 'driver-123', + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityAndScoringPresenter.test.ts b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityAndScoringPresenter.test.ts new file mode 100644 index 000000000..5e966fa46 --- /dev/null +++ b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityAndScoringPresenter.test.ts @@ -0,0 +1,487 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { AllLeaguesWithCapacityAndScoringPresenter } from './AllLeaguesWithCapacityAndScoringPresenter'; +import { League } from '@core/racing/domain/entities/League'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; +import { Game } from '@core/racing/domain/entities/Game'; +import { MediaReference } from '@core/domain/media/MediaReference'; +import type { LeagueScoringPreset } from '@core/racing/domain/types/LeagueScoringPreset'; +import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; + +describe('AllLeaguesWithCapacityAndScoringPresenter', () => { + let presenter: AllLeaguesWithCapacityAndScoringPresenter; + + beforeEach(() => { + presenter = new AllLeaguesWithCapacityAndScoringPresenter(); + }); + + describe('present', () => { + it('should map empty leagues array to DTO', async () => { + await presenter.present({ leagues: [] }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagues: [], + totalCount: 0, + }); + }); + + it('should map leagues with basic information', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { + pointsSystem: 'f1-2024', + maxDrivers: 20, + }, + createdAt: new Date('2024-01-01T00:00:00Z'), + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 20, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues).toHaveLength(1); + expect(vm.leagues[0]!.id).toBe('league-1'); + expect(vm.leagues[0]!.name).toBe('Test League'); + expect(vm.leagues[0]!.description).toBe('A test league'); + expect(vm.leagues[0]!.ownerId).toBe('owner-1'); + expect(vm.leagues[0]!.createdAt).toBe('2024-01-01T00:00:00.000Z'); + expect(vm.leagues[0]!.settings.maxDrivers).toBe(20); + expect(vm.leagues[0]!.usedSlots).toBe(5); + expect(vm.totalCount).toBe(1); + }); + + it('should map leagues with social links', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + socialLinks: { + discordUrl: 'https://discord.gg/test', + youtubeUrl: 'https://youtube.com/test', + websiteUrl: 'https://test.com', + }, + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 3, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.socialLinks).toEqual({ + discordUrl: 'https://discord.gg/test', + youtubeUrl: 'https://youtube.com/test', + websiteUrl: 'https://test.com', + }); + }); + + it('should map leagues with category', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + category: 'Formula', + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 2, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.category).toBe('Formula'); + }); + + it('should map leagues with session duration and qualifying format', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { + pointsSystem: 'f1-2024', + sessionDuration: 90, + qualifyingFormat: 'single-lap', + }, + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 4, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.settings.sessionDuration).toBe(90); + expect(vm.leagues[0]!.settings.qualifyingFormat).toBe('single-lap'); + }); + + it('should map leagues with timing summary for hours', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + const season = Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Season 1', + status: 'active', + schedule: { + startDate: new Date('2024-01-01'), + timeOfDay: { hour: 20, minute: 0 }, + } as any, + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-1', + seasonId: 'season-1', + scoringPresetId: 'preset-1', + championships: [ + { + id: 'champ-1', + name: 'Drivers Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'bestNResults', count: 3 }, + }, + ], + }); + + const game = Game.create({ + id: 'iracing', + name: 'iRacing', + }); + + const preset: LeagueScoringPreset = { + id: 'preset-1', + name: 'Test Preset', + description: 'Test preset description', + primaryChampionshipType: 'driver', + dropPolicySummary: 'Best 8 of 12', + sessionSummary: 'Qualifying + Race', + bonusSummary: 'Bonus points for top 3', + defaultTimings: { + mainRaceMinutes: 120, + practiceMinutes: 30, + qualifyingMinutes: 20, + sprintRaceMinutes: 0, + sessionCount: 1 + }, + }; + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + season, + scoringConfig, + game, + preset, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.timingSummary).toBe('2h Race'); + expect(vm.leagues[0]!.scoring).toEqual({ + gameId: 'iracing', + gameName: 'iRacing', + primaryChampionshipType: 'driver', + scoringPresetId: 'preset-1', + scoringPresetName: 'Test Preset', + dropPolicySummary: 'Best 8 of 12', + scoringPatternSummary: 'Qualifying + Race', + }); + }); + + it('should map leagues with timing summary for minutes', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + const season = Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Season 1', + status: 'active', + schedule: { + startDate: new Date('2024-01-01'), + timeOfDay: { hour: 20, minute: 0 }, + } as any, + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-1', + seasonId: 'season-1', + scoringPresetId: 'preset-1', + championships: [ + { + id: 'champ-1', + name: 'Drivers Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'bestNResults', count: 3 }, + }, + ], + }); + + const game = Game.create({ + id: 'iracing', + name: 'iRacing', + }); + + const preset: LeagueScoringPreset = { + id: 'preset-1', + name: 'Test Preset', + description: 'Test preset description', + primaryChampionshipType: 'driver', + dropPolicySummary: 'Best 8 of 12', + sessionSummary: 'Qualifying + Race', + bonusSummary: 'Bonus points for top 3', + defaultTimings: { + mainRaceMinutes: 45, + practiceMinutes: 15, + qualifyingMinutes: 10, + sprintRaceMinutes: 0, + sessionCount: 1 + }, + }; + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + season, + scoringConfig, + game, + preset, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.timingSummary).toBe('45m Race'); + }); + + it('should handle leagues without scoring information', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.scoring).toBeUndefined(); + expect(vm.leagues[0]!.timingSummary).toBeUndefined(); + }); + + it('should handle leagues with system default logoRef', async () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + // Should be null when it's system default (as per presenter logic) + expect(vm.leagues[0]!.logoUrl).toBeNull(); + }); + + it('should handle multiple leagues with different configurations', async () => { + const league1 = League.create({ + id: 'league-1', + name: 'League One', + description: 'First league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024', maxDrivers: 20 }, + category: 'Formula', + }); + + const league2 = League.create({ + id: 'league-2', + name: 'League Two', + description: 'Second league', + ownerId: 'owner-2', + settings: { pointsSystem: 'indycar', maxDrivers: 40 }, + socialLinks: { discordUrl: 'https://discord.gg/league2' }, + }); + + await presenter.present({ + leagues: [ + { + league: league1, + currentDrivers: 10, + maxDrivers: 20, + }, + { + league: league2, + currentDrivers: 25, + maxDrivers: 40, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues).toHaveLength(2); + expect(vm.totalCount).toBe(2); + expect(vm.leagues[0]!.id).toBe('league-1'); + expect(vm.leagues[0]!.category).toBe('Formula'); + expect(vm.leagues[0]!.settings.maxDrivers).toBe(20); + expect(vm.leagues[1]!.id).toBe('league-2'); + expect(vm.leagues[1]!.socialLinks?.discordUrl).toBe('https://discord.gg/league2'); + expect(vm.leagues[1]!.settings.maxDrivers).toBe(40); + }); + }); + + describe('getViewModel', () => { + it('should throw error when not presented', () => { + expect(() => presenter.getViewModel()).toThrow('Presenter not presented'); + }); + + it('should return the model when presented', async () => { + await presenter.present({ leagues: [] }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagues: [], + totalCount: 0, + }); + }); + }); + + describe('setMediaResolver', () => { + it('should use media resolver to resolve logo URLs', async () => { + const mockResolver = { + resolve: async () => 'https://cdn.example.com/league-logo.png', + }; + + presenter.setMediaResolver(mockResolver); + + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + // Override logoRef to uploaded type + (league as any).logoRef = MediaReference.fromJSON({ + type: 'uploaded', + mediaId: 'media-123', + }); + + await presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.logoUrl).toBe('https://cdn.example.com/league-logo.png'); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.test.ts b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.test.ts new file mode 100644 index 000000000..8debafa26 --- /dev/null +++ b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.test.ts @@ -0,0 +1,228 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { AllLeaguesWithCapacityPresenter } from './AllLeaguesWithCapacityPresenter'; +import { League } from '@core/racing/domain/entities/League'; + +describe('AllLeaguesWithCapacityPresenter', () => { + let presenter: AllLeaguesWithCapacityPresenter; + + beforeEach(() => { + presenter = new AllLeaguesWithCapacityPresenter(); + }); + + describe('present', () => { + it('should map empty leagues array to DTO', () => { + presenter.present({ leagues: [] }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagues: [], + totalCount: 0, + }); + }); + + it('should map leagues with basic information', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { + pointsSystem: 'f1-2024', + maxDrivers: 20, + }, + createdAt: new Date('2024-01-01T00:00:00Z'), + }); + + presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 20, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues).toHaveLength(1); + expect(vm.leagues[0]!.id).toBe('league-1'); + expect(vm.leagues[0]!.name).toBe('Test League'); + expect(vm.leagues[0]!.description).toBe('A test league'); + expect(vm.leagues[0]!.ownerId).toBe('owner-1'); + expect(vm.leagues[0]!.createdAt).toBe('2024-01-01T00:00:00.000Z'); + expect(vm.leagues[0]!.settings.maxDrivers).toBe(20); + expect(vm.leagues[0]!.usedSlots).toBe(5); + expect(vm.totalCount).toBe(1); + }); + + it('should map leagues with social links', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + socialLinks: { + discordUrl: 'https://discord.gg/test', + youtubeUrl: 'https://youtube.com/test', + websiteUrl: 'https://test.com', + }, + }); + + presenter.present({ + leagues: [ + { + league, + currentDrivers: 3, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.socialLinks).toEqual({ + discordUrl: 'https://discord.gg/test', + youtubeUrl: 'https://youtube.com/test', + websiteUrl: 'https://test.com', + }); + }); + + it('should handle leagues without social links', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + presenter.present({ + leagues: [ + { + league, + currentDrivers: 2, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.socialLinks).toEqual({}); + }); + + it('should handle multiple leagues with different configurations', () => { + const league1 = League.create({ + id: 'league-1', + name: 'League One', + description: 'First league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024', maxDrivers: 20 }, + }); + + const league2 = League.create({ + id: 'league-2', + name: 'League Two', + description: 'Second league', + ownerId: 'owner-2', + settings: { pointsSystem: 'indycar', maxDrivers: 40 }, + socialLinks: { discordUrl: 'https://discord.gg/league2' }, + }); + + presenter.present({ + leagues: [ + { + league: league1, + currentDrivers: 10, + maxDrivers: 20, + }, + { + league: league2, + currentDrivers: 25, + maxDrivers: 40, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues).toHaveLength(2); + expect(vm.totalCount).toBe(2); + expect(vm.leagues[0]!.id).toBe('league-1'); + expect(vm.leagues[0]!.settings.maxDrivers).toBe(20); + expect(vm.leagues[0]!.usedSlots).toBe(10); + expect(vm.leagues[1]!.id).toBe('league-2'); + expect(vm.leagues[1]!.socialLinks?.discordUrl).toBe('https://discord.gg/league2'); + expect(vm.leagues[1]!.settings.maxDrivers).toBe(40); + expect(vm.leagues[1]!.usedSlots).toBe(25); + }); + + it('should handle leagues with zero drivers', () => { + const league = League.create({ + id: 'league-1', + name: 'Empty League', + description: 'No drivers yet', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024', maxDrivers: 32 }, + }); + + presenter.present({ + leagues: [ + { + league, + currentDrivers: 0, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.usedSlots).toBe(0); + }); + + it('should handle leagues with description containing special characters', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league with "quotes" and & symbols!', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + presenter.present({ + leagues: [ + { + league, + currentDrivers: 5, + maxDrivers: 32, + }, + ], + }); + + const vm = presenter.getViewModel(); + + expect(vm.leagues[0]!.description).toBe('A test league with "quotes" and & symbols!'); + }); + }); + + describe('getViewModel', () => { + it('should throw error when not presented', () => { + expect(() => presenter.getViewModel()).toThrow('Presenter not presented'); + }); + + it('should return the model when presented', () => { + presenter.present({ leagues: [] }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagues: [], + totalCount: 0, + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.test.ts b/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.test.ts new file mode 100644 index 000000000..b85524605 --- /dev/null +++ b/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { ApproveLeagueJoinRequestPresenter } from './ApproveLeagueJoinRequestPresenter'; + +describe('ApproveLeagueJoinRequestPresenter', () => { + let presenter: ApproveLeagueJoinRequestPresenter; + + beforeEach(() => { + presenter = new ApproveLeagueJoinRequestPresenter(); + }); + + describe('present', () => { + it('should present successful approval result', () => { + const result = { + success: true, + message: 'Join request approved successfully', + }; + + presenter.present(result); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + success: true, + message: 'Join request approved successfully', + }); + }); + + it('should present failed approval result', () => { + const result = { + success: false, + message: 'Failed to approve join request', + }; + + presenter.present(result); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + success: false, + message: 'Failed to approve join request', + }); + }); + + it('should handle empty message', () => { + const result = { + success: true, + message: '', + }; + + presenter.present(result); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + success: true, + message: '', + }); + }); + }); + + describe('getViewModel', () => { + it('should return null before presentation', () => { + expect(presenter.getViewModel()).toBeNull(); + }); + + it('should return the model after presentation', () => { + presenter.present({ success: true, message: 'Test' }); + + expect(presenter.getViewModel()).toEqual({ + success: true, + message: 'Test', + }); + }); + }); + + describe('reset', () => { + it('should reset the model to null', () => { + presenter.present({ success: true, message: 'Test' }); + expect(presenter.getViewModel()).not.toBeNull(); + + presenter.reset(); + + expect(presenter.getViewModel()).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/CreateLeaguePresenter.test.ts b/apps/api/src/domain/league/presenters/CreateLeaguePresenter.test.ts new file mode 100644 index 000000000..590d81ceb --- /dev/null +++ b/apps/api/src/domain/league/presenters/CreateLeaguePresenter.test.ts @@ -0,0 +1,228 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { CreateLeaguePresenter } from './CreateLeaguePresenter'; +import { League } from '@core/racing/domain/entities/League'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; +import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; + +describe('CreateLeaguePresenter', () => { + let presenter: CreateLeaguePresenter; + + beforeEach(() => { + presenter = new CreateLeaguePresenter(); + }); + + describe('present', () => { + it('should present successful league creation', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + const season = Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Test League Season 1', + status: 'active', + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-1', + seasonId: 'season-1', + scoringPresetId: 'preset-1', + championships: [ + { + id: 'champ-1', + name: 'Driver Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'none' }, + }, + ], + }); + + presenter.present({ league, season, scoringConfig }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagueId: 'league-1', + success: true, + }); + }); + + it('should present league with different ID', () => { + const league = League.create({ + id: 'league-abc-123', + name: 'Another League', + description: 'Another test league', + ownerId: 'owner-2', + settings: { pointsSystem: 'indycar' }, + }); + + const season = Season.create({ + id: 'season-2', + leagueId: 'league-abc-123', + gameId: 'iracing', + name: 'Another League Season 1', + status: 'active', + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-2', + seasonId: 'season-2', + scoringPresetId: 'preset-2', + championships: [ + { + id: 'champ-2', + name: 'Driver Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'none' }, + }, + ], + }); + + presenter.present({ league, season, scoringConfig }); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + leagueId: 'league-abc-123', + success: true, + }); + }); + }); + + describe('getViewModel', () => { + it('should throw error when not presented', () => { + expect(() => presenter.getViewModel()).toThrow('Presenter not presented'); + }); + + it('should return the model when presented', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + const season = Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Test League Season 1', + status: 'active', + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-1', + seasonId: 'season-1', + scoringPresetId: 'preset-1', + championships: [ + { + id: 'champ-1', + name: 'Driver Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'none' }, + }, + ], + }); + + presenter.present({ league, season, scoringConfig }); + + expect(presenter.getViewModel()).toEqual({ + leagueId: 'league-1', + success: true, + }); + }); + }); + + describe('reset', () => { + it('should reset the model and cause getViewModel to throw', () => { + const league = League.create({ + id: 'league-1', + name: 'Test League', + description: 'A test league', + ownerId: 'owner-1', + settings: { pointsSystem: 'f1-2024' }, + }); + + const season = Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Test League Season 1', + status: 'active', + } as any); + + const scoringConfig = LeagueScoringConfig.create({ + id: 'scoring-1', + seasonId: 'season-1', + scoringPresetId: 'preset-1', + championships: [ + { + id: 'champ-1', + name: 'Driver Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'none' }, + }, + ], + }); + + presenter.present({ league, season, scoringConfig }); + expect(presenter.getViewModel()).not.toBeNull(); + + presenter.reset(); + + expect(() => presenter.getViewModel()).toThrow('Presenter not presented'); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.test.ts b/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.test.ts new file mode 100644 index 000000000..7d8d105c0 --- /dev/null +++ b/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { RejectLeagueJoinRequestPresenter } from './RejectLeagueJoinRequestPresenter'; + +describe('RejectLeagueJoinRequestPresenter', () => { + let presenter: RejectLeagueJoinRequestPresenter; + + beforeEach(() => { + presenter = new RejectLeagueJoinRequestPresenter(); + }); + + describe('present', () => { + it('should present successful rejection result', () => { + const result = { + success: true, + message: 'Join request rejected successfully', + }; + + presenter.present(result); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + success: true, + message: 'Join request rejected successfully', + }); + }); + + it('should present failed rejection result', () => { + const result = { + success: false, + message: 'Failed to reject join request', + }; + + presenter.present(result); + + const vm = presenter.getViewModel(); + + expect(vm).toEqual({ + success: false, + message: 'Failed to reject join request', + }); + }); + }); + + describe('getViewModel', () => { + it('should return null before presentation', () => { + expect(presenter.getViewModel()).toBeNull(); + }); + + it('should return the model after presentation', () => { + presenter.present({ success: true, message: 'Test' }); + + expect(presenter.getViewModel()).toEqual({ + success: true, + message: 'Test', + }); + }); + }); + + describe('reset', () => { + it('should reset the model to null', () => { + presenter.present({ success: true, message: 'Test' }); + expect(presenter.getViewModel()).not.toBeNull(); + + presenter.reset(); + + expect(presenter.getViewModel()).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.test.ts b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.test.ts new file mode 100644 index 000000000..65c82fde1 --- /dev/null +++ b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { TotalLeaguesPresenter } from './TotalLeaguesPresenter'; + +describe('TotalLeaguesPresenter', () => { + let presenter: TotalLeaguesPresenter; + + beforeEach(() => { + presenter = new TotalLeaguesPresenter(); + }); + + describe('present', () => { + it('should present total leagues count', () => { + presenter.present({ totalLeagues: 42 }); + + const vm = presenter.getResponseModel(); + + expect(vm).toEqual({ + totalLeagues: 42, + }); + }); + + it('should present zero total leagues', () => { + presenter.present({ totalLeagues: 0 }); + + const vm = presenter.getResponseModel(); + + expect(vm).toEqual({ + totalLeagues: 0, + }); + }); + + it('should present large total leagues count', () => { + presenter.present({ totalLeagues: 999999 }); + + const vm = presenter.getResponseModel(); + + expect(vm).toEqual({ + totalLeagues: 999999, + }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before presentation', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return the model after presentation', () => { + presenter.present({ totalLeagues: 10 }); + + expect(presenter.getResponseModel()).toEqual({ + totalLeagues: 10, + }); + }); + }); + + describe('reset', () => { + it('should reset the model to null', () => { + presenter.present({ totalLeagues: 25 }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + + expect(presenter.getResponseModel()).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/DeleteMediaPresenter.test.ts b/apps/api/src/domain/media/presenters/DeleteMediaPresenter.test.ts new file mode 100644 index 000000000..0d2b545fa --- /dev/null +++ b/apps/api/src/domain/media/presenters/DeleteMediaPresenter.test.ts @@ -0,0 +1,61 @@ +import { DeleteMediaPresenter } from './DeleteMediaPresenter'; + +describe('DeleteMediaPresenter', () => { + let presenter: DeleteMediaPresenter; + + beforeEach(() => { + presenter = new DeleteMediaPresenter(); + }); + + describe('transform', () => { + it('should map successful result to response model', () => { + const result = { mediaId: 'media-123', deleted: true }; + + const response = presenter.transform(result); + + expect(response).toEqual({ success: true }); + expect(presenter.getResponseModel()).toEqual({ success: true }); + }); + + it('should handle failed deletion', () => { + const result = { mediaId: 'media-123', deleted: false }; + + const response = presenter.transform(result); + + expect(response).toEqual({ success: false }); + expect(presenter.getResponseModel()).toEqual({ success: false }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ mediaId: 'media-123', deleted: true }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before transform()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + + it('should return model after transform()', () => { + presenter.transform({ mediaId: 'media-123', deleted: true }); + expect(presenter.responseModel).toEqual({ success: true }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ mediaId: 'media-123', deleted: true }); + expect(presenter.getResponseModel()).toEqual({ success: true }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/GetAvatarPresenter.test.ts b/apps/api/src/domain/media/presenters/GetAvatarPresenter.test.ts new file mode 100644 index 000000000..8840f8a51 --- /dev/null +++ b/apps/api/src/domain/media/presenters/GetAvatarPresenter.test.ts @@ -0,0 +1,95 @@ +import { GetAvatarPresenter } from './GetAvatarPresenter'; + +describe('GetAvatarPresenter', () => { + let presenter: GetAvatarPresenter; + + beforeEach(() => { + presenter = new GetAvatarPresenter(); + }); + + describe('transform', () => { + it('should map result to response model', () => { + const result = { + avatar: { + id: 'avatar-123', + driverId: 'driver-456', + mediaUrl: 'https://example.com/avatar.png', + selectedAt: new Date('2024-01-01'), + }, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ avatarUrl: 'https://example.com/avatar.png' }); + expect(presenter.getResponseModel()).toEqual({ avatarUrl: 'https://example.com/avatar.png' }); + }); + + it('should handle different avatar URLs', () => { + const result = { + avatar: { + id: 'avatar-789', + driverId: 'driver-456', + mediaUrl: 'https://cdn.example.com/avatars/test.jpg', + selectedAt: new Date('2024-01-02'), + }, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ avatarUrl: 'https://cdn.example.com/avatars/test.jpg' }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ + avatar: { + id: 'avatar-123', + driverId: 'driver-456', + mediaUrl: 'https://example.com/avatar.png', + selectedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should return null when model is null', () => { + expect(presenter.responseModel).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ + avatar: { + id: 'avatar-123', + driverId: 'driver-456', + mediaUrl: 'https://example.com/avatar.png', + selectedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.responseModel).toEqual({ avatarUrl: 'https://example.com/avatar.png' }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ + avatar: { + id: 'avatar-123', + driverId: 'driver-456', + mediaUrl: 'https://example.com/avatar.png', + selectedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.getResponseModel()).toEqual({ avatarUrl: 'https://example.com/avatar.png' }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/GetMediaPresenter.test.ts b/apps/api/src/domain/media/presenters/GetMediaPresenter.test.ts new file mode 100644 index 000000000..9cbfb5d91 --- /dev/null +++ b/apps/api/src/domain/media/presenters/GetMediaPresenter.test.ts @@ -0,0 +1,165 @@ +import { GetMediaPresenter } from './GetMediaPresenter'; + +describe('GetMediaPresenter', () => { + let presenter: GetMediaPresenter; + + beforeEach(() => { + presenter = new GetMediaPresenter(); + }); + + describe('transform', () => { + it('should map result to response model', () => { + const result = { + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + }, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + id: 'media-123', + url: 'https://example.com/avatar.png', + type: 'image', + uploadedAt: new Date('2024-01-01'), + size: 12345, + }); + }); + + it('should handle metadata', () => { + const result = { + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + metadata: { category: 'profile', width: 100, height: 100 }, + }, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + id: 'media-123', + url: 'https://example.com/avatar.png', + type: 'image', + uploadedAt: new Date('2024-01-01'), + size: 12345, + category: 'profile', + }); + }); + + it('should handle result without metadata', () => { + const result = { + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + }, + }; + + const response = presenter.transform(result); + + expect(response?.category).toBeUndefined(); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should return null when model is null', () => { + expect(presenter.responseModel).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.responseModel).toEqual({ + id: 'media-123', + url: 'https://example.com/avatar.png', + type: 'image', + uploadedAt: new Date('2024-01-01'), + size: 12345, + }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ + media: { + id: 'media-123', + filename: 'avatar.png', + originalName: 'my-avatar.png', + mimeType: 'image/png', + size: 12345, + url: 'https://example.com/avatar.png', + type: 'image', + uploadedBy: 'user-456', + uploadedAt: new Date('2024-01-01'), + }, + }); + expect(presenter.getResponseModel()).toEqual({ + id: 'media-123', + url: 'https://example.com/avatar.png', + type: 'image', + uploadedAt: new Date('2024-01-01'), + size: 12345, + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.test.ts b/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.test.ts new file mode 100644 index 000000000..3a8645e23 --- /dev/null +++ b/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.test.ts @@ -0,0 +1,126 @@ +import { RequestAvatarGenerationPresenter } from './RequestAvatarGenerationPresenter'; + +describe('RequestAvatarGenerationPresenter', () => { + let presenter: RequestAvatarGenerationPresenter; + + beforeEach(() => { + presenter = new RequestAvatarGenerationPresenter(); + }); + + describe('transform', () => { + it('should map completed result to response model', () => { + const result = { + requestId: 'req-123', + status: 'completed' as const, + avatarUrls: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png'], + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: true, + requestId: 'req-123', + avatarUrls: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png'], + }); + }); + + it('should handle empty avatarUrls', () => { + const result = { + requestId: 'req-123', + status: 'completed' as const, + avatarUrls: [], + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: true, + requestId: 'req-123', + avatarUrls: [], + }); + }); + + it('should handle undefined avatarUrls', () => { + const result = { + requestId: 'req-123', + status: 'completed' as const, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: true, + requestId: 'req-123', + avatarUrls: [], + }); + }); + + it('should handle non-completed status', () => { + const result = { + requestId: 'req-123', + status: 'generating' as const, + avatarUrls: [], + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: false, + requestId: 'req-123', + avatarUrls: [], + }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ + requestId: 'req-123', + status: 'completed' as const, + avatarUrls: ['https://example.com/avatar1.png'], + }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before transform()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + + it('should return model after transform()', () => { + presenter.transform({ + requestId: 'req-123', + status: 'completed' as const, + avatarUrls: ['https://example.com/avatar1.png'], + }); + expect(presenter.responseModel).toEqual({ + success: true, + requestId: 'req-123', + avatarUrls: ['https://example.com/avatar1.png'], + }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ + requestId: 'req-123', + status: 'completed' as const, + avatarUrls: ['https://example.com/avatar1.png'], + }); + expect(presenter.getResponseModel()).toEqual({ + success: true, + requestId: 'req-123', + avatarUrls: ['https://example.com/avatar1.png'], + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.test.ts b/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.test.ts new file mode 100644 index 000000000..e1f1f47ec --- /dev/null +++ b/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.test.ts @@ -0,0 +1,66 @@ +import { UpdateAvatarPresenter } from './UpdateAvatarPresenter'; + +describe('UpdateAvatarPresenter', () => { + let presenter: UpdateAvatarPresenter; + + beforeEach(() => { + presenter = new UpdateAvatarPresenter(); + }); + + describe('transform', () => { + it('should map result to response model', () => { + const result = { + avatarId: 'avatar-123', + driverId: 'driver-456', + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ success: true }); + expect(presenter.getResponseModel()).toEqual({ success: true }); + }); + + it('should always return success true', () => { + const result = { + avatarId: 'avatar-789', + driverId: 'driver-999', + }; + + const response = presenter.transform(result); + + expect(response.success).toBe(true); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ avatarId: 'avatar-123', driverId: 'driver-456' }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before transform()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + + it('should return model after transform()', () => { + presenter.transform({ avatarId: 'avatar-123', driverId: 'driver-456' }); + expect(presenter.responseModel).toEqual({ success: true }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ avatarId: 'avatar-123', driverId: 'driver-456' }); + expect(presenter.getResponseModel()).toEqual({ success: true }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/media/presenters/UploadMediaPresenter.test.ts b/apps/api/src/domain/media/presenters/UploadMediaPresenter.test.ts new file mode 100644 index 000000000..cf5ad8561 --- /dev/null +++ b/apps/api/src/domain/media/presenters/UploadMediaPresenter.test.ts @@ -0,0 +1,92 @@ +import { UploadMediaPresenter } from './UploadMediaPresenter'; + +describe('UploadMediaPresenter', () => { + let presenter: UploadMediaPresenter; + + beforeEach(() => { + presenter = new UploadMediaPresenter(); + }); + + describe('transform', () => { + it('should map result with URL to response model', () => { + const result = { + mediaId: 'media-123', + url: 'https://example.com/uploaded-file.png', + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: true, + mediaId: 'media-123', + url: 'https://example.com/uploaded-file.png', + }); + }); + + it('should map result without URL to response model', () => { + const result = { + mediaId: 'media-123', + url: undefined, + }; + + const response = presenter.transform(result); + + expect(response).toEqual({ + success: true, + mediaId: 'media-123', + }); + expect(response.url).toBeUndefined(); + }); + + it('should always set success to true', () => { + const result = { + mediaId: 'media-456', + url: 'https://example.com/test.jpg', + }; + + const response = presenter.transform(result); + + expect(response.success).toBe(true); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.transform({ mediaId: 'media-123', url: 'https://example.com/file.png' }); + expect(presenter.getResponseModel()).not.toBeNull(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before transform()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + + it('should return model after transform()', () => { + presenter.transform({ mediaId: 'media-123', url: 'https://example.com/file.png' }); + expect(presenter.responseModel).toEqual({ + success: true, + mediaId: 'media-123', + url: 'https://example.com/file.png', + }); + }); + }); + + describe('getResponseModel', () => { + it('should return null before transform()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after transform()', () => { + presenter.transform({ mediaId: 'media-123', url: 'https://example.com/file.png' }); + expect(presenter.getResponseModel()).toEqual({ + success: true, + mediaId: 'media-123', + url: 'https://example.com/file.png', + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/AllRacesPageDataPresenter.test.ts b/apps/api/src/domain/race/presenters/AllRacesPageDataPresenter.test.ts new file mode 100644 index 000000000..c869b40c7 --- /dev/null +++ b/apps/api/src/domain/race/presenters/AllRacesPageDataPresenter.test.ts @@ -0,0 +1,134 @@ +import { AllRacesPageDataPresenter } from './AllRacesPageDataPresenter'; +import type { GetAllRacesPageDataResult } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase'; + +describe('AllRacesPageDataPresenter', () => { + let presenter: AllRacesPageDataPresenter; + + beforeEach(() => { + presenter = new AllRacesPageDataPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result: GetAllRacesPageDataResult = { + races: [ + { + id: 'race-1', + track: 'Track A', + car: 'Car A', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + leagueId: 'league-1', + leagueName: 'League A', + strengthOfField: 1500, + }, + { + id: 'race-2', + track: 'Track B', + car: 'Car B', + scheduledAt: '2024-01-02T10:00:00Z', + status: 'completed', + leagueId: 'league-2', + leagueName: 'League B', + strengthOfField: null, + }, + ], + filters: { + statuses: [ + { value: 'all', label: 'All Statuses' }, + { value: 'scheduled', label: 'Scheduled' }, + ], + leagues: [ + { id: 'league-1', name: 'League A' }, + { id: 'league-2', name: 'League B' }, + ], + }, + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + races: [ + { + id: 'race-1', + track: 'Track A', + car: 'Car A', + scheduledAt: '2024-01-01T10:00:00Z', + status: 'scheduled', + leagueId: 'league-1', + leagueName: 'League A', + strengthOfField: 1500, + }, + { + id: 'race-2', + track: 'Track B', + car: 'Car B', + scheduledAt: '2024-01-02T10:00:00Z', + status: 'completed', + leagueId: 'league-2', + leagueName: 'League B', + strengthOfField: null, + }, + ], + filters: { + statuses: [ + { value: 'all', label: 'All Statuses' }, + { value: 'scheduled', label: 'Scheduled' }, + ], + leagues: [ + { id: 'league-1', name: 'League A' }, + { id: 'league-2', name: 'League B' }, + ], + }, + }); + }); + + it('should handle empty races', () => { + const result: GetAllRacesPageDataResult = { + races: [], + filters: { + statuses: [{ value: 'all', label: 'All Statuses' }], + leagues: [], + }, + }; + + presenter.present(result); + + expect(presenter.responseModel.races).toEqual([]); + expect(presenter.responseModel.filters.leagues).toEqual([]); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + const result: GetAllRacesPageDataResult = { + races: [{ id: 'race-1', track: 'Track A', car: 'Car A', scheduledAt: '2024-01-01', status: 'scheduled', leagueId: 'league-1', leagueName: 'League A', strengthOfField: null }], + filters: { statuses: [], leagues: [] }, + }; + presenter.present(result); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('viewModel', () => { + it('should return same as responseModel', () => { + const result: GetAllRacesPageDataResult = { + races: [{ id: 'race-1', track: 'Track A', car: 'Car A', scheduledAt: '2024-01-01', status: 'scheduled', leagueId: 'league-1', leagueName: 'League A', strengthOfField: null }], + filters: { statuses: [], leagues: [] }, + }; + + presenter.present(result); + + expect(presenter.viewModel).toEqual(presenter.responseModel); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.test.ts b/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.test.ts new file mode 100644 index 000000000..37b4b2aed --- /dev/null +++ b/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.test.ts @@ -0,0 +1,62 @@ +import { GetTotalRacesPresenter } from './GetTotalRacesPresenter'; +import type { GetTotalRacesResult } from '@core/racing/application/use-cases/GetTotalRacesUseCase'; + +describe('GetTotalRacesPresenter', () => { + let presenter: GetTotalRacesPresenter; + + beforeEach(() => { + presenter = new GetTotalRacesPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result: GetTotalRacesResult = { totalRaces: 42 }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ totalRaces: 42 }); + }); + + it('should handle zero races', () => { + const result: GetTotalRacesResult = { totalRaces: 0 }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ totalRaces: 0 }); + }); + + it('should handle large numbers', () => { + const result: GetTotalRacesResult = { totalRaces: 999999 }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ totalRaces: 999999 }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ totalRaces: 42 }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('viewModel', () => { + it('should return same as responseModel', () => { + const result: GetTotalRacesResult = { totalRaces: 42 }; + + presenter.present(result); + + expect(presenter.viewModel).toEqual(presenter.responseModel); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.test.ts b/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.test.ts new file mode 100644 index 000000000..3217778e3 --- /dev/null +++ b/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.test.ts @@ -0,0 +1,115 @@ +import { ImportRaceResultsApiPresenter } from './ImportRaceResultsApiPresenter'; +import type { ImportRaceResultsApiResult } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase'; + +describe('ImportRaceResultsApiPresenter', () => { + let presenter: ImportRaceResultsApiPresenter; + + beforeEach(() => { + presenter = new ImportRaceResultsApiPresenter(); + }); + + describe('present', () => { + it('should map successful result to response model', () => { + const result: ImportRaceResultsApiResult = { + success: true, + raceId: 'race-123', + leagueId: 'league-456', + driversProcessed: 10, + resultsRecorded: 10, + errors: [], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + success: true, + raceId: 'race-123', + driversProcessed: 10, + resultsRecorded: 10, + errors: [], + }); + }); + + it('should handle result with errors', () => { + const result: ImportRaceResultsApiResult = { + success: true, + raceId: 'race-123', + leagueId: 'league-456', + driversProcessed: 10, + resultsRecorded: 8, + errors: ['Driver not found: 12345', 'Driver not found: 67890'], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + success: true, + raceId: 'race-123', + driversProcessed: 10, + resultsRecorded: 8, + errors: ['Driver not found: 12345', 'Driver not found: 67890'], + }); + }); + + it('should handle zero drivers processed', () => { + const result: ImportRaceResultsApiResult = { + success: true, + raceId: 'race-123', + leagueId: 'league-456', + driversProcessed: 0, + resultsRecorded: 0, + errors: [], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + success: true, + raceId: 'race-123', + driversProcessed: 0, + resultsRecorded: 0, + errors: [], + }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ + success: true, + raceId: 'race-123', + leagueId: 'league-456', + driversProcessed: 10, + resultsRecorded: 10, + errors: [], + }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('viewModel', () => { + it('should return same as responseModel', () => { + const result: ImportRaceResultsApiResult = { + success: true, + raceId: 'race-123', + leagueId: 'league-456', + driversProcessed: 10, + resultsRecorded: 10, + errors: [], + }; + + presenter.present(result); + + expect(presenter.viewModel).toEqual(presenter.responseModel); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/RacePenaltiesPresenter.test.ts b/apps/api/src/domain/race/presenters/RacePenaltiesPresenter.test.ts new file mode 100644 index 000000000..ae1f6715b --- /dev/null +++ b/apps/api/src/domain/race/presenters/RacePenaltiesPresenter.test.ts @@ -0,0 +1,159 @@ +import { RacePenaltiesPresenter } from './RacePenaltiesPresenter'; +import type { GetRacePenaltiesResult } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase'; + +describe('RacePenaltiesPresenter', () => { + let presenter: RacePenaltiesPresenter; + + beforeEach(() => { + presenter = new RacePenaltiesPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const mockPenalty = { + id: 'penalty-123', + driverId: 'driver-456', + type: 'time_penalty', + value: 10, + reason: 'Track limits violation', + issuedBy: 'steward-789', + issuedAt: new Date('2024-01-01T10:00:00Z'), + notes: 'Multiple violations', + }; + + const mockDriver = { + id: 'driver-456', + name: { toString: () => 'John Doe' } as any, + }; + + const result: GetRacePenaltiesResult = { + penalties: [mockPenalty as any], + drivers: [mockDriver as any], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + penalties: [ + { + id: 'penalty-123', + driverId: 'driver-456', + type: 'time_penalty', + value: 10, + reason: 'Track limits violation', + issuedBy: 'steward-789', + issuedAt: '2024-01-01T10:00:00.000Z', + notes: 'Multiple violations', + }, + ], + driverMap: { + 'driver-456': 'John Doe', + }, + }); + }); + + it('should handle multiple penalties and drivers', () => { + const result: GetRacePenaltiesResult = { + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'time_penalty', + value: 5, + reason: 'Reason 1', + issuedBy: 'steward-1', + issuedAt: new Date('2024-01-01T10:00:00Z'), + } as any, + { + id: 'penalty-2', + driverId: 'driver-2', + type: 'drive_through', + value: 0, + reason: 'Reason 2', + issuedBy: 'steward-1', + issuedAt: new Date('2024-01-01T10:05:00Z'), + } as any, + ], + drivers: [ + { id: 'driver-1', name: { toString: () => 'Driver One' } } as any, + { id: 'driver-2', name: { toString: () => 'Driver Two' } } as any, + ], + }; + + presenter.present(result); + + expect(presenter.responseModel.penalties).toHaveLength(2); + expect(presenter.responseModel.driverMap).toEqual({ + 'driver-1': 'Driver One', + 'driver-2': 'Driver Two', + }); + }); + + it('should handle empty penalties', () => { + const result: GetRacePenaltiesResult = { + penalties: [], + drivers: [], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + penalties: [], + driverMap: {}, + }); + }); + + it('should handle penalties with undefined value', () => { + const result: GetRacePenaltiesResult = { + penalties: [ + { + id: 'penalty-1', + driverId: 'driver-1', + type: 'disqualification', + value: undefined, + reason: 'Reason', + issuedBy: 'steward-1', + issuedAt: new Date('2024-01-01T10:00:00Z'), + } as any, + ], + drivers: [{ id: 'driver-1', name: { toString: () => 'Driver One' } } as any], + }; + + presenter.present(result); + + expect(presenter.responseModel.penalties[0]?.value).toBe(0); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ + penalties: [{ id: 'p1', driverId: 'd1', type: 't', value: 1, reason: 'r', issuedBy: 's1', issuedAt: new Date() } as any], + drivers: [{ id: 'd1', name: { toString: () => 'D1' } } as any], + }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('viewModel', () => { + it('should return same as responseModel', () => { + const result: GetRacePenaltiesResult = { + penalties: [{ id: 'p1', driverId: 'd1', type: 't', value: 1, reason: 'r', issuedBy: 's1', issuedAt: new Date() } as any], + drivers: [{ id: 'd1', name: { toString: () => 'D1' } } as any], + }; + + presenter.present(result); + + expect(presenter.viewModel).toEqual(presenter.responseModel); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/RaceProtestsPresenter.test.ts b/apps/api/src/domain/race/presenters/RaceProtestsPresenter.test.ts new file mode 100644 index 000000000..3473403c3 --- /dev/null +++ b/apps/api/src/domain/race/presenters/RaceProtestsPresenter.test.ts @@ -0,0 +1,198 @@ +import { RaceProtestsPresenter } from './RaceProtestsPresenter'; +import type { GetRaceProtestsResult } from '@core/racing/application/use-cases/GetRaceProtestsUseCase'; + +describe('RaceProtestsPresenter', () => { + let presenter: RaceProtestsPresenter; + + beforeEach(() => { + presenter = new RaceProtestsPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const mockProtest = { + id: 'protest-123', + protestingDriverId: 'driver-456', + accusedDriverId: 'driver-789', + incident: { + lap: { toNumber: () => 5 } as any, + description: { toString: () => 'Contact at turn 3' } as any, + }, + status: { toString: () => 'pending' } as any, + filedAt: new Date('2024-01-01T10:30:00Z'), + }; + + const mockDriver1 = { + id: 'driver-456', + name: { toString: () => 'John Doe' } as any, + }; + + const mockDriver2 = { + id: 'driver-789', + name: { toString: () => 'Jane Smith' } as any, + }; + + const result: GetRaceProtestsResult = { + protests: [mockProtest as any], + drivers: [mockDriver1 as any, mockDriver2 as any], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + protests: [ + { + id: 'protest-123', + protestingDriverId: 'driver-456', + accusedDriverId: 'driver-789', + incident: { + lap: 5, + description: 'Contact at turn 3', + }, + status: 'pending', + filedAt: '2024-01-01T10:30:00.000Z', + }, + ], + driverMap: { + 'driver-456': 'John Doe', + 'driver-789': 'Jane Smith', + }, + }); + }); + + it('should handle multiple protests and drivers', () => { + const result: GetRaceProtestsResult = { + protests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: { toNumber: () => 3 } as any, + description: { toString: () => 'Incident 1' } as any + }, + status: { toString: () => 'pending' } as any, + filedAt: new Date('2024-01-01T10:00:00Z'), + } as any, + { + id: 'protest-2', + protestingDriverId: 'driver-3', + accusedDriverId: 'driver-1', + incident: { + lap: { toNumber: () => 7 } as any, + description: { toString: () => 'Incident 2' } as any + }, + status: { toString: () => 'reviewed' } as any, + filedAt: new Date('2024-01-01T10:10:00Z'), + } as any, + ], + drivers: [ + { id: 'driver-1', name: { toString: () => 'Driver One' } } as any, + { id: 'driver-2', name: { toString: () => 'Driver Two' } } as any, + { id: 'driver-3', name: { toString: () => 'Driver Three' } } as any, + ], + }; + + presenter.present(result); + + expect(presenter.responseModel.protests).toHaveLength(2); + expect(presenter.responseModel.driverMap).toEqual({ + 'driver-1': 'Driver One', + 'driver-2': 'Driver Two', + 'driver-3': 'Driver Three', + }); + }); + + it('should handle empty protests', () => { + const result: GetRaceProtestsResult = { + protests: [], + drivers: [], + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + protests: [], + driverMap: {}, + }); + }); + + it('should handle protests with reviewed status', () => { + const result: GetRaceProtestsResult = { + protests: [ + { + id: 'protest-1', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: { toNumber: () => 5 } as any, + description: { toString: () => 'Test' } as any + }, + status: { toString: () => 'approved' } as any, + filedAt: new Date('2024-01-01T10:00:00Z'), + } as any, + ], + drivers: [ + { id: 'driver-1', name: { toString: () => 'Driver One' } } as any, + { id: 'driver-2', name: { toString: () => 'Driver Two' } } as any, + ], + }; + + presenter.present(result); + + expect(presenter.responseModel.protests[0]?.status).toBe('approved'); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ + protests: [{ + id: 'p1', + protestingDriverId: 'd1', + accusedDriverId: 'd2', + incident: { + lap: { toNumber: () => 1 } as any, + description: { toString: () => 'test' } as any + }, + status: { toString: () => 'pending' } as any, + filedAt: new Date() + } as any], + drivers: [{ id: 'd1', name: { toString: () => 'D1' } } as any, { id: 'd2', name: { toString: () => 'D2' } } as any], + }); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + }); + + describe('viewModel', () => { + it('should return same as responseModel', () => { + const result: GetRaceProtestsResult = { + protests: [{ + id: 'p1', + protestingDriverId: 'd1', + accusedDriverId: 'd2', + incident: { + lap: { toNumber: () => 1 } as any, + description: { toString: () => 'test' } as any + }, + status: { toString: () => 'pending' } as any, + filedAt: new Date() + } as any], + drivers: [{ id: 'd1', name: { toString: () => 'D1' } } as any, { id: 'd2', name: { toString: () => 'D2' } } as any], + }; + + presenter.present(result); + + expect(presenter.viewModel).toEqual(presenter.responseModel); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/CreateTeamPresenter.test.ts b/apps/api/src/domain/team/presenters/CreateTeamPresenter.test.ts new file mode 100644 index 000000000..b72b84020 --- /dev/null +++ b/apps/api/src/domain/team/presenters/CreateTeamPresenter.test.ts @@ -0,0 +1,79 @@ +import { CreateTeamPresenter } from './CreateTeamPresenter'; + +describe('CreateTeamPresenter', () => { + let presenter: CreateTeamPresenter; + + beforeEach(() => { + presenter = new CreateTeamPresenter(); + }); + + describe('present', () => { + it('should map result to response model', () => { + const result = { + team: { + id: 'team-123', + } as any, + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + id: 'team-123', + success: true, + }); + }); + + it('should handle different team IDs', () => { + const result = { + team: { + id: 'team-456', + } as any, + }; + + presenter.present(result); + + expect(presenter.responseModel).toEqual({ + id: 'team-456', + success: true, + }); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ team: { id: 'team-123' } } as any); + expect(presenter.responseModel).toBeDefined(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('getResponseModel', () => { + it('should return null before present()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after present()', () => { + presenter.present({ team: { id: 'team-123' } } as any); + expect(presenter.getResponseModel()).toEqual({ + id: 'team-123', + success: true, + }); + }); + }); + + describe('responseModel', () => { + it('should throw error when accessed before present()', () => { + expect(() => presenter.responseModel).toThrow('Presenter not presented'); + }); + + it('should return model after present()', () => { + presenter.present({ team: { id: 'team-123' } } as any); + expect(presenter.responseModel).toEqual({ + id: 'team-123', + success: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/DriverTeamPresenter.test.ts b/apps/api/src/domain/team/presenters/DriverTeamPresenter.test.ts new file mode 100644 index 000000000..88ef04cbc --- /dev/null +++ b/apps/api/src/domain/team/presenters/DriverTeamPresenter.test.ts @@ -0,0 +1,218 @@ +import { DriverTeamPresenter } from './DriverTeamPresenter'; + +describe('DriverTeamPresenter', () => { + let presenter: DriverTeamPresenter; + + beforeEach(() => { + presenter = new DriverTeamPresenter(); + }); + + describe('present', () => { + it('should map result to response model for owner', () => { + const result = { + driverId: 'driver-123', + team: { + id: 'team-456', + name: { toString: () => 'Team Alpha' } as any, + tag: { toString: () => 'TA' } as any, + description: { toString: () => 'Best team' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [{ toString: () => 'league-1' } as any], + isRecruiting: true, + createdAt: { toDate: () => new Date('2024-01-01') } as any, + } as any, + membership: { + role: 'owner' as const, + joinedAt: new Date('2024-01-01'), + status: 'active' as const, + } as any, + }; + + presenter.present(result); + + const model = presenter.getResponseModel(); + expect(model).toEqual({ + team: { + id: 'team-456', + name: 'Team Alpha', + tag: 'TA', + description: 'Best team', + ownerId: 'driver-123', + leagues: ['league-1'], + isRecruiting: true, + createdAt: '2024-01-01T00:00:00.000Z', + }, + membership: { + role: 'owner', + joinedAt: '2024-01-01T00:00:00.000Z', + isActive: true, + }, + isOwner: true, + canManage: true, + }); + }); + + it('should map result for manager', () => { + const result = { + driverId: 'driver-456', + team: { + id: 'team-789', + name: { toString: () => 'Team Beta' } as any, + tag: { toString: () => 'TB' } as any, + description: { toString: () => '' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [] as any[], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-02') } as any, + } as any, + membership: { + role: 'manager' as const, + joinedAt: new Date('2024-01-02'), + status: 'active' as const, + } as any, + }; + + presenter.present(result); + + const model = presenter.getResponseModel(); + expect(model?.isOwner).toBe(false); + expect(model?.canManage).toBe(true); + expect(model?.membership.role).toBe('manager'); + }); + + it('should map result for member', () => { + const result = { + driverId: 'driver-789', + team: { + id: 'team-abc', + name: { toString: () => 'Team Gamma' } as any, + tag: { toString: () => 'TG' } as any, + description: { toString: () => 'Test team' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [{ toString: () => 'league-2' } as any], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-03') } as any, + } as any, + membership: { + role: 'driver' as const, + joinedAt: new Date('2024-01-03'), + status: 'active' as const, + } as any, + }; + + presenter.present(result); + + const model = presenter.getResponseModel(); + expect(model?.isOwner).toBe(false); + expect(model?.canManage).toBe(false); + expect(model?.membership.role).toBe('member'); + }); + + it('should handle empty description', () => { + const result = { + driverId: 'driver-123', + team: { + id: 'team-456', + name: { toString: () => 'Team' } as any, + tag: { toString: () => 'T' } as any, + description: { toString: () => '' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [] as any[], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-01') } as any, + } as any, + membership: { + role: 'owner' as const, + joinedAt: new Date('2024-01-01'), + status: 'active' as const, + } as any, + }; + + presenter.present(result); + + const model = presenter.getResponseModel(); + expect(model?.team.description).toBe(''); + }); + + it('should handle empty leagues', () => { + const result = { + driverId: 'driver-123', + team: { + id: 'team-456', + name: { toString: () => 'Team' } as any, + tag: { toString: () => 'T' } as any, + description: { toString: () => 'Test' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [] as any[], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-01') } as any, + } as any, + membership: { + role: 'owner' as const, + joinedAt: new Date('2024-01-01'), + status: 'active' as const, + } as any, + }; + + presenter.present(result); + + const model = presenter.getResponseModel(); + expect(model?.team.leagues).toEqual([]); + }); + }); + + describe('reset', () => { + it('should clear the model', () => { + presenter.present({ + driverId: 'driver-123', + team: { + id: 'team-456', + name: { toString: () => 'Team' } as any, + tag: { toString: () => 'T' } as any, + description: { toString: () => '' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [] as any[], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-01') } as any, + } as any, + membership: { + role: 'owner' as const, + joinedAt: new Date('2024-01-01'), + status: 'active' as const, + } as any, + }); + expect(presenter.getResponseModel()).toBeDefined(); + + presenter.reset(); + expect(presenter.getResponseModel()).toBeNull(); + }); + }); + + describe('getResponseModel', () => { + it('should return null before present()', () => { + expect(presenter.getResponseModel()).toBeNull(); + }); + + it('should return model after present()', () => { + presenter.present({ + driverId: 'driver-123', + team: { + id: 'team-456', + name: { toString: () => 'Team' } as any, + tag: { toString: () => 'T' } as any, + description: { toString: () => '' } as any, + ownerId: { toString: () => 'driver-123' } as any, + leagues: [] as any[], + isRecruiting: false, + createdAt: { toDate: () => new Date('2024-01-01') } as any, + } as any, + membership: { + role: 'owner' as const, + joinedAt: new Date('2024-01-01'), + status: 'active' as const, + } as any, + }); + expect(presenter.getResponseModel()).not.toBeNull(); + }); + }); +}); \ No newline at end of file