integration tests
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped

This commit is contained in:
2026-01-23 23:46:03 +01:00
parent 95276df5af
commit 9bb6b228f1
13 changed files with 4733 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
# Test Coverage Analysis & Gap Report
## 1. Executive Summary
We have compared the existing E2E and Integration tests against the core concepts defined in [`docs/concept/`](docs/concept) and the testing principles in [`docs/TESTING_LAYERS.md`](docs/TESTING_LAYERS.md).
While the functional coverage is high, there are critical gaps in **Integration Testing** specifically regarding external boundaries (iRacing API) and specific infrastructure-heavy business logic (Rating Engine).
## 2. Concept vs. Test Mapping
| Concept Area | E2E Coverage | Integration Coverage | Status |
|--------------|--------------|----------------------|--------|
| **League Management** | [`leagues/`](tests/e2e/leagues) | [`leagues/`](tests/integration/leagues) | ✅ Covered |
| **Season/Schedule** | [`leagues/league-schedule.spec.ts`](tests/e2e/leagues/league-schedule.spec.ts) | [`leagues/schedule/`](tests/integration/leagues/schedule) | ✅ Covered |
| **Results Import** | [`races/race-results.spec.ts`](tests/e2e/races/race-results.spec.ts) | [`races/results/`](tests/integration/races/results) | ⚠️ Missing iRacing API Integration |
| **Complaints/Penalties** | [`leagues/league-stewarding.spec.ts`](tests/e2e/leagues/league-stewarding.spec.ts) | [`races/stewarding/`](tests/integration/races/stewarding) | ✅ Covered |
| **Team Competition** | [`teams/`](tests/e2e/teams) | [`teams/`](tests/integration/teams) | ✅ Covered |
| **Driver Profile/Stats** | [`drivers/`](tests/e2e/drivers) | [`drivers/profile/`](tests/integration/drivers/profile) | ✅ Covered |
| **Rating System** | None | None | ❌ Missing |
| **Social/Messaging** | None | None | ❌ Missing |
## 3. Identified Gaps in Integration Tests
According to [`docs/TESTING_LAYERS.md`](docs/TESTING_LAYERS.md), integration tests should protect **environmental correctness** (DB, external APIs, Auth).
### 🚨 Critical Gaps (Infrastructure/Boundaries)
1. **iRacing API Integration**:
- *Concept*: [`docs/concept/ADMINS.md`](docs/concept/ADMINS.md:83) (Automatic Results Import).
- *Gap*: We have tests for *displaying* results, but no integration tests verifying the actual handshake and parsing logic with the iRacing API boundary.
2. **Rating Engine Persistence**:
- *Concept*: [`docs/concept/RATING.md`](docs/concept/RATING.md) (GridPilot Rating).
- *Gap*: The rating system involves complex calculations that must be persisted correctly. We lack integration tests for the `RatingService` interacting with the DB.
3. **Auth/Identity Provider**:
- *Concept*: [`docs/concept/CONCEPT.md`](docs/concept/CONCEPT.md:172) (Safety, Security & Trust).
- *Gap*: No integration tests for the Auth boundary (e.g., JWT validation, session persistence).
### 🛠 Functional Gaps (Business Logic Integration)
1. **Social/Messaging**:
- *Concept*: [`docs/concept/SOCIAL.md`](docs/concept/SOCIAL.md) (Messaging, Notifications).
- *Gap*: No integration tests for message persistence or notification delivery (queues).
2. **Constructors-Style Scoring**:
- *Concept*: [`docs/concept/RACING.md`](docs/concept/RACING.md:47) (Constructors-Style Points).
- *Gap*: While we have `StandingsCalculation.test.ts`, we need specific integration tests for complex multi-driver team scoring scenarios against the DB.
## 4. Proposed Action Plan
1. **Implement iRacing API Contract/Integration Tests**: Verify the parsing of iRacing result payloads.
2. **Add Rating Persistence Tests**: Ensure `GridPilot Rating` updates correctly in the DB after race results are processed.
3. **Add Social/Notification Integration**: Test the persistence of messages and the triggering of notifications.
4. **Auth Integration**: Verify the system-level Auth flow as per the "Trust" requirement.
---
*Uncle Bob's Note: Remember, the closer a test is to the code, the more of them you should have. But for the system to be robust, the boundaries must be ironclad.*

127
tests/e2e/rating/README.md Normal file
View File

@@ -0,0 +1,127 @@
# Rating BDD E2E Tests
This directory contains BDD (Behavior-Driven Development) E2E tests for the GridPilot Rating system.
## Overview
The GridPilot Rating system is a competition rating designed specifically for league racing. Unlike iRating (which is for matchmaking), GridPilot Rating measures:
- **Results Strength**: How well you finish relative to field strength
- **Consistency**: Stability of finishing positions over a season
- **Clean Driving**: Incidents per race, weighted by severity
- **Racecraft**: Positions gained/lost vs. incident involvement
- **Reliability**: Attendance, DNS/DNF record
- **Team Contribution**: Points earned for your team; lineup efficiency
## Test Files
### [`rating-profile.spec.ts`](rating-profile.spec.ts)
Tests the driver profile rating display, including:
- Current GridPilot Rating value
- Rating breakdown by component (results, consistency, clean driving, etc.)
- Rating trend over time (seasons)
- Rating comparison with peers
- Rating impact on team contribution
**Key Scenarios:**
- Driver sees their current GridPilot Rating on profile
- Driver sees rating breakdown by component
- Driver sees rating trend over multiple seasons
- Driver sees how rating compares to league peers
- Driver sees rating impact on team contribution
- Driver sees rating explanation/tooltip
- Driver sees rating update after race completion
### [`rating-calculation.spec.ts`](rating-calculation.spec.ts)
Tests the rating calculation logic and updates:
- Rating calculation after race completion
- Rating update based on finishing position
- Rating update based on field strength
- Rating update based on incidents
- Rating update based on consistency
- Rating update based on team contribution
- Rating update based on season performance
**Key Scenarios:**
- Rating increases after strong finish against strong field
- Rating decreases after poor finish or incidents
- Rating reflects consistency over multiple races
- Rating accounts for team contribution
- Rating updates immediately after results are processed
- Rating calculation is transparent and understandable
### [`rating-leaderboard.spec.ts`](rating-leaderboard.spec.ts)
Tests the rating-based leaderboards:
- Global driver rankings by GridPilot Rating
- League-specific driver rankings
- Team rankings based on driver ratings
- Rating-based filtering and sorting
- Rating-based search functionality
**Key Scenarios:**
- User sees drivers ranked by GridPilot Rating
- User can filter drivers by rating range
- User can search for drivers by rating
- User can sort drivers by different rating components
- User sees team rankings based on driver ratings
- User sees rating-based leaderboards with accurate data
## Test Structure
Each test file follows this pattern:
```typescript
import { test, expect } from '@playwright/test';
test.describe('GridPilot Rating System', () => {
test.beforeEach(async ({ page }) => {
// TODO: Implement authentication setup
});
test('Driver sees their GridPilot Rating on profile', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver views their rating
// Given I am a registered driver "John Doe"
// And I have completed several races
// And I am on my profile page
// Then I should see my GridPilot Rating
// And I should see the rating breakdown
});
});
```
## Test Philosophy
These tests follow the BDD E2E testing concept:
- **Focus on outcomes, not visual implementation**: Tests validate what the user sees and can verify, not how it's rendered
- **Use Gherkin syntax**: Tests are written in Given/When/Then format
- **Validate final user outcomes**: Tests serve as acceptance criteria for the rating functionality
- **Use Playwright**: Tests are implemented using Playwright for browser automation
## TODO Implementation
All tests are currently placeholders with TODO comments. The actual test implementation should:
1. Set up authentication (login as a test driver)
2. Navigate to the appropriate page
3. Verify the expected outcomes using Playwright assertions
4. Handle loading states, error states, and edge cases
5. Use test data that matches the expected behavior
## Test Data
Tests should use realistic test data that matches the expected behavior:
- Driver: "John Doe" or similar test driver with varying performance
- Races: Completed races with different results (wins, podiums, DNFs)
- Fields: Races with varying field strength (strong vs. weak fields)
- Incidents: Races with different incident counts
- Teams: Teams with multiple drivers contributing to team score
## Future Enhancements
- Add test data factories/fixtures for consistent test data
- Add helper functions for common actions (login, navigation, etc.)
- Add visual regression tests for rating display
- Add performance tests for rating calculation
- Add accessibility tests for rating pages
- Add cross-browser compatibility testing

View File

