321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
/**
|
|
* Unit tests for GetDashboardUseCase
|
|
*
|
|
* Tests cover:
|
|
* 1) Validation of driverId (empty and whitespace)
|
|
* 2) Driver not found
|
|
* 3) Filters invalid races (missing trackName, past dates)
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { GetDashboardUseCase } from './GetDashboardUseCase';
|
|
import { ValidationError } from '../../../shared/errors/ValidationError';
|
|
import { DriverNotFoundError } from '../../domain/errors/DriverNotFoundError';
|
|
import { DashboardRepository } from '../ports/DashboardRepository';
|
|
import { DashboardEventPublisher } from '../ports/DashboardEventPublisher';
|
|
import { Logger } from '../../../shared/domain/Logger';
|
|
import { DriverData, RaceData, LeagueStandingData, ActivityData } from '../ports/DashboardRepository';
|
|
|
|
describe('GetDashboardUseCase', () => {
|
|
let mockDriverRepository: DashboardRepository;
|
|
let mockRaceRepository: DashboardRepository;
|
|
let mockLeagueRepository: DashboardRepository;
|
|
let mockActivityRepository: DashboardRepository;
|
|
let mockEventPublisher: DashboardEventPublisher;
|
|
let mockLogger: Logger;
|
|
|
|
let useCase: GetDashboardUseCase;
|
|
|
|
beforeEach(() => {
|
|
// Mock all ports with vi.fn()
|
|
mockDriverRepository = {
|
|
findDriverById: vi.fn(),
|
|
getUpcomingRaces: vi.fn(),
|
|
getLeagueStandings: vi.fn(),
|
|
getRecentActivity: vi.fn(),
|
|
getFriends: vi.fn(),
|
|
};
|
|
|
|
mockRaceRepository = {
|
|
findDriverById: vi.fn(),
|
|
getUpcomingRaces: vi.fn(),
|
|
getLeagueStandings: vi.fn(),
|
|
getRecentActivity: vi.fn(),
|
|
getFriends: vi.fn(),
|
|
};
|
|
|
|
mockLeagueRepository = {
|
|
findDriverById: vi.fn(),
|
|
getUpcomingRaces: vi.fn(),
|
|
getLeagueStandings: vi.fn(),
|
|
getRecentActivity: vi.fn(),
|
|
getFriends: vi.fn(),
|
|
};
|
|
|
|
mockActivityRepository = {
|
|
findDriverById: vi.fn(),
|
|
getUpcomingRaces: vi.fn(),
|
|
getLeagueStandings: vi.fn(),
|
|
getRecentActivity: vi.fn(),
|
|
getFriends: vi.fn(),
|
|
};
|
|
|
|
mockEventPublisher = {
|
|
publishDashboardAccessed: vi.fn(),
|
|
publishDashboardError: vi.fn(),
|
|
};
|
|
|
|
mockLogger = {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
|
|
useCase = new GetDashboardUseCase({
|
|
driverRepository: mockDriverRepository,
|
|
raceRepository: mockRaceRepository,
|
|
leagueRepository: mockLeagueRepository,
|
|
activityRepository: mockActivityRepository,
|
|
eventPublisher: mockEventPublisher,
|
|
logger: mockLogger,
|
|
});
|
|
});
|
|
|
|
describe('Scenario 1: Validation of driverId', () => {
|
|
it('should throw ValidationError when driverId is empty string', async () => {
|
|
// Given
|
|
const query = { driverId: '' };
|
|
|
|
// When & Then
|
|
await expect(useCase.execute(query)).rejects.toThrow(ValidationError);
|
|
await expect(useCase.execute(query)).rejects.toThrow('Driver ID cannot be empty');
|
|
|
|
// Verify no repositories were called
|
|
expect(mockDriverRepository.findDriverById).not.toHaveBeenCalled();
|
|
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
|
|
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
|
|
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
|
|
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw ValidationError when driverId is whitespace only', async () => {
|
|
// Given
|
|
const query = { driverId: ' ' };
|
|
|
|
// When & Then
|
|
await expect(useCase.execute(query)).rejects.toThrow(ValidationError);
|
|
await expect(useCase.execute(query)).rejects.toThrow('Driver ID cannot be empty');
|
|
|
|
// Verify no repositories were called
|
|
expect(mockDriverRepository.findDriverById).not.toHaveBeenCalled();
|
|
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
|
|
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
|
|
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
|
|
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Scenario 2: Driver not found', () => {
|
|
it('should throw DriverNotFoundError when driverRepository.findDriverById returns null', async () => {
|
|
// Given
|
|
const query = { driverId: 'driver-123' };
|
|
(mockDriverRepository.findDriverById as any).mockResolvedValue(null);
|
|
|
|
// When & Then
|
|
await expect(useCase.execute(query)).rejects.toThrow(DriverNotFoundError);
|
|
await expect(useCase.execute(query)).rejects.toThrow('Driver with ID "driver-123" not found');
|
|
|
|
// Verify driver repository was called
|
|
expect(mockDriverRepository.findDriverById).toHaveBeenCalledWith('driver-123');
|
|
|
|
// Verify other repositories were not called (since driver not found)
|
|
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
|
|
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
|
|
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
|
|
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Scenario 3: Filters invalid races', () => {
|
|
it('should exclude races missing trackName', async () => {
|
|
// Given
|
|
const query = { driverId: 'driver-123' };
|
|
|
|
// Mock driver exists
|
|
(mockDriverRepository.findDriverById as any).mockResolvedValue({
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
rating: 1500,
|
|
rank: 10,
|
|
starts: 50,
|
|
wins: 10,
|
|
podiums: 20,
|
|
leagues: 3,
|
|
} as DriverData);
|
|
|
|
// Mock races with missing trackName
|
|
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
|
|
{
|
|
id: 'race-1',
|
|
trackName: '', // Missing trackName
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-25T10:00:00.000Z'),
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
trackName: 'Track A',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-26T10:00:00.000Z'),
|
|
},
|
|
] as RaceData[]);
|
|
|
|
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
|
|
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
|
|
|
|
// When
|
|
const result = await useCase.execute(query);
|
|
|
|
// Then
|
|
expect(result.upcomingRaces).toHaveLength(1);
|
|
expect(result.upcomingRaces[0].trackName).toBe('Track A');
|
|
});
|
|
|
|
it('should exclude races with past scheduledDate', async () => {
|
|
// Given
|
|
const query = { driverId: 'driver-123' };
|
|
|
|
// Mock driver exists
|
|
(mockDriverRepository.findDriverById as any).mockResolvedValue({
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
rating: 1500,
|
|
rank: 10,
|
|
starts: 50,
|
|
wins: 10,
|
|
podiums: 20,
|
|
leagues: 3,
|
|
} as DriverData);
|
|
|
|
// Mock races with past dates
|
|
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
|
|
{
|
|
id: 'race-1',
|
|
trackName: 'Track A',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-23T10:00:00.000Z'), // Past
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
trackName: 'Track B',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-26T10:00:00.000Z'), // Future
|
|
},
|
|
] as RaceData[]);
|
|
|
|
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
|
|
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
|
|
|
|
// When
|
|
const result = await useCase.execute(query);
|
|
|
|
// Then
|
|
expect(result.upcomingRaces).toHaveLength(1);
|
|
expect(result.upcomingRaces[0].trackName).toBe('Track B');
|
|
});
|
|
|
|
it('should exclude races with missing trackName and past dates', async () => {
|
|
// Given
|
|
const query = { driverId: 'driver-123' };
|
|
|
|
// Mock driver exists
|
|
(mockDriverRepository.findDriverById as any).mockResolvedValue({
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
rating: 1500,
|
|
rank: 10,
|
|
starts: 50,
|
|
wins: 10,
|
|
podiums: 20,
|
|
leagues: 3,
|
|
} as DriverData);
|
|
|
|
// Mock races with various invalid states
|
|
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
|
|
{
|
|
id: 'race-1',
|
|
trackName: '', // Missing trackName
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-25T10:00:00.000Z'), // Future
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
trackName: 'Track A',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-23T10:00:00.000Z'), // Past
|
|
},
|
|
{
|
|
id: 'race-3',
|
|
trackName: 'Track B',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-26T10:00:00.000Z'), // Future
|
|
},
|
|
] as RaceData[]);
|
|
|
|
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
|
|
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
|
|
|
|
// When
|
|
const result = await useCase.execute(query);
|
|
|
|
// Then
|
|
expect(result.upcomingRaces).toHaveLength(1);
|
|
expect(result.upcomingRaces[0].trackName).toBe('Track B');
|
|
});
|
|
|
|
it('should include only valid races with trackName and future dates', async () => {
|
|
// Given
|
|
const query = { driverId: 'driver-123' };
|
|
|
|
// Mock driver exists
|
|
(mockDriverRepository.findDriverById as any).mockResolvedValue({
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
rating: 1500,
|
|
rank: 10,
|
|
starts: 50,
|
|
wins: 10,
|
|
podiums: 20,
|
|
leagues: 3,
|
|
} as DriverData);
|
|
|
|
// Mock races with valid data
|
|
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
|
|
{
|
|
id: 'race-1',
|
|
trackName: 'Track A',
|
|
carType: 'GT3',
|
|
scheduledDate: new Date('2026-01-26T10:00:00.000Z'),
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
trackName: 'Track B',
|
|
carType: 'GT4',
|
|
scheduledDate: new Date('2026-01-27T10:00:00.000Z'),
|
|
},
|
|
] as RaceData[]);
|
|
|
|
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
|
|
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
|
|
|
|
// When
|
|
const result = await useCase.execute(query);
|
|
|
|
// Then
|
|
expect(result.upcomingRaces).toHaveLength(2);
|
|
expect(result.upcomingRaces[0].trackName).toBe('Track A');
|
|
expect(result.upcomingRaces[1].trackName).toBe('Track B');
|
|
});
|
|
});
|
|
});
|