969 lines
35 KiB
TypeScript
969 lines
35 KiB
TypeScript
/**
|
|
* Integration Test: Profile Overview Use Case Orchestration
|
|
*
|
|
* Tests the orchestration logic of profile overview-related Use Cases:
|
|
* - GetProfileOverviewUseCase: Retrieves driver's profile overview with stats, team memberships, and social summary
|
|
* - UpdateDriverProfileUseCase: Updates driver's profile information
|
|
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
|
* - Uses In-Memory adapters for fast, deterministic testing
|
|
*
|
|
* Focus: Business logic orchestration, NOT UI rendering
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
|
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
|
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
|
import { InMemorySocialGraphRepository } from '../../../adapters/social/persistence/inmemory/InMemorySocialAndFeed';
|
|
import { InMemoryDriverStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
|
import { InMemoryDriverExtendedProfileProvider } from '../../../adapters/racing/ports/InMemoryDriverExtendedProfileProvider';
|
|
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
|
import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository';
|
|
import { InMemoryStandingRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryStandingRepository';
|
|
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
|
import { GetProfileOverviewUseCase } from '../../../core/racing/application/use-cases/GetProfileOverviewUseCase';
|
|
import { UpdateDriverProfileUseCase } from '../../../core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
|
import { DriverStatsUseCase } from '../../../core/racing/application/use-cases/DriverStatsUseCase';
|
|
import { RankingUseCase } from '../../../core/racing/application/use-cases/RankingUseCase';
|
|
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
|
import { Team } from '../../../core/racing/domain/entities/Team';
|
|
import { TeamMembership } from '../../../core/racing/domain/types/TeamMembership';
|
|
import { DriverStats } from '../../../core/racing/application/use-cases/DriverStatsUseCase';
|
|
import { DriverRanking } from '../../../core/racing/application/use-cases/RankingUseCase';
|
|
import { Logger } from '../../../core/shared/domain/Logger';
|
|
|
|
// Mock logger for testing
|
|
class MockLogger implements Logger {
|
|
debug(message: string, ...args: any[]): void {}
|
|
info(message: string, ...args: any[]): void {}
|
|
warn(message: string, ...args: any[]): void {}
|
|
error(message: string, ...args: any[]): void {}
|
|
}
|
|
|
|
describe('Profile Overview Use Case Orchestration', () => {
|
|
let driverRepository: InMemoryDriverRepository;
|
|
let teamRepository: InMemoryTeamRepository;
|
|
let teamMembershipRepository: InMemoryTeamMembershipRepository;
|
|
let socialRepository: InMemorySocialGraphRepository;
|
|
let driverStatsRepository: InMemoryDriverStatsRepository;
|
|
let driverExtendedProfileProvider: InMemoryDriverExtendedProfileProvider;
|
|
let eventPublisher: InMemoryEventPublisher;
|
|
let resultRepository: InMemoryResultRepository;
|
|
let standingRepository: InMemoryStandingRepository;
|
|
let raceRepository: InMemoryRaceRepository;
|
|
let driverStatsUseCase: DriverStatsUseCase;
|
|
let rankingUseCase: RankingUseCase;
|
|
let getProfileOverviewUseCase: GetProfileOverviewUseCase;
|
|
let updateDriverProfileUseCase: UpdateDriverProfileUseCase;
|
|
let logger: MockLogger;
|
|
|
|
beforeAll(() => {
|
|
logger = new MockLogger();
|
|
driverRepository = new InMemoryDriverRepository(logger);
|
|
teamRepository = new InMemoryTeamRepository(logger);
|
|
teamMembershipRepository = new InMemoryTeamMembershipRepository(logger);
|
|
socialRepository = new InMemorySocialGraphRepository(logger);
|
|
driverStatsRepository = new InMemoryDriverStatsRepository(logger);
|
|
driverExtendedProfileProvider = new InMemoryDriverExtendedProfileProvider(logger);
|
|
eventPublisher = new InMemoryEventPublisher();
|
|
resultRepository = new InMemoryResultRepository(logger, raceRepository);
|
|
standingRepository = new InMemoryStandingRepository(logger, {}, resultRepository, raceRepository);
|
|
raceRepository = new InMemoryRaceRepository(logger);
|
|
driverStatsUseCase = new DriverStatsUseCase(resultRepository, standingRepository, driverStatsRepository, logger);
|
|
rankingUseCase = new RankingUseCase(standingRepository, driverRepository, driverStatsRepository, logger);
|
|
getProfileOverviewUseCase = new GetProfileOverviewUseCase(
|
|
driverRepository,
|
|
teamRepository,
|
|
teamMembershipRepository,
|
|
socialRepository,
|
|
driverExtendedProfileProvider,
|
|
driverStatsUseCase,
|
|
rankingUseCase
|
|
);
|
|
updateDriverProfileUseCase = new UpdateDriverProfileUseCase(driverRepository, logger);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await driverRepository.clear();
|
|
await teamRepository.clear();
|
|
await teamMembershipRepository.clear();
|
|
await socialRepository.clear();
|
|
await driverStatsRepository.clear();
|
|
eventPublisher.clear();
|
|
});
|
|
|
|
describe('GetProfileOverviewUseCase - Success Path', () => {
|
|
it('should retrieve complete profile overview for driver with all data', async () => {
|
|
// Scenario: Driver with complete profile data
|
|
// Given: A driver exists with complete personal information
|
|
const driverId = 'driver-123';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '12345',
|
|
name: 'John Doe',
|
|
country: 'US',
|
|
bio: 'Professional racing driver with 10 years experience',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has complete statistics
|
|
const stats: DriverStats = {
|
|
totalRaces: 50,
|
|
wins: 15,
|
|
podiums: 25,
|
|
dnfs: 5,
|
|
avgFinish: 8.5,
|
|
bestFinish: 1,
|
|
worstFinish: 20,
|
|
finishRate: 90,
|
|
winRate: 30,
|
|
podiumRate: 50,
|
|
percentile: 85,
|
|
rating: 1850,
|
|
consistency: 92,
|
|
overallRank: 42,
|
|
};
|
|
await driverStatsRepository.saveDriverStats(driverId, stats);
|
|
|
|
// And: The driver is a member of a team
|
|
const team = Team.create({
|
|
id: 'team-1',
|
|
name: 'Racing Team',
|
|
tag: 'RT',
|
|
description: 'Professional racing team',
|
|
ownerId: 'owner-1',
|
|
isRecruiting: true,
|
|
});
|
|
await teamRepository.create(team);
|
|
|
|
const membership: TeamMembership = {
|
|
teamId: 'team-1',
|
|
driverId: driverId,
|
|
role: 'Driver',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-01-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership);
|
|
|
|
// And: The driver has friends
|
|
const friendDriver = Driver.create({
|
|
id: 'friend-1',
|
|
iracingId: '67890',
|
|
name: 'Jane Smith',
|
|
country: 'UK',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(friendDriver);
|
|
await socialRepository.seed({
|
|
drivers: [driver, friendDriver],
|
|
friendships: [{ driverId: driverId, friendId: 'friend-1' }],
|
|
feedEvents: [],
|
|
});
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain all profile sections
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
// And: Driver info should be complete
|
|
expect(profile.driverInfo.driver.id).toBe(driverId);
|
|
expect(profile.driverInfo.driver.name.toString()).toBe('John Doe');
|
|
expect(profile.driverInfo.driver.country.toString()).toBe('US');
|
|
expect(profile.driverInfo.driver.bio?.toString()).toBe('Professional racing driver with 10 years experience');
|
|
expect(profile.driverInfo.totalDrivers).toBeGreaterThan(0);
|
|
expect(profile.driverInfo.globalRank).toBe(42);
|
|
expect(profile.driverInfo.consistency).toBe(92);
|
|
expect(profile.driverInfo.rating).toBe(1850);
|
|
|
|
// And: Stats should be complete
|
|
expect(profile.stats).not.toBeNull();
|
|
expect(profile.stats!.totalRaces).toBe(50);
|
|
expect(profile.stats!.wins).toBe(15);
|
|
expect(profile.stats!.podiums).toBe(25);
|
|
expect(profile.stats!.dnfs).toBe(5);
|
|
expect(profile.stats!.avgFinish).toBe(8.5);
|
|
expect(profile.stats!.bestFinish).toBe(1);
|
|
expect(profile.stats!.worstFinish).toBe(20);
|
|
expect(profile.stats!.finishRate).toBe(90);
|
|
expect(profile.stats!.winRate).toBe(30);
|
|
expect(profile.stats!.podiumRate).toBe(50);
|
|
expect(profile.stats!.percentile).toBe(85);
|
|
expect(profile.stats!.rating).toBe(1850);
|
|
expect(profile.stats!.consistency).toBe(92);
|
|
expect(profile.stats!.overallRank).toBe(42);
|
|
|
|
// And: Finish distribution should be calculated
|
|
expect(profile.finishDistribution).not.toBeNull();
|
|
expect(profile.finishDistribution!.totalRaces).toBe(50);
|
|
expect(profile.finishDistribution!.wins).toBe(15);
|
|
expect(profile.finishDistribution!.podiums).toBe(25);
|
|
expect(profile.finishDistribution!.dnfs).toBe(5);
|
|
expect(profile.finishDistribution!.topTen).toBeGreaterThan(0);
|
|
expect(profile.finishDistribution!.other).toBeGreaterThan(0);
|
|
|
|
// And: Team memberships should be present
|
|
expect(profile.teamMemberships).toHaveLength(1);
|
|
expect(profile.teamMemberships[0].team.id).toBe('team-1');
|
|
expect(profile.teamMemberships[0].team.name.toString()).toBe('Racing Team');
|
|
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
|
|
expect(profile.teamMemberships[0].membership.status).toBe('active');
|
|
|
|
// And: Social summary should show friends
|
|
expect(profile.socialSummary.friendsCount).toBe(1);
|
|
expect(profile.socialSummary.friends).toHaveLength(1);
|
|
expect(profile.socialSummary.friends[0].id).toBe('friend-1');
|
|
expect(profile.socialSummary.friends[0].name.toString()).toBe('Jane Smith');
|
|
|
|
// And: Extended profile should be present (generated by provider)
|
|
expect(profile.extendedProfile).not.toBeNull();
|
|
expect(profile.extendedProfile!.socialHandles).toBeInstanceOf(Array);
|
|
expect(profile.extendedProfile!.achievements).toBeInstanceOf(Array);
|
|
});
|
|
|
|
it('should retrieve profile overview for driver with minimal data', async () => {
|
|
// Scenario: Driver with minimal profile data
|
|
// Given: A driver exists with minimal information
|
|
const driverId = 'driver-456';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '78901',
|
|
name: 'New Driver',
|
|
country: 'DE',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has no statistics
|
|
// And: The driver is not a member of any team
|
|
// And: The driver has no friends
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain basic driver info
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
// And: Driver info should be present
|
|
expect(profile.driverInfo.driver.id).toBe(driverId);
|
|
expect(profile.driverInfo.driver.name.toString()).toBe('New Driver');
|
|
expect(profile.driverInfo.driver.country.toString()).toBe('DE');
|
|
expect(profile.driverInfo.totalDrivers).toBeGreaterThan(0);
|
|
|
|
// And: Stats should be null (no data)
|
|
expect(profile.stats).toBeNull();
|
|
|
|
// And: Finish distribution should be null
|
|
expect(profile.finishDistribution).toBeNull();
|
|
|
|
// And: Team memberships should be empty
|
|
expect(profile.teamMemberships).toHaveLength(0);
|
|
|
|
// And: Social summary should show no friends
|
|
expect(profile.socialSummary.friendsCount).toBe(0);
|
|
expect(profile.socialSummary.friends).toHaveLength(0);
|
|
|
|
// And: Extended profile should be present (generated by provider)
|
|
expect(profile.extendedProfile).not.toBeNull();
|
|
});
|
|
|
|
it('should retrieve profile overview with multiple team memberships', async () => {
|
|
// Scenario: Driver with multiple team memberships
|
|
// Given: A driver exists
|
|
const driverId = 'driver-789';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '11111',
|
|
name: 'Multi Team Driver',
|
|
country: 'FR',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver is a member of multiple teams
|
|
const team1 = Team.create({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
tag: 'TA',
|
|
description: 'Team A',
|
|
ownerId: 'owner-1',
|
|
isRecruiting: true,
|
|
});
|
|
await teamRepository.create(team1);
|
|
|
|
const team2 = Team.create({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
tag: 'TB',
|
|
description: 'Team B',
|
|
ownerId: 'owner-2',
|
|
isRecruiting: false,
|
|
});
|
|
await teamRepository.create(team2);
|
|
|
|
const membership1: TeamMembership = {
|
|
teamId: 'team-1',
|
|
driverId: driverId,
|
|
role: 'Driver',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-01-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership1);
|
|
|
|
const membership2: TeamMembership = {
|
|
teamId: 'team-2',
|
|
driverId: driverId,
|
|
role: 'Admin',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-02-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership2);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain all team memberships
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
// And: Team memberships should include both teams
|
|
expect(profile.teamMemberships).toHaveLength(2);
|
|
expect(profile.teamMemberships[0].team.id).toBe('team-1');
|
|
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
|
|
expect(profile.teamMemberships[1].team.id).toBe('team-2');
|
|
expect(profile.teamMemberships[1].membership.role).toBe('Admin');
|
|
|
|
// And: Team memberships should be sorted by joined date
|
|
expect(profile.teamMemberships[0].membership.joinedAt.getTime()).toBeLessThan(
|
|
profile.teamMemberships[1].membership.joinedAt.getTime()
|
|
);
|
|
});
|
|
|
|
it('should retrieve profile overview with multiple friends', async () => {
|
|
// Scenario: Driver with multiple friends
|
|
// Given: A driver exists
|
|
const driverId = 'driver-friends';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '22222',
|
|
name: 'Social Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has multiple friends
|
|
const friend1 = Driver.create({
|
|
id: 'friend-1',
|
|
iracingId: '33333',
|
|
name: 'Friend 1',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(friend1);
|
|
|
|
const friend2 = Driver.create({
|
|
id: 'friend-2',
|
|
iracingId: '44444',
|
|
name: 'Friend 2',
|
|
country: 'UK',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(friend2);
|
|
|
|
const friend3 = Driver.create({
|
|
id: 'friend-3',
|
|
iracingId: '55555',
|
|
name: 'Friend 3',
|
|
country: 'DE',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(friend3);
|
|
|
|
await socialRepository.seed({
|
|
drivers: [driver, friend1, friend2, friend3],
|
|
friendships: [
|
|
{ driverId: driverId, friendId: 'friend-1' },
|
|
{ driverId: driverId, friendId: 'friend-2' },
|
|
{ driverId: driverId, friendId: 'friend-3' },
|
|
],
|
|
feedEvents: [],
|
|
});
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain all friends
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
// And: Social summary should show 3 friends
|
|
expect(profile.socialSummary.friendsCount).toBe(3);
|
|
expect(profile.socialSummary.friends).toHaveLength(3);
|
|
|
|
// And: All friends should be present
|
|
const friendIds = profile.socialSummary.friends.map(f => f.id);
|
|
expect(friendIds).toContain('friend-1');
|
|
expect(friendIds).toContain('friend-2');
|
|
expect(friendIds).toContain('friend-3');
|
|
});
|
|
});
|
|
|
|
describe('GetProfileOverviewUseCase - Edge Cases', () => {
|
|
it('should handle driver with no statistics', async () => {
|
|
// Scenario: Driver without statistics
|
|
// Given: A driver exists
|
|
const driverId = 'driver-no-stats';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '66666',
|
|
name: 'No Stats Driver',
|
|
country: 'CA',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has no statistics
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain driver info with null stats
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
expect(profile.driverInfo.driver.id).toBe(driverId);
|
|
expect(profile.stats).toBeNull();
|
|
expect(profile.finishDistribution).toBeNull();
|
|
});
|
|
|
|
it('should handle driver with no team memberships', async () => {
|
|
// Scenario: Driver without team memberships
|
|
// Given: A driver exists
|
|
const driverId = 'driver-no-teams';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '77777',
|
|
name: 'Solo Driver',
|
|
country: 'IT',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver is not a member of any team
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain driver info with empty team memberships
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
expect(profile.driverInfo.driver.id).toBe(driverId);
|
|
expect(profile.teamMemberships).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle driver with no friends', async () => {
|
|
// Scenario: Driver without friends
|
|
// Given: A driver exists
|
|
const driverId = 'driver-no-friends';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '88888',
|
|
name: 'Lonely Driver',
|
|
country: 'ES',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has no friends
|
|
// When: GetProfileOverviewUseCase.execute() is called with driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should contain driver info with empty social summary
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
|
|
expect(profile.driverInfo.driver.id).toBe(driverId);
|
|
expect(profile.socialSummary.friendsCount).toBe(0);
|
|
expect(profile.socialSummary.friends).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('GetProfileOverviewUseCase - Error Handling', () => {
|
|
it('should return error when driver does not exist', async () => {
|
|
// Scenario: Non-existent driver
|
|
// Given: No driver exists with the given ID
|
|
const nonExistentDriverId = 'non-existent-driver';
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called with non-existent driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId: nonExistentDriverId });
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
|
expect(error.details.message).toBe('Driver not found');
|
|
});
|
|
|
|
it('should return error when driver ID is invalid', async () => {
|
|
// Scenario: Invalid driver ID
|
|
// Given: An invalid driver ID (empty string)
|
|
const invalidDriverId = '';
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called with invalid driver ID
|
|
const result = await getProfileOverviewUseCase.execute({ driverId: invalidDriverId });
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
|
expect(error.details.message).toBe('Driver not found');
|
|
});
|
|
});
|
|
|
|
describe('UpdateDriverProfileUseCase - Success Path', () => {
|
|
it('should update driver bio', async () => {
|
|
// Scenario: Update driver bio
|
|
// Given: A driver exists with bio
|
|
const driverId = 'driver-update-bio';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '99999',
|
|
name: 'Update Driver',
|
|
country: 'US',
|
|
bio: 'Original bio',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with new bio
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId,
|
|
bio: 'Updated bio',
|
|
});
|
|
|
|
// Then: The operation should succeed
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
// And: The driver's bio should be updated
|
|
const updatedDriver = await driverRepository.findById(driverId);
|
|
expect(updatedDriver).not.toBeNull();
|
|
expect(updatedDriver!.bio?.toString()).toBe('Updated bio');
|
|
});
|
|
|
|
it('should update driver country', async () => {
|
|
// Scenario: Update driver country
|
|
// Given: A driver exists with country
|
|
const driverId = 'driver-update-country';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '10101',
|
|
name: 'Country Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with new country
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId,
|
|
country: 'DE',
|
|
});
|
|
|
|
// Then: The operation should succeed
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
// And: The driver's country should be updated
|
|
const updatedDriver = await driverRepository.findById(driverId);
|
|
expect(updatedDriver).not.toBeNull();
|
|
expect(updatedDriver!.country.toString()).toBe('DE');
|
|
});
|
|
|
|
it('should update multiple profile fields at once', async () => {
|
|
// Scenario: Update multiple fields
|
|
// Given: A driver exists
|
|
const driverId = 'driver-update-multiple';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '11111',
|
|
name: 'Multi Update Driver',
|
|
country: 'US',
|
|
bio: 'Original bio',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with multiple updates
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId,
|
|
bio: 'Updated bio',
|
|
country: 'FR',
|
|
});
|
|
|
|
// Then: The operation should succeed
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
// And: Both fields should be updated
|
|
const updatedDriver = await driverRepository.findById(driverId);
|
|
expect(updatedDriver).not.toBeNull();
|
|
expect(updatedDriver!.bio?.toString()).toBe('Updated bio');
|
|
expect(updatedDriver!.country.toString()).toBe('FR');
|
|
});
|
|
});
|
|
|
|
describe('UpdateDriverProfileUseCase - Validation', () => {
|
|
it('should reject update with empty bio', async () => {
|
|
// Scenario: Empty bio
|
|
// Given: A driver exists
|
|
const driverId = 'driver-empty-bio';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '12121',
|
|
name: 'Empty Bio Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with empty bio
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId,
|
|
bio: '',
|
|
});
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('INVALID_PROFILE_DATA');
|
|
expect(error.details.message).toBe('Profile data is invalid');
|
|
});
|
|
|
|
it('should reject update with empty country', async () => {
|
|
// Scenario: Empty country
|
|
// Given: A driver exists
|
|
const driverId = 'driver-empty-country';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '13131',
|
|
name: 'Empty Country Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with empty country
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId,
|
|
country: '',
|
|
});
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('INVALID_PROFILE_DATA');
|
|
expect(error.details.message).toBe('Profile data is invalid');
|
|
});
|
|
});
|
|
|
|
describe('UpdateDriverProfileUseCase - Error Handling', () => {
|
|
it('should return error when driver does not exist', async () => {
|
|
// Scenario: Non-existent driver
|
|
// Given: No driver exists with the given ID
|
|
const nonExistentDriverId = 'non-existent-driver';
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with non-existent driver ID
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId: nonExistentDriverId,
|
|
bio: 'New bio',
|
|
});
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
|
expect(error.details.message).toContain('Driver with id');
|
|
});
|
|
|
|
it('should return error when driver ID is invalid', async () => {
|
|
// Scenario: Invalid driver ID
|
|
// Given: An invalid driver ID (empty string)
|
|
const invalidDriverId = '';
|
|
|
|
// When: UpdateDriverProfileUseCase.execute() is called with invalid driver ID
|
|
const result = await updateDriverProfileUseCase.execute({
|
|
driverId: invalidDriverId,
|
|
bio: 'New bio',
|
|
});
|
|
|
|
// Then: Should return error
|
|
expect(result.isErr()).toBe(true);
|
|
const error = result.getError();
|
|
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
|
expect(error.details.message).toContain('Driver with id');
|
|
});
|
|
});
|
|
|
|
describe('Profile Data Orchestration', () => {
|
|
it('should correctly calculate win percentage from race results', async () => {
|
|
// Scenario: Win percentage calculation
|
|
// Given: A driver exists
|
|
const driverId = 'driver-win-percentage';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '14141',
|
|
name: 'Win Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has 10 race starts and 3 wins
|
|
const stats: DriverStats = {
|
|
totalRaces: 10,
|
|
wins: 3,
|
|
podiums: 5,
|
|
dnfs: 0,
|
|
avgFinish: 5.0,
|
|
bestFinish: 1,
|
|
worstFinish: 10,
|
|
finishRate: 100,
|
|
winRate: 30,
|
|
podiumRate: 50,
|
|
percentile: 70,
|
|
rating: 1600,
|
|
consistency: 85,
|
|
overallRank: 100,
|
|
};
|
|
await driverStatsRepository.saveDriverStats(driverId, stats);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should show win percentage as 30%
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
expect(profile.stats!.winRate).toBe(30);
|
|
});
|
|
|
|
it('should correctly calculate podium rate from race results', async () => {
|
|
// Scenario: Podium rate calculation
|
|
// Given: A driver exists
|
|
const driverId = 'driver-podium-rate';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '15151',
|
|
name: 'Podium Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has 10 race starts and 5 podiums
|
|
const stats: DriverStats = {
|
|
totalRaces: 10,
|
|
wins: 2,
|
|
podiums: 5,
|
|
dnfs: 0,
|
|
avgFinish: 4.0,
|
|
bestFinish: 1,
|
|
worstFinish: 8,
|
|
finishRate: 100,
|
|
winRate: 20,
|
|
podiumRate: 50,
|
|
percentile: 60,
|
|
rating: 1550,
|
|
consistency: 80,
|
|
overallRank: 150,
|
|
};
|
|
await driverStatsRepository.saveDriverStats(driverId, stats);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should show podium rate as 50%
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
expect(profile.stats!.podiumRate).toBe(50);
|
|
});
|
|
|
|
it('should correctly calculate finish distribution', async () => {
|
|
// Scenario: Finish distribution calculation
|
|
// Given: A driver exists
|
|
const driverId = 'driver-finish-dist';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '16161',
|
|
name: 'Finish Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has 20 race starts with various finishes
|
|
const stats: DriverStats = {
|
|
totalRaces: 20,
|
|
wins: 5,
|
|
podiums: 8,
|
|
dnfs: 2,
|
|
avgFinish: 6.5,
|
|
bestFinish: 1,
|
|
worstFinish: 15,
|
|
finishRate: 90,
|
|
winRate: 25,
|
|
podiumRate: 40,
|
|
percentile: 75,
|
|
rating: 1700,
|
|
consistency: 88,
|
|
overallRank: 75,
|
|
};
|
|
await driverStatsRepository.saveDriverStats(driverId, stats);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: The result should show correct finish distribution
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
expect(profile.finishDistribution!.totalRaces).toBe(20);
|
|
expect(profile.finishDistribution!.wins).toBe(5);
|
|
expect(profile.finishDistribution!.podiums).toBe(8);
|
|
expect(profile.finishDistribution!.dnfs).toBe(2);
|
|
expect(profile.finishDistribution!.topTen).toBeGreaterThan(0);
|
|
expect(profile.finishDistribution!.other).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should correctly format team affiliation with role', async () => {
|
|
// Scenario: Team affiliation formatting
|
|
// Given: A driver exists
|
|
const driverId = 'driver-team-affiliation';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '17171',
|
|
name: 'Team Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver is affiliated with a team
|
|
const team = Team.create({
|
|
id: 'team-affiliation',
|
|
name: 'Affiliation Team',
|
|
tag: 'AT',
|
|
description: 'Team for testing',
|
|
ownerId: 'owner-1',
|
|
isRecruiting: true,
|
|
});
|
|
await teamRepository.create(team);
|
|
|
|
const membership: TeamMembership = {
|
|
teamId: 'team-affiliation',
|
|
driverId: driverId,
|
|
role: 'Driver',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-01-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: Team affiliation should show team name and role
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
expect(profile.teamMemberships).toHaveLength(1);
|
|
expect(profile.teamMemberships[0].team.name.toString()).toBe('Affiliation Team');
|
|
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
|
|
});
|
|
|
|
it('should correctly identify driver role in each team', async () => {
|
|
// Scenario: Driver role identification
|
|
// Given: A driver exists
|
|
const driverId = 'driver-roles';
|
|
const driver = Driver.create({
|
|
id: driverId,
|
|
iracingId: '18181',
|
|
name: 'Role Driver',
|
|
country: 'US',
|
|
avatarRef: undefined,
|
|
});
|
|
await driverRepository.create(driver);
|
|
|
|
// And: The driver has different roles in different teams
|
|
const team1 = Team.create({
|
|
id: 'team-role-1',
|
|
name: 'Team A',
|
|
tag: 'TA',
|
|
description: 'Team A',
|
|
ownerId: 'owner-1',
|
|
isRecruiting: true,
|
|
});
|
|
await teamRepository.create(team1);
|
|
|
|
const team2 = Team.create({
|
|
id: 'team-role-2',
|
|
name: 'Team B',
|
|
tag: 'TB',
|
|
description: 'Team B',
|
|
ownerId: 'owner-2',
|
|
isRecruiting: false,
|
|
});
|
|
await teamRepository.create(team2);
|
|
|
|
const team3 = Team.create({
|
|
id: 'team-role-3',
|
|
name: 'Team C',
|
|
tag: 'TC',
|
|
description: 'Team C',
|
|
ownerId: driverId,
|
|
isRecruiting: true,
|
|
});
|
|
await teamRepository.create(team3);
|
|
|
|
const membership1: TeamMembership = {
|
|
teamId: 'team-role-1',
|
|
driverId: driverId,
|
|
role: 'Driver',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-01-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership1);
|
|
|
|
const membership2: TeamMembership = {
|
|
teamId: 'team-role-2',
|
|
driverId: driverId,
|
|
role: 'Admin',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-02-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership2);
|
|
|
|
const membership3: TeamMembership = {
|
|
teamId: 'team-role-3',
|
|
driverId: driverId,
|
|
role: 'Owner',
|
|
status: 'active',
|
|
joinedAt: new Date('2024-03-01'),
|
|
};
|
|
await teamMembershipRepository.saveMembership(membership3);
|
|
|
|
// When: GetProfileOverviewUseCase.execute() is called
|
|
const result = await getProfileOverviewUseCase.execute({ driverId });
|
|
|
|
// Then: Each team should show the correct role
|
|
expect(result.isOk()).toBe(true);
|
|
const profile = result.unwrap();
|
|
expect(profile.teamMemberships).toHaveLength(3);
|
|
|
|
const teamARole = profile.teamMemberships.find(m => m.team.id === 'team-role-1')?.membership.role;
|
|
const teamBRole = profile.teamMemberships.find(m => m.team.id === 'team-role-2')?.membership.role;
|
|
const teamCRole = profile.teamMemberships.find(m => m.team.id === 'team-role-3')?.membership.role;
|
|
|
|
expect(teamARole).toBe('Driver');
|
|
expect(teamBRole).toBe('Admin');
|
|
expect(teamCRole).toBe('Owner');
|
|
});
|
|
});
|
|
});
|