@@ -0,0 +1,129 @@
import { test, expect } from '@playwright/test';
test.describe('GridPilot Rating - Calculation Logic', () => {
test.beforeEach(async ({ page }) => {
// TODO: Implement authentication setup
// - Login as test driver
// - Ensure test data exists
});
test('Rating increases after strong finish against strong field', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver finishes well against strong competition
// Given I am a driver with baseline rating
// And I complete a race against strong field
// And I finish in top positions
// When I view my rating after race
// Then my rating should increase
// And I should see the increase amount
});
test('Rating decreases after poor finish or incidents', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver has poor race with incidents
// Given I am a driver with baseline rating
// And I complete a race with poor finish
// And I have multiple incidents
// When I view my rating after race
// Then my rating should decrease
// And I should see the decrease amount
});
test('Rating reflects consistency over multiple races', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver shows consistent performance
// Given I complete multiple races
// And I finish in similar positions each race
// When I view my rating
// Then my consistency score should be high
// And my rating should be stable
});
test('Rating accounts for team contribution', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver contributes to team success
// Given I am on a team
// And I score points for my team
// When I view my rating
// Then my team contribution score should reflect this
// And my overall rating should include team impact
});
test('Rating updates immediately after results are processed', async ({ page }) => {
// TODO: Implement test
// Scenario: Race results are processed
// Given I just completed a race
// And results are being processed
// When results are available
// Then my rating should update immediately
// And I should see the update in real-time
});
test('Rating calculation is transparent and understandable', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver wants to understand rating changes
// Given I view my rating details
// When I see a rating change
// Then I should see explanation of what caused it
// And I should see breakdown of calculation
// And I should see tips for improvement
});
test('Rating handles DNFs appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver has DNF
// Given I complete a race
// And I have a DNF (Did Not Finish)
// When I view my rating
// Then my rating should be affected
// And my reliability score should decrease
// And I should see explanation of DNF impact
});
test('Rating handles DNS appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver has DNS
// Given I have a DNS (Did Not Start)
// When I view my rating
// Then my rating should be affected
// And my reliability score should decrease
// And I should see explanation of DNS impact
});
test('Rating handles small field sizes appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver races in small field
// Given I complete a race with small field
// When I view my rating
// Then my rating should be normalized for field size
// And I should see explanation of field size impact
});
test('Rating handles large field sizes appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver races in large field
// Given I complete a race with large field
// When I view my rating
// Then my rating should be normalized for field size
// And I should see explanation of field size impact
});
test('Rating handles clean races appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver has clean race
// Given I complete a race with zero incidents
// When I view my rating
// Then my clean driving score should increase
// And my rating should benefit from clean driving
});
test('Rating handles penalty scenarios appropriately', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver receives penalty
// Given I complete a race
// And I receive a penalty
// When I view my rating
// Then my rating should be affected by penalty
// And I should see explanation of penalty impact
});
});

View File

@@ -0,0 +1,123 @@
import { test, expect } from '@playwright/test';
test.describe('GridPilot Rating - Leaderboards', () => {
test.beforeEach(async ({ page }) => {
// TODO: Implement authentication setup
// - Login as test user
// - Ensure test data exists
});
test('User sees drivers ranked by GridPilot Rating', async ({ page }) => {
// TODO: Implement test
// Scenario: User views rating-based leaderboard
// Given I am on the leaderboards page
// When I view the driver rankings
// Then I should see drivers sorted by GridPilot Rating
// And I should see rating values for each driver
// And I should see ranking numbers
});
test('User can filter drivers by rating range', async ({ page }) => {
// TODO: Implement test
// Scenario: User filters leaderboard by rating
// Given I am on the driver leaderboards page
// When I set a rating range filter
// Then I should see only drivers within that range
// And I should see filter summary
});
test('User can search for drivers by rating', async ({ page }) => {
// TODO: Implement test
// Scenario: User searches for specific rating
// Given I am on the driver leaderboards page
// When I search for drivers with specific rating
// Then I should see matching drivers
// And I should see search results count
});
test('User can sort drivers by different rating components', async ({ page }) => {
// TODO: Implement test
// Scenario: User sorts leaderboard by rating component
// Given I am on the driver leaderboards page
// When I sort by "Results Strength"
// Then drivers should be sorted by results strength
// When I sort by "Clean Driving"
// Then drivers should be sorted by clean driving score
// And I should see the sort indicator
});
test('User sees team rankings based on driver ratings', async ({ page }) => {
// TODO: Implement test
// Scenario: User views team leaderboards
// Given I am on the team leaderboards page
// When I view team rankings
// Then I should see teams ranked by combined driver ratings
// And I should see team rating breakdown
// And I should see driver contributions
});
test('User sees rating-based leaderboards with accurate data', async ({ page }) => {
// TODO: Implement test
// Scenario: User verifies leaderboard accuracy
// Given I am viewing a rating-based leaderboard
// When I check the data
// Then ratings should match driver profiles
// And rankings should be correct
// And calculations should be accurate
});
test('User sees empty state when no rating data exists', async ({ page }) => {
// TODO: Implement test
// Scenario: Leaderboard with no data
// Given there are no drivers with ratings
// When I view the leaderboards
// Then I should see empty state
// And I should see message about no data
});
test('User sees loading state while leaderboards load', async ({ page }) => {
// TODO: Implement test
// Scenario: Leaderboards load slowly
// Given I navigate to leaderboards
// When data is loading
// Then I should see loading skeleton
// And I should see loading indicators
});
test('User sees error state when leaderboards fail to load', async ({ page }) => {
// TODO: Implement test
// Scenario: Leaderboards fail to load
// Given I navigate to leaderboards
// When data fails to load
// Then I should see error message
// And I should see retry button
});
test('User can navigate from leaderboard to driver profile', async ({ page }) => {
// TODO: Implement test
// Scenario: User clicks on driver in leaderboard
// Given I am viewing a rating-based leaderboard
// When I click on a driver entry
// Then I should navigate to that driver's profile
// And I should see their detailed rating
});
test('User sees pagination for large leaderboards', async ({ page }) => {
// TODO: Implement test
// Scenario: Leaderboard has many drivers
// Given there are many drivers with ratings
// When I view the leaderboards
// Then I should see pagination controls
// And I can navigate through pages
// And I should see page count
});
test('User sees rating percentile information', async ({ page }) => {
// TODO: Implement test
// Scenario: User wants to know relative standing
// Given I am viewing a driver in leaderboard
// When I look at their rating
// Then I should see percentile (e.g., "Top 10%")
// And I should see how many drivers are above/below
});
});

View File

@@ -0,0 +1,115 @@
import { test, expect } from '@playwright/test';
test.describe('GridPilot Rating - Profile Display', () => {
test.beforeEach(async ({ page }) => {
// TODO: Implement authentication setup
// - Login as test driver
// - Ensure driver has rating data
});
test('Driver sees their GridPilot Rating on profile', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver views their rating on profile
// Given I am a registered driver "John Doe"
// And I have completed several races with varying results
// And I am on my profile page
// Then I should see my GridPilot Rating displayed
// And I should see the rating value (e.g., "1500")
// And I should see the rating label (e.g., "GridPilot Rating")
});
test('Driver sees rating breakdown by component', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver views detailed rating breakdown
// Given I am on my profile page
// When I view the rating details
// Then I should see breakdown by:
// - Results Strength
// - Consistency
// - Clean Driving
// - Racecraft
// - Reliability
// - Team Contribution
// And each component should have a score/value
});
test('Driver sees rating trend over multiple seasons', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver views rating history
// Given I have raced in multiple seasons
// When I view my rating history
// Then I should see rating trend over time
// And I should see rating changes per season
// And I should see rating peaks and valleys
});
test('Driver sees rating comparison with league peers', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver compares rating with peers
// Given I am in a league with other drivers
// When I view my rating
// Then I should see how my rating compares to league average
// And I should see my percentile in the league
// And I should see my rank in the league
});
test('Driver sees rating impact on team contribution', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver sees how rating affects team
// Given I am on a team
// When I view my rating
// Then I should see my contribution to team score
// And I should see my percentage of team total
// And I should see how my rating affects team ranking
});
test('Driver sees rating explanation/tooltip', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver seeks explanation of rating
// Given I am viewing my rating
// When I hover over rating components
// Then I should see explanation of what each component means
// And I should see how each component is calculated
// And I should see tips for improving each component
});
test('Driver sees rating update after race completion', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver sees rating update after race
// Given I just completed a race
// When I view my profile
// Then I should see my rating has updated
// And I should see the change (e.g., "+15")
// And I should see what caused the change
});
test('Driver sees empty state when no rating data exists', async ({ page }) => {
// TODO: Implement test
// Scenario: New driver views profile
// Given I am a new driver with no races
// When I view my profile
// Then I should see empty state for rating
// And I should see message about rating calculation
// And I should see call to action to complete races
});
test('Driver sees loading state while rating loads', async ({ page }) => {
// TODO: Implement test
// Scenario: Driver views profile with slow connection
// Given I am on my profile page
// When rating data is loading
// Then I should see loading skeleton
// And I should see loading indicator
// And I should see placeholder values
});
test('Driver sees error state when rating fails to load', async ({ page }) => {
// TODO: Implement test
// Scenario: Rating data fails to load
// Given I am on my profile page
// When rating data fails to load
// Then I should see error message
// And I should see retry button
// And I should see fallback UI
});
});

View File

