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
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:
52
plans/test_gap_analysis.md
Normal file
52
plans/test_gap_analysis.md
Normal 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
127
tests/e2e/rating/README.md
Normal 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
|
||||
129
tests/e2e/rating/rating-calculation.spec.ts
Normal file
129
tests/e2e/rating/rating-calculation.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
123
tests/e2e/rating/rating-leaderboard.spec.ts
Normal file
123
tests/e2e/rating/rating-leaderboard.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
115
tests/e2e/rating/rating-profile.spec.ts
Normal file
115
tests/e2e/rating/rating-profile.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
239
tests/integration/rating/README.md
Normal file
239
tests/integration/rating/README.md
Normal 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)
|
||||
42
tests/integration/rating/RatingTestContext.ts
Normal file
42
tests/integration/rating/RatingTestContext.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user