integration tests
This commit is contained in:
@@ -0,0 +1,968 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user