@@ -0,0 +1,239 @@
# Rating Integration Tests
This directory contains integration tests for the GridPilot Rating system, following the clean integration strategy defined in [`plans/clean_integration_strategy.md`](../../plans/clean_integration_strategy.md).
## Testing Philosophy
These tests focus on **Use Case orchestration** - verifying that Use Cases correctly interact with their Ports (Repositories, Event Publishers, etc.) using In-Memory adapters for fast, deterministic testing.
### Key Principles
1. **Business Logic Only**: Tests verify business logic orchestration, NOT UI rendering
2. **In-Memory Adapters**: Use In-Memory adapters for speed and determinism
3. **Zero Implementation**: These are placeholders - no actual test logic implemented
4. **Use Case Focus**: Tests verify Use Case interactions with Ports
5. **Orchestration Patterns**: Tests follow Given/When/Then patterns for business logic
## Test Files
### Core Rating Functionality
- **[`rating-calculation-use-cases.integration.test.ts`](./rating-calculation-use-cases.integration.test.ts)**
- Tests for rating calculation use cases
- Covers: `CalculateRatingUseCase`, `UpdateRatingUseCase`, `GetRatingUseCase`, etc.
- Focus: Verifies rating calculation logic with In-Memory adapters
- **[`rating-persistence-use-cases.integration.test.ts`](./rating-persistence-use-cases.integration.test.ts)**
- Tests for rating persistence use cases
- Covers: `SaveRatingUseCase`, `GetRatingHistoryUseCase`, `GetRatingTrendUseCase`, etc.
- Focus: Verifies rating data persistence and retrieval
- **[`rating-leaderboard-use-cases.integration.test.ts`](./rating-leaderboard-use-cases.integration.test.ts)**
- Tests for rating-based leaderboard use cases
- Covers: `GetRatingLeaderboardUseCase`, `GetRatingPercentileUseCase`, `GetRatingComparisonUseCase`, etc.
- Focus: Verifies leaderboard orchestration with In-Memory adapters
### Advanced Rating Functionality
- **[`rating-team-contribution-use-cases.integration.test.ts`](./rating-team-contribution-use-cases.integration.test.ts)**
- Tests for team contribution rating use cases
- Covers: `CalculateTeamContributionUseCase`, `GetTeamRatingUseCase`, `GetTeamContributionBreakdownUseCase`, etc.
- Focus: Verifies team rating logic and contribution calculations
- **[`rating-consistency-use-cases.integration.test.ts`](./rating-consistency-use-cases.integration.test.ts)**
- Tests for consistency rating use cases
- Covers: `CalculateConsistencyUseCase`, `GetConsistencyScoreUseCase`, `GetConsistencyTrendUseCase`, etc.
- Focus: Verifies consistency calculation logic
- **[`rating-reliability-use-cases.integration.test.ts`](./rating-reliability-use-cases.integration.test.ts)**
- Tests for reliability rating use cases
- Covers: `CalculateReliabilityUseCase`, `GetReliabilityScoreUseCase`, `GetReliabilityTrendUseCase`, etc.
- Focus: Verifies reliability calculation logic (attendance, DNFs, DNSs)
## Test Structure
Each test file follows the same structure:
```typescript
describe('Use Case Orchestration', () => {
let repositories: InMemoryAdapters;
let useCase: UseCase;
let eventPublisher: InMemoryEventPublisher;
beforeAll(() => {
// Initialize In-Memory repositories and event publisher
});
beforeEach(() => {
// Clear all In-Memory repositories before each test
});
describe('UseCase - Success Path', () => {
it('should [expected outcome]', async () => {
// TODO: Implement test
// Scenario: [description]
// Given: [setup]
// When: [action]
// Then: [expected result]
// And: [event emission]
});
});
describe('UseCase - Edge Cases', () => {
it('should handle [edge case]', async () => {
// TODO: Implement test
// Scenario: [description]
// Given: [setup]
// When: [action]
// Then: [expected result]
// And: [event emission]
});
});
describe('UseCase - Error Handling', () => {
it('should handle [error case]', async () => {
// TODO: Implement test
// Scenario: [description]
// Given: [setup]
// When: [action]
// Then: [expected error]
// And: [event emission]
});
});
describe('UseCase - Data Orchestration', () => {
it('should correctly format [data type]', async () => {
// TODO: Implement test
// Scenario: [description]
// Given: [setup]
// When: [action]
// Then: [expected data format]
});
});
});
```
## Implementation Guidelines
### When Implementing Tests
1. **Initialize In-Memory Adapters**:
```typescript
repository = new InMemoryRatingRepository();
eventPublisher = new InMemoryEventPublisher();
useCase = new UseCase({ repository, eventPublisher });
```
2. **Clear Repositories Before Each Test**:
```typescript
beforeEach(() => {
repository.clear();
eventPublisher.clear();
});
```
3. **Test Orchestration**:
- Verify Use Case calls the correct repository methods
- Verify Use Case publishes correct events
- Verify Use Case returns correct data structure
- Verify Use Case handles errors appropriately
4. **Test Data Format**:
- Verify rating is calculated correctly
- Verify rating breakdown is accurate
- Verify rating updates are applied correctly
- Verify rating history is maintained
### Example Implementation
```typescript
it('should calculate rating after race completion', async () => {
// Given: A driver with baseline rating
const driver = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
await driverRepository.create(driver);
// Given: A completed race with results
const race = Race.create({
id: 'r1',
leagueId: 'l1',
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await raceRepository.create(race);
const result = Result.create({
id: 'res1',
raceId: 'r1',
driverId: 'd1',
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId: 'd1',
raceId: 'r1'
});
// Then: The rating should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe('d1');
expect(rating.rating).toBeGreaterThan(0);
expect(rating.components).toBeDefined();
expect(rating.components.resultsStrength).toBeGreaterThan(0);
expect(rating.components.consistency).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.racecraft).toBeGreaterThan(0);
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.teamContribution).toBeGreaterThan(0);
// And: EventPublisher should emit RatingCalculatedEvent
expect(eventPublisher.events).toContainEqual(
expect.objectContaining({ type: 'RatingCalculatedEvent' })
);
});
```
## Observations
Based on the concept documentation, the rating system is complex with many components:
1. **Rating Components**: Results Strength, Consistency, Clean Driving, Racecraft, Reliability, Team Contribution
2. **Calculation Logic**: Weighted scoring based on multiple factors
3. **Persistence**: Rating history and trend tracking
4. **Leaderboards**: Rating-based rankings and comparisons
5. **Team Integration**: Team contribution scoring
6. **Transparency**: Clear explanation of rating changes
Each test file contains comprehensive test scenarios covering:
- Success paths
- Edge cases (small fields, DNFs, DNSs, penalties)
- Error handling
- Data orchestration patterns
- Calculation accuracy
- Persistence verification
## Next Steps
1. **Implement Test Logic**: Replace TODO comments with actual test implementations
2. **Add In-Memory Adapters**: Create In-Memory adapters for all required repositories
3. **Create Use Cases**: Implement the Use Cases referenced in the tests
4. **Create Ports**: Implement the Ports (Repositories, Event Publishers, etc.)
5. **Run Tests**: Execute tests to verify Use Case orchestration
6. **Refine Tests**: Update tests based on actual implementation details
## Related Documentation
- [Clean Integration Strategy](../../plans/clean_integration_strategy.md)
- [Testing Layers](../../docs/TESTING_LAYERS.md)
- [BDD E2E Tests](../e2e/bdd/rating/)
- [Rating Concept](../../docs/concept/RATING.md)

View File

@@ -0,0 +1,42 @@
import { InMemoryDriverRepository } from '../../../../core/racing/infrastructure/repositories/InMemoryDriverRepository';
import { InMemoryRaceRepository } from '../../../../core/racing/infrastructure/repositories/InMemoryRaceRepository';
import { InMemoryLeagueRepository } from '../../../../core/racing/infrastructure/repositories/InMemoryLeagueRepository';
import { InMemoryResultRepository } from '../../../../core/racing/infrastructure/repositories/InMemoryResultRepository';
import { InMemoryRatingRepository } from '../../../../core/rating/infrastructure/repositories/InMemoryRatingRepository';
import { InMemoryEventPublisher } from '../../../../adapters/events/InMemoryEventPublisher';
export class RatingTestContext {
private static instance: RatingTestContext;
public readonly driverRepository: InMemoryDriverRepository;
public readonly raceRepository: InMemoryRaceRepository;
public readonly leagueRepository: InMemoryLeagueRepository;
public readonly resultRepository: InMemoryResultRepository;
public readonly ratingRepository: InMemoryRatingRepository;
public readonly eventPublisher: InMemoryEventPublisher;
private constructor() {
this.driverRepository = new InMemoryDriverRepository();
this.raceRepository = new InMemoryRaceRepository();
this.leagueRepository = new InMemoryLeagueRepository();
this.resultRepository = new InMemoryResultRepository();
this.ratingRepository = new InMemoryRatingRepository();
this.eventPublisher = new InMemoryEventPublisher();
}
public static create(): RatingTestContext {
if (!RatingTestContext.instance) {
RatingTestContext.instance = new RatingTestContext();
}
return RatingTestContext.instance;
}
public async clear(): Promise<void> {
await this.driverRepository.clear();
await this.raceRepository.clear();
await this.leagueRepository.clear();
await this.resultRepository.clear();
await this.ratingRepository.clear();
this.eventPublisher.clear();
}
}

View File

