integration test placeholders
This commit is contained in:
189
tests/integration/dashboard/README.md
Normal file
189
tests/integration/dashboard/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Dashboard Integration Tests
|
||||
|
||||
This directory contains integration tests for the dashboard functionality, following the Clean Integration Testing strategy defined in `plans/clean_integration_strategy.md`.
|
||||
|
||||
## Test Philosophy
|
||||
|
||||
These tests focus on **Use Case orchestration** and **business logic**, not UI rendering. They verify that:
|
||||
|
||||
1. **Use Cases correctly orchestrate** interactions between their Ports (Repositories, Event Publishers)
|
||||
2. **Data flows correctly** from repositories through use cases to presenters
|
||||
3. **Error handling works** at the business logic level
|
||||
4. **In-Memory adapters** are used for speed and determinism
|
||||
|
||||
## Test Files
|
||||
|
||||
### 1. [`dashboard-use-cases.integration.test.ts`](dashboard-use-cases.integration.test.ts)
|
||||
Tests the orchestration logic of dashboard-related Use Cases.
|
||||
|
||||
**Focus:** Use Case orchestration patterns
|
||||
- GetDashboardUseCase: Retrieves driver statistics, upcoming races, standings, and activity
|
||||
- Validates that Use Cases correctly interact with their Ports
|
||||
- Tests success paths and edge cases
|
||||
|
||||
**Scenarios:**
|
||||
- Driver with complete data
|
||||
- New driver with no history
|
||||
- Driver with many upcoming races (limited to 3)
|
||||
- Driver in multiple championships
|
||||
- Driver with recent activity sorted by timestamp
|
||||
- Edge cases: no upcoming races, no championships, no activity
|
||||
- Error cases: driver not found, invalid ID, repository errors
|
||||
|
||||
### 2. [`dashboard-data-flow.integration.test.ts`](dashboard-data-flow.integration.test.ts)
|
||||
Tests the complete data flow from repositories to DTOs.
|
||||
|
||||
**Focus:** Data transformation and flow
|
||||
- Repository → Use Case → Presenter → DTO
|
||||
- Data validation and transformation
|
||||
- DTO structure and formatting
|
||||
|
||||
**Scenarios:**
|
||||
- Complete data flow for driver with all data
|
||||
- Complete data flow for new driver with no data
|
||||
- Data consistency across multiple calls
|
||||
- Maximum upcoming races handling
|
||||
- Many championship standings
|
||||
- Many recent activities
|
||||
- Mixed race statuses
|
||||
- DTO structure validation
|
||||
|
||||
### 3. [`dashboard-error-handling.integration.test.ts`](dashboard-error-handling.integration.test.ts)
|
||||
Tests error handling and edge cases at the Use Case level.
|
||||
|
||||
**Focus:** Error orchestration and handling
|
||||
- Repository errors (driver not found, data access errors)
|
||||
- Validation errors (invalid driver ID, invalid parameters)
|
||||
- Business logic errors (permission denied, data inconsistencies)
|
||||
- Error recovery and fallbacks
|
||||
|
||||
**Scenarios:**
|
||||
- Driver not found errors
|
||||
- Validation errors (empty, null, undefined, malformed IDs)
|
||||
- Repository query errors (driver, race, league, activity)
|
||||
- Event publisher error handling
|
||||
- Business logic error handling (corrupted data, inconsistencies)
|
||||
- Error recovery and fallbacks
|
||||
- Error propagation
|
||||
- Error logging and observability
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tests/integration/dashboard/
|
||||
├── dashboard-use-cases.integration.test.ts # Use Case orchestration tests
|
||||
├── dashboard-data-flow.integration.test.ts # Data flow tests
|
||||
├── dashboard-error-handling.integration.test.ts # Error handling tests
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Test Pattern
|
||||
|
||||
All tests follow the Clean Integration Test pattern:
|
||||
|
||||
```typescript
|
||||
describe('Feature - Test Scenario', () => {
|
||||
let harness: IntegrationTestHarness;
|
||||
let inMemoryRepository: InMemoryRepository;
|
||||
let useCase: UseCase;
|
||||
|
||||
beforeAll(() => {
|
||||
// Initialize In-Memory adapters
|
||||
// inMemoryRepository = new InMemoryRepository();
|
||||
// useCase = new UseCase({ repository: inMemoryRepository });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear In-Memory repositories
|
||||
// inMemoryRepository.clear();
|
||||
});
|
||||
|
||||
it('should [expected behavior]', async () => {
|
||||
// TODO: Implement test
|
||||
// Given: Setup test data in In-Memory repositories
|
||||
// When: Execute the Use Case
|
||||
// Then: Verify orchestration (repository calls, event emissions)
|
||||
// And: Verify result structure and data
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
### 1. Use In-Memory Adapters
|
||||
- All tests use In-Memory repositories for speed and determinism
|
||||
- No external database or network dependencies
|
||||
- Tests run in milliseconds
|
||||
|
||||
### 2. Focus on Orchestration
|
||||
- Tests verify **what** the Use Case does, not **how** it does it
|
||||
- Verify repository calls, event emissions, and data flow
|
||||
- Don't test UI rendering or visual aspects
|
||||
|
||||
### 3. Zero Implementation
|
||||
- These are **placeholders** with TODO comments
|
||||
- No actual implementation logic
|
||||
- Just the test framework and structure
|
||||
|
||||
### 4. Business Logic Only
|
||||
- Tests are for business logic, not UI
|
||||
- Focus on Use Case orchestration
|
||||
- Verify data transformation and error handling
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all dashboard integration tests
|
||||
npm test -- tests/integration/dashboard/
|
||||
|
||||
# Run specific test file
|
||||
npm test -- tests/integration/dashboard/dashboard-use-cases.integration.test.ts
|
||||
|
||||
# Run with verbose output
|
||||
npm test -- tests/integration/dashboard/ --reporter=verbose
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- [`plans/clean_integration_strategy.md`](../../../plans/clean_integration_strategy.md) - Clean Integration Testing strategy
|
||||
- [`tests/e2e/bdd/dashboard/`](../../e2e/bdd/dashboard/) - BDD E2E tests (user outcomes)
|
||||
- [`tests/integration/harness/`](../harness/) - Integration test harness
|
||||
- [`tests/integration/league/`](../league/) - Example integration tests
|
||||
|
||||
## Observations
|
||||
|
||||
Based on the BDD E2E tests, the dashboard functionality requires integration test coverage for:
|
||||
|
||||
1. **Driver Statistics Calculation**
|
||||
- Rating, rank, starts, wins, podiums, leagues
|
||||
- Derived from race results and league participation
|
||||
|
||||
2. **Upcoming Race Management**
|
||||
- Retrieval of scheduled races
|
||||
- Limiting to 3 races
|
||||
- Sorting by scheduled date
|
||||
- Time-until-race calculation
|
||||
|
||||
3. **Championship Standings**
|
||||
- League participation tracking
|
||||
- Position and points calculation
|
||||
- Driver count per league
|
||||
|
||||
4. **Recent Activity Feed**
|
||||
- Activity type categorization (race_result, etc.)
|
||||
- Timestamp sorting (newest first)
|
||||
- Status assignment (success, info)
|
||||
|
||||
5. **Error Handling**
|
||||
- Driver not found scenarios
|
||||
- Invalid driver ID validation
|
||||
- Repository error propagation
|
||||
- Event publisher error handling
|
||||
|
||||
6. **Edge Cases**
|
||||
- New drivers with no data
|
||||
- Drivers with partial data
|
||||
- Maximum data limits (upcoming races)
|
||||
- Data inconsistencies
|
||||
|
||||
These integration tests will provide fast, deterministic verification of the dashboard business logic before UI implementation.
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Integration Test: Dashboard Data Flow
|
||||
*
|
||||
* Tests the complete data flow for dashboard functionality:
|
||||
* 1. Repository queries return correct data
|
||||
* 2. Use case processes and orchestrates data correctly
|
||||
* 3. Presenter transforms data to DTOs
|
||||
* 4. API returns correct response structure
|
||||
*
|
||||
* Focus: Data transformation and flow, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryActivityRepository } from '../../../adapters/activity/persistence/inmemory/InMemoryActivityRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetDashboardUseCase } from '../../../core/dashboard/use-cases/GetDashboardUseCase';
|
||||
import { DashboardPresenter } from '../../../core/dashboard/presenters/DashboardPresenter';
|
||||
import { DashboardDTO } from '../../../core/dashboard/dto/DashboardDTO';
|
||||
|
||||
describe('Dashboard Data Flow Integration', () => {
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let activityRepository: InMemoryActivityRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getDashboardUseCase: GetDashboardUseCase;
|
||||
let dashboardPresenter: DashboardPresenter;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories, event publisher, use case, and presenter
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// activityRepository = new InMemoryActivityRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getDashboardUseCase = new GetDashboardUseCase({
|
||||
// driverRepository,
|
||||
// raceRepository,
|
||||
// leagueRepository,
|
||||
// activityRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// dashboardPresenter = new DashboardPresenter();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// driverRepository.clear();
|
||||
// raceRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// activityRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
});
|
||||
|
||||
describe('Repository to Use Case Data Flow', () => {
|
||||
it('should correctly flow driver data from repository to use case', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver data flow
|
||||
// Given: A driver exists in the repository with specific statistics
|
||||
// And: The driver has rating 1500, rank 123, 10 starts, 3 wins, 5 podiums
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: The use case should retrieve driver data from repository
|
||||
// And: The use case should calculate derived statistics
|
||||
// And: The result should contain all driver statistics
|
||||
});
|
||||
|
||||
it('should correctly flow race data from repository to use case', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race data flow
|
||||
// Given: Multiple races exist in the repository
|
||||
// And: Some races are scheduled for the future
|
||||
// And: Some races are completed
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: The use case should retrieve upcoming races from repository
|
||||
// And: The use case should limit results to 3 races
|
||||
// And: The use case should sort races by scheduled date
|
||||
});
|
||||
|
||||
it('should correctly flow league data from repository to use case', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League data flow
|
||||
// Given: Multiple leagues exist in the repository
|
||||
// And: The driver is participating in some leagues
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: The use case should retrieve league memberships from repository
|
||||
// And: The use case should calculate standings for each league
|
||||
// And: The result should contain league name, position, points, and driver count
|
||||
});
|
||||
|
||||
it('should correctly flow activity data from repository to use case', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Activity data flow
|
||||
// Given: Multiple activities exist in the repository
|
||||
// And: Activities include race results and other events
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: The use case should retrieve recent activities from repository
|
||||
// And: The use case should sort activities by timestamp (newest first)
|
||||
// And: The result should contain activity type, description, and timestamp
|
||||
});
|
||||
});
|
||||
|
||||
describe('Use Case to Presenter Data Flow', () => {
|
||||
it('should correctly transform use case result to DTO', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Use case result transformation
|
||||
// Given: A driver exists with complete data
|
||||
// And: GetDashboardUseCase.execute() returns a DashboardResult
|
||||
// When: DashboardPresenter.present() is called with the result
|
||||
// Then: The presenter should transform the result to DashboardDTO
|
||||
// And: The DTO should have correct structure and types
|
||||
// And: All fields should be properly formatted
|
||||
});
|
||||
|
||||
it('should correctly handle empty data in DTO transformation', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty data transformation
|
||||
// Given: A driver exists with no data
|
||||
// And: GetDashboardUseCase.execute() returns a DashboardResult with empty sections
|
||||
// When: DashboardPresenter.present() is called
|
||||
// Then: The DTO should have empty arrays for sections
|
||||
// And: The DTO should have default values for statistics
|
||||
// And: The DTO structure should remain valid
|
||||
});
|
||||
|
||||
it('should correctly format dates and times in DTO', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Date formatting in DTO
|
||||
// Given: A driver exists with upcoming races
|
||||
// And: Races have scheduled dates in the future
|
||||
// When: DashboardPresenter.present() is called
|
||||
// Then: The DTO should have formatted date strings
|
||||
// And: The DTO should have time-until-race strings
|
||||
// And: The DTO should have activity timestamps
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete Data Flow: Repository -> Use Case -> Presenter', () => {
|
||||
it('should complete full data flow for driver with all data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Complete data flow
|
||||
// Given: A driver exists with complete data in repositories
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called with the result
|
||||
// Then: The final DTO should contain:
|
||||
// - Driver statistics (rating, rank, starts, wins, podiums, leagues)
|
||||
// - Upcoming races (up to 3, sorted by date)
|
||||
// - Championship standings (league name, position, points, driver count)
|
||||
// - Recent activity (type, description, timestamp, status)
|
||||
// And: All data should be correctly transformed and formatted
|
||||
});
|
||||
|
||||
it('should complete full data flow for new driver with no data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Complete data flow for new driver
|
||||
// Given: A newly registered driver exists with no data
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called with the result
|
||||
// Then: The final DTO should contain:
|
||||
// - Basic driver statistics (rating, rank, starts, wins, podiums, leagues)
|
||||
// - Empty upcoming races array
|
||||
// - Empty championship standings array
|
||||
// - Empty recent activity array
|
||||
// And: All fields should have appropriate default values
|
||||
});
|
||||
|
||||
it('should maintain data consistency across multiple data flows', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Data consistency
|
||||
// Given: A driver exists with data
|
||||
// When: GetDashboardUseCase.execute() is called multiple times
|
||||
// And: DashboardPresenter.present() is called for each result
|
||||
// Then: All DTOs should be identical
|
||||
// And: Data should remain consistent across calls
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Transformation Edge Cases', () => {
|
||||
it('should handle driver with maximum upcoming races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Maximum upcoming races
|
||||
// Given: A driver exists
|
||||
// And: The driver has 10 upcoming races scheduled
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should contain exactly 3 upcoming races
|
||||
// And: The races should be the 3 earliest scheduled races
|
||||
});
|
||||
|
||||
it('should handle driver with many championship standings', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Many championship standings
|
||||
// Given: A driver exists
|
||||
// And: The driver is participating in 5 championships
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should contain standings for all 5 championships
|
||||
// And: Each standing should have correct data
|
||||
});
|
||||
|
||||
it('should handle driver with many recent activities', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Many recent activities
|
||||
// Given: A driver exists
|
||||
// And: The driver has 20 recent activities
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should contain all 20 activities
|
||||
// And: Activities should be sorted by timestamp (newest first)
|
||||
});
|
||||
|
||||
it('should handle driver with mixed race statuses', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Mixed race statuses
|
||||
// Given: A driver exists
|
||||
// And: The driver has completed races, scheduled races, and cancelled races
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: Driver statistics should only count completed races
|
||||
// And: Upcoming races should only include scheduled races
|
||||
// And: Cancelled races should not appear in any section
|
||||
});
|
||||
});
|
||||
|
||||
describe('DTO Structure Validation', () => {
|
||||
it('should validate DTO structure for complete dashboard', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: DTO structure validation
|
||||
// Given: A driver exists with complete data
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should have all required properties
|
||||
// And: Each property should have correct type
|
||||
// And: Nested objects should have correct structure
|
||||
});
|
||||
|
||||
it('should validate DTO structure for empty dashboard', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty DTO structure validation
|
||||
// Given: A driver exists with no data
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should have all required properties
|
||||
// And: Array properties should be empty arrays
|
||||
// And: Object properties should have default values
|
||||
});
|
||||
|
||||
it('should validate DTO structure for partial data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Partial DTO structure validation
|
||||
// Given: A driver exists with some data but not all
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// And: DashboardPresenter.present() is called
|
||||
// Then: The DTO should have all required properties
|
||||
// And: Properties with data should have correct values
|
||||
// And: Properties without data should have appropriate defaults
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Integration Test: Dashboard Error Handling
|
||||
*
|
||||
* Tests error handling and edge cases at the Use Case level:
|
||||
* - Repository errors (driver not found, data access errors)
|
||||
* - Validation errors (invalid driver ID, invalid parameters)
|
||||
* - Business logic errors (permission denied, data inconsistencies)
|
||||
*
|
||||
* Focus: Error orchestration and handling, NOT UI error messages
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryActivityRepository } from '../../../adapters/activity/persistence/inmemory/InMemoryActivityRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetDashboardUseCase } from '../../../core/dashboard/use-cases/GetDashboardUseCase';
|
||||
import { DriverNotFoundError } from '../../../core/dashboard/errors/DriverNotFoundError';
|
||||
import { ValidationError } from '../../../core/shared/errors/ValidationError';
|
||||
|
||||
describe('Dashboard Error Handling Integration', () => {
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let activityRepository: InMemoryActivityRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getDashboardUseCase: GetDashboardUseCase;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories, event publisher, and use case
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// activityRepository = new InMemoryActivityRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getDashboardUseCase = new GetDashboardUseCase({
|
||||
// driverRepository,
|
||||
// raceRepository,
|
||||
// leagueRepository,
|
||||
// activityRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// driverRepository.clear();
|
||||
// raceRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// activityRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
});
|
||||
|
||||
describe('Driver Not Found Errors', () => {
|
||||
it('should throw DriverNotFoundError when driver does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with ID "non-existent-driver-id"
|
||||
// When: GetDashboardUseCase.execute() is called with "non-existent-driver-id"
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: Error message should indicate driver not found
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw DriverNotFoundError when driver ID is valid but not found', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Valid ID but no driver
|
||||
// Given: A valid UUID format driver ID
|
||||
// And: No driver exists with that ID
|
||||
// When: GetDashboardUseCase.execute() is called with the ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should not throw error when driver exists', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Existing driver
|
||||
// Given: A driver exists with ID "existing-driver-id"
|
||||
// When: GetDashboardUseCase.execute() is called with "existing-driver-id"
|
||||
// Then: Should NOT throw DriverNotFoundError
|
||||
// And: Should return dashboard data successfully
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Errors', () => {
|
||||
it('should throw ValidationError when driver ID is empty string', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty driver ID
|
||||
// Given: An empty string as driver ID
|
||||
// When: GetDashboardUseCase.execute() is called with empty string
|
||||
// Then: Should throw ValidationError
|
||||
// And: Error should indicate invalid driver ID
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw ValidationError when driver ID is null', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Null driver ID
|
||||
// Given: null as driver ID
|
||||
// When: GetDashboardUseCase.execute() is called with null
|
||||
// Then: Should throw ValidationError
|
||||
// And: Error should indicate invalid driver ID
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw ValidationError when driver ID is undefined', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Undefined driver ID
|
||||
// Given: undefined as driver ID
|
||||
// When: GetDashboardUseCase.execute() is called with undefined
|
||||
// Then: Should throw ValidationError
|
||||
// And: Error should indicate invalid driver ID
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw ValidationError when driver ID is not a string', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid type driver ID
|
||||
// Given: A number as driver ID
|
||||
// When: GetDashboardUseCase.execute() is called with number
|
||||
// Then: Should throw ValidationError
|
||||
// And: Error should indicate invalid driver ID type
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw ValidationError when driver ID is malformed', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Malformed driver ID
|
||||
// Given: A malformed string as driver ID (e.g., "invalid-id-format")
|
||||
// When: GetDashboardUseCase.execute() is called with malformed ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: Error should indicate invalid driver ID format
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repository Error Handling', () => {
|
||||
it('should handle driver repository query error', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver repository error
|
||||
// Given: A driver exists
|
||||
// And: DriverRepository throws an error during query
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle race repository query error', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race repository error
|
||||
// Given: A driver exists
|
||||
// And: RaceRepository throws an error during query
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle league repository query error', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League repository error
|
||||
// Given: A driver exists
|
||||
// And: LeagueRepository throws an error during query
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle activity repository query error', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Activity repository error
|
||||
// Given: A driver exists
|
||||
// And: ActivityRepository throws an error during query
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle multiple repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Multiple repository errors
|
||||
// Given: A driver exists
|
||||
// And: Multiple repositories throw errors
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle errors appropriately
|
||||
// And: Should not crash the application
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Publisher Error Handling', () => {
|
||||
it('should handle event publisher error gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event publisher error
|
||||
// Given: A driver exists with data
|
||||
// And: EventPublisher throws an error during emit
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should complete the use case execution
|
||||
// And: Should not propagate the event publisher error
|
||||
// And: Dashboard data should still be returned
|
||||
});
|
||||
|
||||
it('should not fail when event publisher is unavailable', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event publisher unavailable
|
||||
// Given: A driver exists with data
|
||||
// And: EventPublisher is configured to fail
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should complete the use case execution
|
||||
// And: Dashboard data should still be returned
|
||||
// And: Should not throw error
|
||||
});
|
||||
});
|
||||
|
||||
describe('Business Logic Error Handling', () => {
|
||||
it('should handle driver with corrupted data gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Corrupted driver data
|
||||
// Given: A driver exists with corrupted/invalid data
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle the corrupted data gracefully
|
||||
// And: Should not crash the application
|
||||
// And: Should return valid dashboard data where possible
|
||||
});
|
||||
|
||||
it('should handle race data inconsistencies', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race data inconsistencies
|
||||
// Given: A driver exists
|
||||
// And: Race data has inconsistencies (e.g., scheduled date in past)
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle inconsistencies gracefully
|
||||
// And: Should filter out invalid races
|
||||
// And: Should return valid dashboard data
|
||||
});
|
||||
|
||||
it('should handle league data inconsistencies', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League data inconsistencies
|
||||
// Given: A driver exists
|
||||
// And: League data has inconsistencies (e.g., missing required fields)
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle inconsistencies gracefully
|
||||
// And: Should filter out invalid leagues
|
||||
// And: Should return valid dashboard data
|
||||
});
|
||||
|
||||
it('should handle activity data inconsistencies', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Activity data inconsistencies
|
||||
// Given: A driver exists
|
||||
// And: Activity data has inconsistencies (e.g., missing timestamp)
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle inconsistencies gracefully
|
||||
// And: Should filter out invalid activities
|
||||
// And: Should return valid dashboard data
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Recovery and Fallbacks', () => {
|
||||
it('should return partial data when one repository fails', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Partial data recovery
|
||||
// Given: A driver exists
|
||||
// And: RaceRepository fails but other repositories succeed
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should return dashboard data with available sections
|
||||
// And: Should not include failed section
|
||||
// And: Should not throw error
|
||||
});
|
||||
|
||||
it('should return empty sections when data is unavailable', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty sections fallback
|
||||
// Given: A driver exists
|
||||
// And: All repositories return empty results
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should return dashboard with empty sections
|
||||
// And: Should include basic driver statistics
|
||||
// And: Should not throw error
|
||||
});
|
||||
|
||||
it('should handle timeout scenarios gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Timeout handling
|
||||
// Given: A driver exists
|
||||
// And: Repository queries take too long
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should handle timeout gracefully
|
||||
// And: Should not crash the application
|
||||
// And: Should return appropriate error or timeout response
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Propagation', () => {
|
||||
it('should propagate DriverNotFoundError to caller', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Error propagation
|
||||
// Given: No driver exists
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: DriverNotFoundError should be thrown
|
||||
// And: Error should be catchable by caller
|
||||
// And: Error should have appropriate message
|
||||
});
|
||||
|
||||
it('should propagate ValidationError to caller', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Validation error propagation
|
||||
// Given: Invalid driver ID
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: ValidationError should be thrown
|
||||
// And: Error should be catchable by caller
|
||||
// And: Error should have appropriate message
|
||||
});
|
||||
|
||||
it('should propagate repository errors to caller', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository error propagation
|
||||
// Given: A driver exists
|
||||
// And: Repository throws error
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Repository error should be propagated
|
||||
// And: Error should be catchable by caller
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Logging and Observability', () => {
|
||||
it('should log errors appropriately', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Error logging
|
||||
// Given: A driver exists
|
||||
// And: An error occurs during execution
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Error should be logged appropriately
|
||||
// And: Log should include error details
|
||||
// And: Log should include context information
|
||||
});
|
||||
|
||||
it('should include context in error messages', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Error context
|
||||
// Given: A driver exists
|
||||
// And: An error occurs during execution
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Error message should include driver ID
|
||||
// And: Error message should include operation details
|
||||
// And: Error message should be informative
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Integration Test: Dashboard Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of dashboard-related Use Cases:
|
||||
* - GetDashboardUseCase: Retrieves driver statistics, upcoming races, standings, and activity
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryActivityRepository } from '../../../adapters/activity/persistence/inmemory/InMemoryActivityRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetDashboardUseCase } from '../../../core/dashboard/use-cases/GetDashboardUseCase';
|
||||
import { DashboardQuery } from '../../../core/dashboard/ports/DashboardQuery';
|
||||
|
||||
describe('Dashboard Use Case Orchestration', () => {
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let activityRepository: InMemoryActivityRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getDashboardUseCase: GetDashboardUseCase;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// activityRepository = new InMemoryActivityRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getDashboardUseCase = new GetDashboardUseCase({
|
||||
// driverRepository,
|
||||
// raceRepository,
|
||||
// leagueRepository,
|
||||
// activityRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// driverRepository.clear();
|
||||
// raceRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// activityRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
});
|
||||
|
||||
describe('GetDashboardUseCase - Success Path', () => {
|
||||
it('should retrieve complete dashboard data for a driver with all data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with complete data
|
||||
// Given: A driver exists with statistics (rating, rank, starts, wins, podiums)
|
||||
// And: The driver has upcoming races scheduled
|
||||
// And: The driver is participating in active championships
|
||||
// And: The driver has recent activity (race results, events)
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain all dashboard sections
|
||||
// And: Driver statistics should be correctly calculated
|
||||
// And: Upcoming races should be limited to 3
|
||||
// And: Championship standings should include league info
|
||||
// And: Recent activity should be sorted by timestamp
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve dashboard data for a new driver with no history', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: New driver with minimal data
|
||||
// Given: A newly registered driver exists
|
||||
// And: The driver has no race history
|
||||
// And: The driver has no upcoming races
|
||||
// And: The driver is not in any championships
|
||||
// And: The driver has no recent activity
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain basic driver statistics
|
||||
// And: Upcoming races section should be empty
|
||||
// And: Championship standings section should be empty
|
||||
// And: Recent activity section should be empty
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve dashboard data with upcoming races limited to 3', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with many upcoming races
|
||||
// Given: A driver exists
|
||||
// And: The driver has 5 upcoming races scheduled
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain only 3 upcoming races
|
||||
// And: The races should be sorted by scheduled date (earliest first)
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve dashboard data with championship standings for multiple leagues', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver in multiple championships
|
||||
// Given: A driver exists
|
||||
// And: The driver is participating in 3 active championships
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain standings for all 3 leagues
|
||||
// And: Each league should show position, points, and total drivers
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve dashboard data with recent activity sorted by timestamp', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with multiple recent activities
|
||||
// Given: A driver exists
|
||||
// And: The driver has 5 recent activities (race results, events)
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain all activities
|
||||
// And: Activities should be sorted by timestamp (newest first)
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDashboardUseCase - Edge Cases', () => {
|
||||
it('should handle driver with no upcoming races but has completed races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with completed races but no upcoming races
|
||||
// Given: A driver exists
|
||||
// And: The driver has completed races in the past
|
||||
// And: The driver has no upcoming races scheduled
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain driver statistics from completed races
|
||||
// And: Upcoming races section should be empty
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle driver with upcoming races but no completed races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with upcoming races but no completed races
|
||||
// Given: A driver exists
|
||||
// And: The driver has upcoming races scheduled
|
||||
// And: The driver has no completed races
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain upcoming races
|
||||
// And: Driver statistics should show zeros for wins, podiums, etc.
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle driver with championship standings but no recent activity', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver in championships but no recent activity
|
||||
// Given: A driver exists
|
||||
// And: The driver is participating in active championships
|
||||
// And: The driver has no recent activity
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain championship standings
|
||||
// And: Recent activity section should be empty
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle driver with recent activity but no championship standings', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with recent activity but not in championships
|
||||
// Given: A driver exists
|
||||
// And: The driver has recent activity
|
||||
// And: The driver is not participating in any championships
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain recent activity
|
||||
// And: Championship standings section should be empty
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle driver with no data at all', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver with absolutely no data
|
||||
// Given: A driver exists
|
||||
// And: The driver has no statistics
|
||||
// And: The driver has no upcoming races
|
||||
// And: The driver has no championship standings
|
||||
// And: The driver has no recent activity
|
||||
// When: GetDashboardUseCase.execute() is called with driver ID
|
||||
// Then: The result should contain basic driver info
|
||||
// And: All sections should be empty or show default values
|
||||
// And: EventPublisher should emit DashboardAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDashboardUseCase - Error Handling', () => {
|
||||
it('should throw error when driver does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
// When: GetDashboardUseCase.execute() is called with non-existent driver ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when driver ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid driver ID
|
||||
// Given: An invalid driver ID (e.g., empty string, null, undefined)
|
||||
// When: GetDashboardUseCase.execute() is called with invalid driver ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A driver exists
|
||||
// And: DriverRepository throws an error during query
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dashboard Data Orchestration', () => {
|
||||
it('should correctly calculate driver statistics from race results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver statistics calculation
|
||||
// Given: A driver exists
|
||||
// And: The driver has 10 completed races
|
||||
// And: The driver has 3 wins
|
||||
// And: The driver has 5 podiums
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Driver statistics should show:
|
||||
// - Starts: 10
|
||||
// - Wins: 3
|
||||
// - Podiums: 5
|
||||
// - Rating: Calculated based on performance
|
||||
// - Rank: Calculated based on rating
|
||||
});
|
||||
|
||||
it('should correctly format upcoming race time information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming race time formatting
|
||||
// Given: A driver exists
|
||||
// And: The driver has an upcoming race scheduled in 2 days 4 hours
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: The upcoming race should include:
|
||||
// - Track name
|
||||
// - Car type
|
||||
// - Scheduled date and time
|
||||
// - Time until race (formatted as "2 days 4 hours")
|
||||
});
|
||||
|
||||
it('should correctly aggregate championship standings across leagues', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Championship standings aggregation
|
||||
// Given: A driver exists
|
||||
// And: The driver is in 2 championships
|
||||
// And: In Championship A: Position 5, 150 points, 20 drivers
|
||||
// And: In Championship B: Position 12, 85 points, 15 drivers
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Championship standings should show:
|
||||
// - League A: Position 5, 150 points, 20 drivers
|
||||
// - League B: Position 12, 85 points, 15 drivers
|
||||
});
|
||||
|
||||
it('should correctly format recent activity with proper status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent activity formatting
|
||||
// Given: A driver exists
|
||||
// And: The driver has a race result (finished 3rd)
|
||||
// And: The driver has a league invitation event
|
||||
// When: GetDashboardUseCase.execute() is called
|
||||
// Then: Recent activity should show:
|
||||
// - Race result: Type "race_result", Status "success", Description "Finished 3rd at Monza"
|
||||
// - League invitation: Type "league_invitation", Status "info", Description "Invited to League XYZ"
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user