@@ -0,0 +1,951 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { CalculateRatingUseCase } from '../../../../core/rating/application/use-cases/CalculateRatingUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Race } from '../../../../core/racing/domain/entities/Race';
import { League } from '../../../../core/racing/domain/entities/League';
import { Result as RaceResult } from '../../../../core/racing/domain/entities/result/Result';
describe('CalculateRatingUseCase', () => {
let context: RatingTestContext;
let calculateRatingUseCase: CalculateRatingUseCase;
beforeAll(() => {
context = RatingTestContext.create();
calculateRatingUseCase = new CalculateRatingUseCase(
context.driverRepository,
context.raceRepository,
context.resultRepository,
context.ratingRepository,
context.eventPublisher
);
});
beforeEach(async () => {
await context.clear();
});
describe('UseCase - Success Path', () => {
it('should calculate rating after race completion with strong finish', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with strong field
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Strong race results (win against strong field)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.rating).toBeGreaterThan(0);
expect(rating.components).toBeDefined();
expect(rating.components.resultsStrength).toBeGreaterThan(0);
expect(rating.components.consistency).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.racecraft).toBeGreaterThan(0);
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.teamContribution).toBeGreaterThan(0);
// And: EventPublisher should emit RatingCalculatedEvent
expect(context.eventPublisher.events).toContainEqual(
expect.objectContaining({ type: 'RatingCalculatedEvent' })
);
});
it('should calculate rating after race completion with poor finish', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Poor race results (last place with incidents)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 20,
lapsCompleted: 18,
totalTime: 4200,
fastestLap: 120,
points: 0,
incidents: 5,
startPosition: 15
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.rating).toBeGreaterThan(0);
expect(rating.components).toBeDefined();
expect(rating.components.resultsStrength).toBeGreaterThan(0);
expect(rating.components.consistency).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.racecraft).toBeGreaterThan(0);
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.teamContribution).toBeGreaterThan(0);
// And: EventPublisher should emit RatingCalculatedEvent
expect(context.eventPublisher.events).toContainEqual(
expect.objectContaining({ type: 'RatingCalculatedEvent' })
);
});
it('should calculate rating with consistency over multiple races', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with consistent results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateRatingUseCase.execute() is called for the last race
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The rating should reflect consistency
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.components.consistency).toBeGreaterThan(0);
expect(rating.components.consistency).toBeGreaterThan(rating.components.resultsStrength);
});
});
describe('UseCase - Edge Cases', () => {
it('should handle DNF (Did Not Finish) appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with DNF
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: DNF result
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 0,
lapsCompleted: 10,
totalTime: 0,
fastestLap: 0,
points: 0,
incidents: 3,
startPosition: 10
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be calculated with DNF impact
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.reliability).toBeLessThan(100);
});
it('should handle DNS (Did Not Start) appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A race with DNS
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: DNS result (no participation)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 0,
lapsCompleted: 0,
totalTime: 0,
fastestLap: 0,
points: 0,
incidents: 0,
startPosition: 0
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be calculated with DNS impact
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.reliability).toBeLessThan(100);
});
it('should handle small field sizes appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with small field
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Win in small field (5 drivers)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be normalized for small field
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.rating).toBeGreaterThan(0);
});
it('should handle large field sizes appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with large field
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Win in large field (30 drivers)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be normalized for large field
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.rating).toBeGreaterThan(0);
});
it('should handle clean races appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with zero incidents
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Clean race (zero incidents)
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 0,
startPosition: 5
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should reflect clean driving
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeGreaterThan(50);
});
it('should handle penalty scenarios appropriately', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with penalty
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// Given: Race with penalty
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 2,
startPosition: 5
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating should be affected by penalty
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId.toString()).toBe(driverId);
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeLessThan(100);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// Given: A completed race
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The result should be an error
expect(ratingResult.isErr()).toBe(true);
});
it('should handle missing race', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A non-existent race
const raceId = 'r999';
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The result should be an error
expect(ratingResult.isErr()).toBe(true);
});
it('should handle missing race results', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with no results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The result should be an error
expect(ratingResult.isErr()).toBe(true);
});
});
describe('UseCase - Data Orchestration', () => {
it('should correctly format rating data structure', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The rating data should be correctly formatted
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.driverId).toBeDefined();
expect(rating.rating).toBeDefined();
expect(rating.components).toBeDefined();
expect(rating.components.resultsStrength).toBeDefined();
expect(rating.components.consistency).toBeDefined();
expect(rating.components.cleanDriving).toBeDefined();
expect(rating.components.racecraft).toBeDefined();
expect(rating.components.reliability).toBeDefined();
expect(rating.components.teamContribution).toBeDefined();
expect(rating.timestamp).toBeDefined();
});
it('should correctly calculate results strength component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with strong finish
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The results strength should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.resultsStrength).toBeGreaterThan(0);
expect(rating.components.resultsStrength).toBeGreaterThan(50);
});
it('should correctly calculate consistency component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with consistent results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateRatingUseCase.execute() is called for the last race
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The consistency should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.consistency).toBeGreaterThan(0);
expect(rating.components.consistency).toBeGreaterThan(50);
});
it('should correctly calculate clean driving component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with zero incidents
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 0,
startPosition: 5
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The clean driving should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.cleanDriving).toBeGreaterThan(0);
expect(rating.components.cleanDriving).toBeGreaterThan(50);
});
it('should correctly calculate racecraft component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with positions gained
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 3,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 15,
incidents: 1,
startPosition: 10
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The racecraft should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.racecraft).toBeGreaterThan(0);
expect(rating.components.racecraft).toBeGreaterThan(50);
});
it('should correctly calculate reliability component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with good attendance
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateRatingUseCase.execute() is called for the last race
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The reliability should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.reliability).toBeGreaterThan(0);
expect(rating.components.reliability).toBeGreaterThan(50);
});
it('should correctly calculate team contribution component', async () => {
// Given: A driver with baseline rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A completed race with points
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(result);
// When: CalculateRatingUseCase.execute() is called
const ratingResult = await calculateRatingUseCase.execute({
driverId,
raceId
});
// Then: The team contribution should be calculated
expect(ratingResult.isOk()).toBe(true);
const rating = ratingResult.unwrap();
expect(rating.components.teamContribution).toBeGreaterThan(0);
expect(rating.components.teamContribution).toBeGreaterThan(50);
});
});
});

View File

@@ -0,0 +1,615 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { CalculateConsistencyUseCase } from '../../../../core/rating/application/use-cases/CalculateConsistencyUseCase';
import { GetConsistencyScoreUseCase } from '../../../../core/rating/application/use-cases/GetConsistencyScoreUseCase';
import { GetConsistencyTrendUseCase } from '../../../../core/rating/application/use-cases/GetConsistencyTrendUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Race } from '../../../../core/racing/domain/entities/Race';
import { League } from '../../../../core/racing/domain/entities/League';
import { Result as RaceResult } from '../../../../core/racing/domain/entities/result/Result';
describe('Rating Consistency Use Cases', () => {
let context: RatingTestContext;
let calculateConsistencyUseCase: CalculateConsistencyUseCase;
let getConsistencyScoreUseCase: GetConsistencyScoreUseCase;
let getConsistencyTrendUseCase: GetConsistencyTrendUseCase;
beforeAll(() => {
context = RatingTestContext.create();
calculateConsistencyUseCase = new CalculateConsistencyUseCase(
context.driverRepository,
context.raceRepository,
context.resultRepository,
context.eventPublisher
);
getConsistencyScoreUseCase = new GetConsistencyScoreUseCase(
context.driverRepository,
context.resultRepository
);
getConsistencyTrendUseCase = new GetConsistencyTrendUseCase(
context.driverRepository,
context.resultRepository
);
});
beforeEach(async () => {
await context.clear();
});
describe('CalculateConsistencyUseCase', () => {
describe('UseCase - Success Path', () => {
it('should calculate consistency for driver with consistent results', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with consistent results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The consistency should be calculated
expect(result.isOk()).toBe(true);
const consistency = result.unwrap();
expect(consistency.driverId.toString()).toBe(driverId);
expect(consistency.consistencyScore).toBeGreaterThan(0);
expect(consistency.consistencyScore).toBeGreaterThan(50);
expect(consistency.raceCount).toBe(5);
expect(consistency.positionVariance).toBeGreaterThan(0);
expect(consistency.positionVariance).toBeLessThan(10);
});
it('should calculate consistency for driver with varying results', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with varying results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const positions = [1, 10, 3, 15, 5];
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: positions[i - 1],
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25 - (positions[i - 1] * 2),
incidents: 1,
startPosition: positions[i - 1]
});
await context.resultRepository.create(result);
}
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The consistency should be calculated
expect(result.isOk()).toBe(true);
const consistency = result.unwrap();
expect(consistency.driverId.toString()).toBe(driverId);
expect(consistency.consistencyScore).toBeGreaterThan(0);
expect(consistency.consistencyScore).toBeLessThan(50);
expect(consistency.raceCount).toBe(5);
expect(consistency.positionVariance).toBeGreaterThan(10);
});
it('should calculate consistency with minimum race count', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Minimum races for consistency calculation
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 3; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r3'
});
// Then: The consistency should be calculated
expect(result.isOk()).toBe(true);
const consistency = result.unwrap();
expect(consistency.driverId.toString()).toBe(driverId);
expect(consistency.consistencyScore).toBeGreaterThan(0);
expect(consistency.raceCount).toBe(3);
});
});
describe('UseCase - Edge Cases', () => {
it('should handle driver with insufficient races', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Only one race
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no races', async () => {
// Given: A driver with no races
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle missing race', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: CalculateConsistencyUseCase.execute() is called
const result = await calculateConsistencyUseCase.execute({
driverId,
raceId: 'r999'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetConsistencyScoreUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve consistency score for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetConsistencyScoreUseCase.execute() is called
const result = await getConsistencyScoreUseCase.execute({ driverId });
// Then: The consistency score should be retrieved
expect(result.isOk()).toBe(true);
const consistency = result.unwrap();
expect(consistency.driverId.toString()).toBe(driverId);
expect(consistency.consistencyScore).toBeGreaterThan(0);
expect(consistency.raceCount).toBe(5);
expect(consistency.positionVariance).toBeGreaterThan(0);
});
it('should retrieve consistency score with race limit', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 10; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetConsistencyScoreUseCase.execute() is called with limit
const result = await getConsistencyScoreUseCase.execute({ driverId, limit: 5 });
// Then: The consistency score should be retrieved with limit
expect(result.isOk()).toBe(true);
const consistency = result.unwrap();
expect(consistency.driverId.toString()).toBe(driverId);
expect(consistency.raceCount).toBe(5);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetConsistencyScoreUseCase.execute() is called
const result = await getConsistencyScoreUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with insufficient races', async () => {
// Given: A driver with only one race
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: GetConsistencyScoreUseCase.execute() is called
const result = await getConsistencyScoreUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetConsistencyTrendUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve consistency trend for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetConsistencyTrendUseCase.execute() is called
const result = await getConsistencyTrendUseCase.execute({ driverId });
// Then: The consistency trend should be retrieved
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend).toBeDefined();
expect(trend.trend.length).toBeGreaterThan(0);
expect(trend.averageConsistency).toBeGreaterThan(0);
expect(trend.improvement).toBeDefined();
});
it('should retrieve consistency trend over specific period', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 10; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetConsistencyTrendUseCase.execute() is called with period
const result = await getConsistencyTrendUseCase.execute({ driverId, period: 7 });
// Then: The consistency trend should be retrieved for the period
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend.length).toBeLessThanOrEqual(7);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetConsistencyTrendUseCase.execute() is called
const result = await getConsistencyTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with insufficient races', async () => {
// Given: A driver with only one race
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: GetConsistencyTrendUseCase.execute() is called
const result = await getConsistencyTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,487 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { GetRatingLeaderboardUseCase } from '../../../../core/rating/application/use-cases/GetRatingLeaderboardUseCase';
import { GetRatingPercentileUseCase } from '../../../../core/rating/application/use-cases/GetRatingPercentileUseCase';
import { GetRatingComparisonUseCase } from '../../../../core/rating/application/use-cases/GetRatingComparisonUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Rating } from '../../../../core/rating/domain/entities/Rating';
describe('Rating Leaderboard Use Cases', () => {
let context: RatingTestContext;
let getRatingLeaderboardUseCase: GetRatingLeaderboardUseCase;
let getRatingPercentileUseCase: GetRatingPercentileUseCase;
let getRatingComparisonUseCase: GetRatingComparisonUseCase;
beforeAll(() => {
context = RatingTestContext.create();
getRatingLeaderboardUseCase = new GetRatingLeaderboardUseCase(
context.driverRepository,
context.ratingRepository
);
getRatingPercentileUseCase = new GetRatingPercentileUseCase(
context.driverRepository,
context.ratingRepository
);
getRatingComparisonUseCase = new GetRatingComparisonUseCase(
context.driverRepository,
context.ratingRepository
);
});
beforeEach(async () => {
await context.clear();
});
describe('GetRatingLeaderboardUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve driver leaderboard sorted by rating', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1600,
components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd3',
rating: 1400,
components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetRatingLeaderboardUseCase.execute() is called
const result = await getRatingLeaderboardUseCase.execute({});
// Then: The leaderboard should be retrieved sorted by rating
expect(result.isOk()).toBe(true);
const leaderboard = result.unwrap();
expect(leaderboard).toHaveLength(3);
expect(leaderboard[0].driverId.toString()).toBe('d2'); // Highest rating
expect(leaderboard[0].rating).toBe(1600);
expect(leaderboard[1].driverId.toString()).toBe('d1');
expect(leaderboard[2].driverId.toString()).toBe('d3');
});
it('should retrieve leaderboard with limit', async () => {
// Given: Multiple drivers with different ratings
const drivers = [];
for (let i = 1; i <= 10; i++) {
const driver = Driver.create({ id: `d${i}`, iracingId: `${100 + i}`, name: `Driver ${i}`, country: 'US' });
drivers.push(driver);
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
for (let i = 1; i <= 10; i++) {
const rating = Rating.create({
driverId: `d${i}`,
rating: 1400 + (i * 20),
components: { resultsStrength: 70 + i, consistency: 65 + i, cleanDriving: 80 + i, racecraft: 75 + i, reliability: 85 + i, teamContribution: 60 + i },
timestamp: new Date()
});
await context.ratingRepository.save(rating);
}
// When: GetRatingLeaderboardUseCase.execute() is called with limit
const result = await getRatingLeaderboardUseCase.execute({ limit: 5 });
// Then: The leaderboard should be retrieved with limit
expect(result.isOk()).toBe(true);
const leaderboard = result.unwrap();
expect(leaderboard).toHaveLength(5);
expect(leaderboard[0].rating).toBe(1600); // d10
expect(leaderboard[4].rating).toBe(1520); // d6
});
it('should retrieve leaderboard with offset', async () => {
// Given: Multiple drivers with different ratings
const drivers = [];
for (let i = 1; i <= 5; i++) {
const driver = Driver.create({ id: `d${i}`, iracingId: `${100 + i}`, name: `Driver ${i}`, country: 'US' });
drivers.push(driver);
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
for (let i = 1; i <= 5; i++) {
const rating = Rating.create({
driverId: `d${i}`,
rating: 1400 + (i * 20),
components: { resultsStrength: 70 + i, consistency: 65 + i, cleanDriving: 80 + i, racecraft: 75 + i, reliability: 85 + i, teamContribution: 60 + i },
timestamp: new Date()
});
await context.ratingRepository.save(rating);
}
// When: GetRatingLeaderboardUseCase.execute() is called with offset
const result = await getRatingLeaderboardUseCase.execute({ offset: 2 });
// Then: The leaderboard should be retrieved with offset
expect(result.isOk()).toBe(true);
const leaderboard = result.unwrap();
expect(leaderboard).toHaveLength(3); // 5 total - 2 offset = 3
expect(leaderboard[0].driverId.toString()).toBe('d3'); // Third highest
});
});
describe('UseCase - Edge Cases', () => {
it('should handle empty leaderboard', async () => {
// Given: No drivers or ratings
// When: GetRatingLeaderboardUseCase.execute() is called
const result = await getRatingLeaderboardUseCase.execute({});
// Then: The leaderboard should be empty
expect(result.isOk()).toBe(true);
const leaderboard = result.unwrap();
expect(leaderboard).toHaveLength(0);
});
it('should handle drivers without ratings', async () => {
// Given: Drivers without ratings
const driver = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: GetRatingLeaderboardUseCase.execute() is called
const result = await getRatingLeaderboardUseCase.execute({});
// Then: The leaderboard should be empty
expect(result.isOk()).toBe(true);
const leaderboard = result.unwrap();
expect(leaderboard).toHaveLength(0);
});
});
describe('UseCase - Error Handling', () => {
it('should handle invalid limit', async () => {
// Given: Drivers with ratings
const driver = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetRatingLeaderboardUseCase.execute() is called with invalid limit
const result = await getRatingLeaderboardUseCase.execute({ limit: -1 });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetRatingPercentileUseCase', () => {
describe('UseCase - Success Path', () => {
it('should calculate percentile for driver', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' }),
Driver.create({ id: 'd4', iracingId: '103', name: 'Alice Brown', country: 'AU' }),
Driver.create({ id: 'd5', iracingId: '104', name: 'Charlie Wilson', country: 'DE' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({ driverId: 'd1', rating: 1500, components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 }, timestamp: new Date() }),
Rating.create({ driverId: 'd2', rating: 1600, components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 }, timestamp: new Date() }),
Rating.create({ driverId: 'd3', rating: 1400, components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 }, timestamp: new Date() }),
Rating.create({ driverId: 'd4', rating: 1550, components: { resultsStrength: 82, consistency: 77, cleanDriving: 91, racecraft: 86, reliability: 94, teamContribution: 72 }, timestamp: new Date() }),
Rating.create({ driverId: 'd5', rating: 1450, components: { resultsStrength: 78, consistency: 73, cleanDriving: 89, racecraft: 83, reliability: 92, teamContribution: 68 }, timestamp: new Date() })
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetRatingPercentileUseCase.execute() is called for driver d2 (highest rating)
const result = await getRatingPercentileUseCase.execute({ driverId: 'd2' });
// Then: The percentile should be calculated
expect(result.isOk()).toBe(true);
const percentile = result.unwrap();
expect(percentile.driverId.toString()).toBe('d2');
expect(percentile.percentile).toBeGreaterThan(0);
expect(percentile.percentile).toBeLessThanOrEqual(100);
expect(percentile.totalDrivers).toBe(5);
expect(percentile.driversAbove).toBe(0);
expect(percentile.driversBelow).toBe(4);
});
it('should calculate percentile for middle-ranked driver', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({ driverId: 'd1', rating: 1500, components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 }, timestamp: new Date() }),
Rating.create({ driverId: 'd2', rating: 1400, components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 }, timestamp: new Date() }),
Rating.create({ driverId: 'd3', rating: 1600, components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 }, timestamp: new Date() })
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetRatingPercentileUseCase.execute() is called for driver d1 (middle rating)
const result = await getRatingPercentileUseCase.execute({ driverId: 'd1' });
// Then: The percentile should be calculated
expect(result.isOk()).toBe(true);
const percentile = result.unwrap();
expect(percentile.driverId.toString()).toBe('d1');
expect(percentile.percentile).toBeGreaterThan(0);
expect(percentile.percentile).toBeLessThan(100);
expect(percentile.totalDrivers).toBe(3);
expect(percentile.driversAbove).toBe(1); // d3
expect(percentile.driversBelow).toBe(1); // d2
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetRatingPercentileUseCase.execute() is called
const result = await getRatingPercentileUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no rating', async () => {
// Given: A driver with no rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: GetRatingPercentileUseCase.execute() is called
const result = await getRatingPercentileUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle empty leaderboard', async () => {
// Given: No drivers or ratings
// When: GetRatingPercentileUseCase.execute() is called
const result = await getRatingPercentileUseCase.execute({ driverId: 'd1' });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetRatingComparisonUseCase', () => {
describe('UseCase - Success Path', () => {
it('should compare driver rating with another driver', async () => {
// Given: Two drivers with different ratings
const driver1 = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
const driver2 = Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' });
await context.driverRepository.create(driver1);
await context.driverRepository.create(driver2);
// Given: Ratings for each driver
const rating1 = Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
});
const rating2 = Rating.create({
driverId: 'd2',
rating: 1600,
components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 },
timestamp: new Date()
});
await context.ratingRepository.save(rating1);
await context.ratingRepository.save(rating2);
// When: GetRatingComparisonUseCase.execute() is called
const result = await getRatingComparisonUseCase.execute({
driverId: 'd1',
compareWithDriverId: 'd2'
});
// Then: The comparison should be retrieved
expect(result.isOk()).toBe(true);
const comparison = result.unwrap();
expect(comparison.driverId.toString()).toBe('d1');
expect(comparison.compareWithDriverId.toString()).toBe('d2');
expect(comparison.driverRating).toBe(1500);
expect(comparison.compareWithRating).toBe(1600);
expect(comparison.difference).toBe(-100);
expect(comparison.differencePercentage).toBeLessThan(0);
});
it('should compare driver rating with league average', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({ driverId: 'd1', rating: 1500, components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 }, timestamp: new Date() }),
Rating.create({ driverId: 'd2', rating: 1600, components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 }, timestamp: new Date() }),
Rating.create({ driverId: 'd3', rating: 1400, components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 }, timestamp: new Date() })
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetRatingComparisonUseCase.execute() is called with league comparison
const result = await getRatingComparisonUseCase.execute({
driverId: 'd1',
compareWithLeague: true
});
// Then: The comparison should be retrieved
expect(result.isOk()).toBe(true);
const comparison = result.unwrap();
expect(comparison.driverId.toString()).toBe('d1');
expect(comparison.driverRating).toBe(1500);
expect(comparison.leagueAverage).toBe(1500); // (1500 + 1600 + 1400) / 3
expect(comparison.difference).toBe(0);
expect(comparison.differencePercentage).toBe(0);
});
it('should compare driver rating with league median', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' }),
Driver.create({ id: 'd4', iracingId: '103', name: 'Alice Brown', country: 'AU' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({ driverId: 'd1', rating: 1500, components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 }, timestamp: new Date() }),
Rating.create({ driverId: 'd2', rating: 1600, components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 }, timestamp: new Date() }),
Rating.create({ driverId: 'd3', rating: 1400, components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 }, timestamp: new Date() }),
Rating.create({ driverId: 'd4', rating: 1550, components: { resultsStrength: 82, consistency: 77, cleanDriving: 91, racecraft: 86, reliability: 94, teamContribution: 72 }, timestamp: new Date() })
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetRatingComparisonUseCase.execute() is called with league median comparison
const result = await getRatingComparisonUseCase.execute({
driverId: 'd1',
compareWithLeague: true,
useMedian: true
});
// Then: The comparison should be retrieved
expect(result.isOk()).toBe(true);
const comparison = result.unwrap();
expect(comparison.driverId.toString()).toBe('d1');
expect(comparison.driverRating).toBe(1500);
expect(comparison.leagueMedian).toBe(1525); // Median of [1400, 1500, 1550, 1600]
expect(comparison.difference).toBe(-25);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
const compareWithDriverId = 'd1';
// When: GetRatingComparisonUseCase.execute() is called
const result = await getRatingComparisonUseCase.execute({
driverId,
compareWithDriverId
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no rating', async () => {
// Given: Drivers with no ratings
const driver1 = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
const driver2 = Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' });
await context.driverRepository.create(driver1);
await context.driverRepository.create(driver2);
// When: GetRatingComparisonUseCase.execute() is called
const result = await getRatingComparisonUseCase.execute({
driverId: 'd1',
compareWithDriverId: 'd2'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle empty league for comparison', async () => {
// Given: A driver with rating
const driver = Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetRatingComparisonUseCase.execute() is called with league comparison
const result = await getRatingComparisonUseCase.execute({
driverId: 'd1',
compareWithLeague: true
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,473 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { SaveRatingUseCase } from '../../../../core/rating/application/use-cases/SaveRatingUseCase';
import { GetRatingUseCase } from '../../../../core/rating/application/use-cases/GetRatingUseCase';
import { GetRatingHistoryUseCase } from '../../../../core/rating/application/use-cases/GetRatingHistoryUseCase';
import { GetRatingTrendUseCase } from '../../../../core/rating/application/use-cases/GetRatingTrendUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Rating } from '../../../../core/rating/domain/entities/Rating';
describe('Rating Persistence Use Cases', () => {
let context: RatingTestContext;
let saveRatingUseCase: SaveRatingUseCase;
let getRatingUseCase: GetRatingUseCase;
let getRatingHistoryUseCase: GetRatingHistoryUseCase;
let getRatingTrendUseCase: GetRatingTrendUseCase;
beforeAll(() => {
context = RatingTestContext.create();
saveRatingUseCase = new SaveRatingUseCase(
context.ratingRepository,
context.eventPublisher
);
getRatingUseCase = new GetRatingUseCase(
context.ratingRepository
);
getRatingHistoryUseCase = new GetRatingHistoryUseCase(
context.ratingRepository
);
getRatingTrendUseCase = new GetRatingTrendUseCase(
context.ratingRepository
);
});
beforeEach(async () => {
await context.clear();
});
describe('SaveRatingUseCase', () => {
describe('UseCase - Success Path', () => {
it('should save rating successfully', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A rating to save
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
// When: SaveRatingUseCase.execute() is called
const result = await saveRatingUseCase.execute({ rating });
// Then: The rating should be saved
expect(result.isOk()).toBe(true);
// And: EventPublisher should emit RatingSavedEvent
expect(context.eventPublisher.events).toContainEqual(
expect.objectContaining({ type: 'RatingSavedEvent' })
);
});
it('should update existing rating', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: An initial rating
const initialRating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date(Date.now() - 86400000)
});
await context.ratingRepository.save(initialRating);
// Given: An updated rating
const updatedRating = Rating.create({
driverId,
rating: 1550,
components: {
resultsStrength: 85,
consistency: 80,
cleanDriving: 92,
racecraft: 88,
reliability: 96,
teamContribution: 75
},
timestamp: new Date()
});
// When: SaveRatingUseCase.execute() is called
const result = await saveRatingUseCase.execute({ rating: updatedRating });
// Then: The rating should be updated
expect(result.isOk()).toBe(true);
// And: EventPublisher should emit RatingUpdatedEvent
expect(context.eventPublisher.events).toContainEqual(
expect.objectContaining({ type: 'RatingUpdatedEvent' })
);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A rating with non-existent driver
const rating = Rating.create({
driverId: 'd999',
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
// When: SaveRatingUseCase.execute() is called
const result = await saveRatingUseCase.execute({ rating });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetRatingUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve rating for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: A saved rating
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetRatingUseCase.execute() is called
const result = await getRatingUseCase.execute({ driverId });
// Then: The rating should be retrieved
expect(result.isOk()).toBe(true);
const retrievedRating = result.unwrap();
expect(retrievedRating.driverId.toString()).toBe(driverId);
expect(retrievedRating.rating).toBe(1500);
expect(retrievedRating.components.resultsStrength).toBe(80);
});
it('should return latest rating for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple ratings for the same driver
const oldRating = Rating.create({
driverId,
rating: 1400,
components: {
resultsStrength: 70,
consistency: 65,
cleanDriving: 80,
racecraft: 75,
reliability: 85,
teamContribution: 60
},
timestamp: new Date(Date.now() - 86400000)
});
await context.ratingRepository.save(oldRating);
const newRating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(newRating);
// When: GetRatingUseCase.execute() is called
const result = await getRatingUseCase.execute({ driverId });
// Then: The latest rating should be retrieved
expect(result.isOk()).toBe(true);
const retrievedRating = result.unwrap();
expect(retrievedRating.driverId.toString()).toBe(driverId);
expect(retrievedRating.rating).toBe(1500);
expect(retrievedRating.timestamp.getTime()).toBe(newRating.timestamp.getTime());
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetRatingUseCase.execute() is called
const result = await getRatingUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no rating', async () => {
// Given: A driver with no rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: GetRatingUseCase.execute() is called
const result = await getRatingUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetRatingHistoryUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve rating history for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple ratings for the same driver
for (let i = 1; i <= 5; i++) {
const rating = Rating.create({
driverId,
rating: 1400 + (i * 20),
components: {
resultsStrength: 70 + (i * 2),
consistency: 65 + (i * 2),
cleanDriving: 80 + (i * 2),
racecraft: 75 + (i * 2),
reliability: 85 + (i * 2),
teamContribution: 60 + (i * 2)
},
timestamp: new Date(Date.now() - (i * 86400000))
});
await context.ratingRepository.save(rating);
}
// When: GetRatingHistoryUseCase.execute() is called
const result = await getRatingHistoryUseCase.execute({ driverId });
// Then: The rating history should be retrieved
expect(result.isOk()).toBe(true);
const history = result.unwrap();
expect(history).toHaveLength(5);
expect(history[0].rating).toBe(1500);
expect(history[4].rating).toBe(1420);
});
it('should retrieve rating history with limit', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple ratings for the same driver
for (let i = 1; i <= 10; i++) {
const rating = Rating.create({
driverId,
rating: 1400 + (i * 10),
components: {
resultsStrength: 70 + i,
consistency: 65 + i,
cleanDriving: 80 + i,
racecraft: 75 + i,
reliability: 85 + i,
teamContribution: 60 + i
},
timestamp: new Date(Date.now() - (i * 86400000))
});
await context.ratingRepository.save(rating);
}
// When: GetRatingHistoryUseCase.execute() is called with limit
const result = await getRatingHistoryUseCase.execute({ driverId, limit: 5 });
// Then: The rating history should be retrieved with limit
expect(result.isOk()).toBe(true);
const history = result.unwrap();
expect(history).toHaveLength(5);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetRatingHistoryUseCase.execute() is called
const result = await getRatingHistoryUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no rating history', async () => {
// Given: A driver with no rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: GetRatingHistoryUseCase.execute() is called
const result = await getRatingHistoryUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetRatingTrendUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve rating trend for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple ratings for the same driver
for (let i = 1; i <= 5; i++) {
const rating = Rating.create({
driverId,
rating: 1400 + (i * 20),
components: {
resultsStrength: 70 + (i * 2),
consistency: 65 + (i * 2),
cleanDriving: 80 + (i * 2),
racecraft: 75 + (i * 2),
reliability: 85 + (i * 2),
teamContribution: 60 + (i * 2)
},
timestamp: new Date(Date.now() - (i * 86400000))
});
await context.ratingRepository.save(rating);
}
// When: GetRatingTrendUseCase.execute() is called
const result = await getRatingTrendUseCase.execute({ driverId });
// Then: The rating trend should be retrieved
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend).toBeDefined();
expect(trend.trend.length).toBeGreaterThan(0);
expect(trend.change).toBeGreaterThan(0);
expect(trend.changePercentage).toBeGreaterThan(0);
});
it('should calculate trend over specific period', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple ratings for the same driver
for (let i = 1; i <= 10; i++) {
const rating = Rating.create({
driverId,
rating: 1400 + (i * 10),
components: {
resultsStrength: 70 + i,
consistency: 65 + i,
cleanDriving: 80 + i,
racecraft: 75 + i,
reliability: 85 + i,
teamContribution: 60 + i
},
timestamp: new Date(Date.now() - (i * 86400000))
});
await context.ratingRepository.save(rating);
}
// When: GetRatingTrendUseCase.execute() is called with period
const result = await getRatingTrendUseCase.execute({ driverId, period: 7 });
// Then: The rating trend should be retrieved for the period
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend.length).toBeLessThanOrEqual(7);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetRatingTrendUseCase.execute() is called
const result = await getRatingTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with insufficient rating history', async () => {
// Given: A driver with only one rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetRatingTrendUseCase.execute() is called
const result = await getRatingTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,850 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { CalculateReliabilityUseCase } from '../../../../core/rating/application/use-cases/CalculateReliabilityUseCase';
import { GetReliabilityScoreUseCase } from '../../../../core/rating/application/use-cases/GetReliabilityScoreUseCase';
import { GetReliabilityTrendUseCase } from '../../../../core/rating/application/use-cases/GetReliabilityTrendUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Race } from '../../../../core/racing/domain/entities/Race';
import { League } from '../../../../core/racing/domain/entities/League';
import { Result as RaceResult } from '../../../../core/racing/domain/entities/result/Result';
describe('Rating Reliability Use Cases', () => {
let context: RatingTestContext;
let calculateReliabilityUseCase: CalculateReliabilityUseCase;
let getReliabilityScoreUseCase: GetReliabilityScoreUseCase;
let getReliabilityTrendUseCase: GetReliabilityTrendUseCase;
beforeAll(() => {
context = RatingTestContext.create();
calculateReliabilityUseCase = new CalculateReliabilityUseCase(
context.driverRepository,
context.raceRepository,
context.resultRepository,
context.eventPublisher
);
getReliabilityScoreUseCase = new GetReliabilityScoreUseCase(
context.driverRepository,
context.resultRepository
);
getReliabilityTrendUseCase = new GetReliabilityTrendUseCase(
context.driverRepository,
context.resultRepository
);
});
beforeEach(async () => {
await context.clear();
});
describe('CalculateReliabilityUseCase', () => {
describe('UseCase - Success Path', () => {
it('should calculate reliability for driver with perfect attendance', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races with perfect attendance
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The reliability should be calculated
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.reliabilityScore).toBeGreaterThan(90);
expect(reliability.raceCount).toBe(5);
expect(reliability.dnfCount).toBe(0);
expect(reliability.dnsCount).toBe(0);
expect(reliability.attendanceRate).toBe(100);
});
it('should calculate reliability with DNFs', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple races with some DNFs
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// First 2 races are DNFs
const isDNF = i <= 2;
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: isDNF ? 0 : 5,
lapsCompleted: isDNF ? 10 : 20,
totalTime: isDNF ? 0 : 3600,
fastestLap: isDNF ? 0 : 105,
points: isDNF ? 0 : 10,
incidents: isDNF ? 3 : 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The reliability should be calculated with DNF impact
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.reliabilityScore).toBeLessThan(90);
expect(reliability.raceCount).toBe(5);
expect(reliability.dnfCount).toBe(2);
expect(reliability.dnsCount).toBe(0);
expect(reliability.attendanceRate).toBe(100);
});
it('should calculate reliability with DNSs', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple races with some DNSs
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
// First 2 races are DNSs
const isDNS = i <= 2;
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: isDNS ? 0 : 5,
lapsCompleted: isDNS ? 0 : 20,
totalTime: isDNS ? 0 : 3600,
fastestLap: isDNS ? 0 : 105,
points: isDNS ? 0 : 10,
incidents: 0,
startPosition: isDNS ? 0 : 5
});
await context.resultRepository.create(result);
}
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The reliability should be calculated with DNS impact
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.reliabilityScore).toBeLessThan(90);
expect(reliability.raceCount).toBe(5);
expect(reliability.dnfCount).toBe(0);
expect(reliability.dnsCount).toBe(2);
expect(reliability.attendanceRate).toBe(60);
});
it('should calculate reliability with mixed DNFs and DNSs', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple races with mixed issues
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
let position, lapsCompleted, totalTime, fastestLap, points, incidents, startPosition;
if (i === 1) {
// DNS
position = 0;
lapsCompleted = 0;
totalTime = 0;
fastestLap = 0;
points = 0;
incidents = 0;
startPosition = 0;
} else if (i === 2) {
// DNF
position = 0;
lapsCompleted = 10;
totalTime = 0;
fastestLap = 0;
points = 0;
incidents = 3;
startPosition = 5;
} else {
// Completed
position = 5;
lapsCompleted = 20;
totalTime = 3600;
fastestLap = 105;
points = 10;
incidents = 1;
startPosition = 5;
}
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position,
lapsCompleted,
totalTime,
fastestLap,
points,
incidents,
startPosition
});
await context.resultRepository.create(result);
}
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r5'
});
// Then: The reliability should be calculated with mixed issues
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.reliabilityScore).toBeLessThan(80);
expect(reliability.raceCount).toBe(5);
expect(reliability.dnfCount).toBe(1);
expect(reliability.dnsCount).toBe(1);
expect(reliability.attendanceRate).toBe(60);
});
it('should calculate reliability with minimum race count', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Minimum races for reliability calculation
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 3; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r3'
});
// Then: The reliability should be calculated
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.raceCount).toBe(3);
});
});
describe('UseCase - Edge Cases', () => {
it('should handle driver with insufficient races', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Only one race
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no races', async () => {
// Given: A driver with no races
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle missing race', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: CalculateReliabilityUseCase.execute() is called
const result = await calculateReliabilityUseCase.execute({
driverId,
raceId: 'r999'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetReliabilityScoreUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve reliability score for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetReliabilityScoreUseCase.execute() is called
const result = await getReliabilityScoreUseCase.execute({ driverId });
// Then: The reliability score should be retrieved
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.reliabilityScore).toBeGreaterThan(0);
expect(reliability.raceCount).toBe(5);
expect(reliability.dnfCount).toBe(0);
expect(reliability.dnsCount).toBe(0);
expect(reliability.attendanceRate).toBe(100);
});
it('should retrieve reliability score with race limit', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 10; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetReliabilityScoreUseCase.execute() is called with limit
const result = await getReliabilityScoreUseCase.execute({ driverId, limit: 5 });
// Then: The reliability score should be retrieved with limit
expect(result.isOk()).toBe(true);
const reliability = result.unwrap();
expect(reliability.driverId.toString()).toBe(driverId);
expect(reliability.raceCount).toBe(5);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetReliabilityScoreUseCase.execute() is called
const result = await getReliabilityScoreUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with insufficient races', async () => {
// Given: A driver with only one race
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: GetReliabilityScoreUseCase.execute() is called
const result = await getReliabilityScoreUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetReliabilityTrendUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve reliability trend for driver', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetReliabilityTrendUseCase.execute() is called
const result = await getReliabilityTrendUseCase.execute({ driverId });
// Then: The reliability trend should be retrieved
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend).toBeDefined();
expect(trend.trend.length).toBeGreaterThan(0);
expect(trend.averageReliability).toBeGreaterThan(0);
expect(trend.attendanceRate).toBe(100);
});
it('should retrieve reliability trend over specific period', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple completed races
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 10; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
}
// When: GetReliabilityTrendUseCase.execute() is called with period
const result = await getReliabilityTrendUseCase.execute({ driverId, period: 7 });
// Then: The reliability trend should be retrieved for the period
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend.length).toBeLessThanOrEqual(7);
});
it('should retrieve reliability trend with DNFs and DNSs', async () => {
// Given: A driver
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Given: Multiple races with mixed results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
for (let i = 1; i <= 5; i++) {
const raceId = `r${i}`;
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - (i * 86400000)),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
let position, lapsCompleted, totalTime, fastestLap, points, incidents, startPosition;
if (i === 1) {
// DNS
position = 0;
lapsCompleted = 0;
totalTime = 0;
fastestLap = 0;
points = 0;
incidents = 0;
startPosition = 0;
} else if (i === 2) {
// DNF
position = 0;
lapsCompleted = 10;
totalTime = 0;
fastestLap = 0;
points = 0;
incidents = 3;
startPosition = 5;
} else {
// Completed
position = 5;
lapsCompleted = 20;
totalTime = 3600;
fastestLap = 105;
points = 10;
incidents = 1;
startPosition = 5;
}
const result = RaceResult.create({
id: `res${i}`,
raceId,
driverId,
position,
lapsCompleted,
totalTime,
fastestLap,
points,
incidents,
startPosition
});
await context.resultRepository.create(result);
}
// When: GetReliabilityTrendUseCase.execute() is called
const result = await getReliabilityTrendUseCase.execute({ driverId });
// Then: The reliability trend should be retrieved
expect(result.isOk()).toBe(true);
const trend = result.unwrap();
expect(trend.driverId.toString()).toBe(driverId);
expect(trend.trend).toBeDefined();
expect(trend.trend.length).toBeGreaterThan(0);
expect(trend.averageReliability).toBeGreaterThan(0);
expect(trend.attendanceRate).toBe(60);
expect(trend.dnfCount).toBe(1);
expect(trend.dnsCount).toBe(1);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: GetReliabilityTrendUseCase.execute() is called
const result = await getReliabilityTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with insufficient races', async () => {
// Given: A driver with only one race
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const result = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 5,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 10,
incidents: 1,
startPosition: 5
});
await context.resultRepository.create(result);
// When: GetReliabilityTrendUseCase.execute() is called
const result = await getReliabilityTrendUseCase.execute({ driverId });
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,530 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RatingTestContext } from './RatingTestContext';
import { CalculateTeamContributionUseCase } from '../../../../core/rating/application/use-cases/CalculateTeamContributionUseCase';
import { GetTeamRatingUseCase } from '../../../../core/rating/application/use-cases/GetTeamRatingUseCase';
import { GetTeamContributionBreakdownUseCase } from '../../../../core/rating/application/use-cases/GetTeamContributionBreakdownUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Team } from '../../../../core/team/domain/entities/Team';
import { Rating } from '../../../../core/rating/domain/entities/Rating';
describe('Rating Team Contribution Use Cases', () => {
let context: RatingTestContext;
let calculateTeamContributionUseCase: CalculateTeamContributionUseCase;
let getTeamRatingUseCase: GetTeamRatingUseCase;
let getTeamContributionBreakdownUseCase: GetTeamContributionBreakdownUseCase;
beforeAll(() => {
context = RatingTestContext.create();
calculateTeamContributionUseCase = new CalculateTeamContributionUseCase(
context.driverRepository,
context.ratingRepository,
context.eventPublisher
);
getTeamRatingUseCase = new GetTeamRatingUseCase(
context.driverRepository,
context.ratingRepository
);
getTeamContributionBreakdownUseCase = new GetTeamContributionBreakdownUseCase(
context.driverRepository,
context.ratingRepository
);
});
beforeEach(async () => {
await context.clear();
});
describe('CalculateTeamContributionUseCase', () => {
describe('UseCase - Success Path', () => {
it('should calculate team contribution for single driver', async () => {
// Given: A driver with rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: CalculateTeamContributionUseCase.execute() is called
const result = await calculateTeamContributionUseCase.execute({
driverId,
teamId: 't1'
});
// Then: The team contribution should be calculated
expect(result.isOk()).toBe(true);
const contribution = result.unwrap();
expect(contribution.driverId.toString()).toBe(driverId);
expect(contribution.teamId.toString()).toBe('t1');
expect(contribution.contributionScore).toBeGreaterThan(0);
expect(contribution.contributionPercentage).toBeGreaterThan(0);
expect(contribution.contributionPercentage).toBeLessThanOrEqual(100);
});
it('should calculate team contribution for multiple drivers', async () => {
// Given: Multiple drivers with ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1600,
components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd3',
rating: 1400,
components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: CalculateTeamContributionUseCase.execute() is called for each driver
const contributions = [];
for (const driver of drivers) {
const result = await calculateTeamContributionUseCase.execute({
driverId: driver.id.toString(),
teamId: 't1'
});
expect(result.isOk()).toBe(true);
contributions.push(result.unwrap());
}
// Then: The team contributions should be calculated
expect(contributions).toHaveLength(3);
expect(contributions[0].contributionScore).toBeGreaterThan(0);
expect(contributions[1].contributionScore).toBeGreaterThan(0);
expect(contributions[2].contributionScore).toBeGreaterThan(0);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing driver', async () => {
// Given: A non-existent driver
const driverId = 'd999';
// When: CalculateTeamContributionUseCase.execute() is called
const result = await calculateTeamContributionUseCase.execute({
driverId,
teamId: 't1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle driver with no rating', async () => {
// Given: A driver with no rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// When: CalculateTeamContributionUseCase.execute() is called
const result = await calculateTeamContributionUseCase.execute({
driverId,
teamId: 't1'
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetTeamRatingUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve team rating from single driver', async () => {
// Given: A driver with rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetTeamRatingUseCase.execute() is called
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: [driverId]
});
// Then: The team rating should be retrieved
expect(result.isOk()).toBe(true);
const teamRating = result.unwrap();
expect(teamRating.teamId.toString()).toBe('t1');
expect(teamRating.teamRating).toBe(1500);
expect(teamRating.driverRatings).toHaveLength(1);
expect(teamRating.driverRatings[0].driverId.toString()).toBe(driverId);
expect(teamRating.driverRatings[0].rating).toBe(1500);
});
it('should retrieve team rating from multiple drivers', async () => {
// Given: Multiple drivers with ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1600,
components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd3',
rating: 1400,
components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetTeamRatingUseCase.execute() is called
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2', 'd3']
});
// Then: The team rating should be retrieved
expect(result.isOk()).toBe(true);
const teamRating = result.unwrap();
expect(teamRating.teamId.toString()).toBe('t1');
expect(teamRating.teamRating).toBeGreaterThan(0);
expect(teamRating.driverRatings).toHaveLength(3);
expect(teamRating.driverRatings[0].rating).toBe(1500);
expect(teamRating.driverRatings[1].rating).toBe(1600);
expect(teamRating.driverRatings[2].rating).toBe(1400);
});
it('should calculate team rating as average of driver ratings', async () => {
// Given: Multiple drivers with ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1700,
components: { resultsStrength: 90, consistency: 85, cleanDriving: 95, racecraft: 90, reliability: 98, teamContribution: 80 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetTeamRatingUseCase.execute() is called
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2']
});
// Then: The team rating should be the average
expect(result.isOk()).toBe(true);
const teamRating = result.unwrap();
expect(teamRating.teamRating).toBe(1600); // (1500 + 1700) / 2
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing drivers', async () => {
// Given: Non-existent drivers
// When: GetTeamRatingUseCase.execute() is called
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: ['d999', 'd998']
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle drivers with no ratings', async () => {
// Given: Drivers with no ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// When: GetTeamRatingUseCase.execute() is called
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2']
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle empty driver list', async () => {
// When: GetTeamRatingUseCase.execute() is called with empty list
const result = await getTeamRatingUseCase.execute({
teamId: 't1',
driverIds: []
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
describe('GetTeamContributionBreakdownUseCase', () => {
describe('UseCase - Success Path', () => {
it('should retrieve contribution breakdown for single driver', async () => {
// Given: A driver with rating
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const rating = Rating.create({
driverId,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 75,
cleanDriving: 90,
racecraft: 85,
reliability: 95,
teamContribution: 70
},
timestamp: new Date()
});
await context.ratingRepository.save(rating);
// When: GetTeamContributionBreakdownUseCase.execute() is called
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: [driverId]
});
// Then: The contribution breakdown should be retrieved
expect(result.isOk()).toBe(true);
const breakdown = result.unwrap();
expect(breakdown.teamId.toString()).toBe('t1');
expect(breakdown.breakdown).toHaveLength(1);
expect(breakdown.breakdown[0].driverId.toString()).toBe(driverId);
expect(breakdown.breakdown[0].rating).toBe(1500);
expect(breakdown.breakdown[0].contributionScore).toBeGreaterThan(0);
expect(breakdown.breakdown[0].contributionPercentage).toBeGreaterThan(0);
expect(breakdown.breakdown[0].contributionPercentage).toBeLessThanOrEqual(100);
});
it('should retrieve contribution breakdown for multiple drivers', async () => {
// Given: Multiple drivers with ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' }),
Driver.create({ id: 'd3', iracingId: '102', name: 'Bob Johnson', country: 'CA' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1600,
components: { resultsStrength: 85, consistency: 80, cleanDriving: 92, racecraft: 88, reliability: 96, teamContribution: 75 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd3',
rating: 1400,
components: { resultsStrength: 75, consistency: 70, cleanDriving: 88, racecraft: 82, reliability: 93, teamContribution: 65 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetTeamContributionBreakdownUseCase.execute() is called
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2', 'd3']
});
// Then: The contribution breakdown should be retrieved
expect(result.isOk()).toBe(true);
const breakdown = result.unwrap();
expect(breakdown.teamId.toString()).toBe('t1');
expect(breakdown.breakdown).toHaveLength(3);
expect(breakdown.breakdown[0].driverId.toString()).toBe('d1');
expect(breakdown.breakdown[1].driverId.toString()).toBe('d2');
expect(breakdown.breakdown[2].driverId.toString()).toBe('d3');
expect(breakdown.breakdown[0].contributionPercentage).toBeGreaterThan(0);
expect(breakdown.breakdown[1].contributionPercentage).toBeGreaterThan(0);
expect(breakdown.breakdown[2].contributionPercentage).toBeGreaterThan(0);
});
it('should calculate contribution percentages correctly', async () => {
// Given: Multiple drivers with different ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// Given: Ratings for each driver
const ratings = [
Rating.create({
driverId: 'd1',
rating: 1500,
components: { resultsStrength: 80, consistency: 75, cleanDriving: 90, racecraft: 85, reliability: 95, teamContribution: 70 },
timestamp: new Date()
}),
Rating.create({
driverId: 'd2',
rating: 1700,
components: { resultsStrength: 90, consistency: 85, cleanDriving: 95, racecraft: 90, reliability: 98, teamContribution: 80 },
timestamp: new Date()
})
];
for (const rating of ratings) {
await context.ratingRepository.save(rating);
}
// When: GetTeamContributionBreakdownUseCase.execute() is called
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2']
});
// Then: The contribution percentages should be calculated correctly
expect(result.isOk()).toBe(true);
const breakdown = result.unwrap();
expect(breakdown.breakdown).toHaveLength(2);
expect(breakdown.breakdown[0].contributionPercentage + breakdown.breakdown[1].contributionPercentage).toBe(100);
});
});
describe('UseCase - Error Handling', () => {
it('should handle missing drivers', async () => {
// Given: Non-existent drivers
// When: GetTeamContributionBreakdownUseCase.execute() is called
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: ['d999', 'd998']
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle drivers with no ratings', async () => {
// Given: Drivers with no ratings
const drivers = [
Driver.create({ id: 'd1', iracingId: '100', name: 'John Doe', country: 'US' }),
Driver.create({ id: 'd2', iracingId: '101', name: 'Jane Smith', country: 'UK' })
];
for (const driver of drivers) {
await context.driverRepository.create(driver);
}
// When: GetTeamContributionBreakdownUseCase.execute() is called
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: ['d1', 'd2']
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
it('should handle empty driver list', async () => {
// When: GetTeamContributionBreakdownUseCase.execute() is called with empty list
const result = await getTeamContributionBreakdownUseCase.execute({
teamId: 't1',
driverIds: []
});
// Then: The result should be an error
expect(result.isErr()).toBe(true);
});
});
});
});