mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
3 changed files with 288 additions and 4 deletions
@ -0,0 +1,281 @@ |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; |
|||
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|||
import { resetHours } from '@ghostfolio/common/helper'; |
|||
|
|||
import { BenchmarksService } from './benchmarks.service'; |
|||
|
|||
describe('BenchmarksService', () => { |
|||
let benchmarksService: BenchmarksService; |
|||
|
|||
const mockBenchmarkService = { |
|||
calculateChangeInPercentage: jest.fn( |
|||
(startPrice: number, endPrice: number) => { |
|||
return (endPrice - startPrice) / startPrice; |
|||
} |
|||
) |
|||
}; |
|||
|
|||
const mockExchangeRateDataService = { |
|||
getExchangeRatesByCurrency: jest.fn().mockResolvedValue({}) |
|||
}; |
|||
|
|||
const mockMarketDataService = { |
|||
marketDataItems: jest.fn() |
|||
}; |
|||
|
|||
const mockPortfolioService = { |
|||
getPerformance: jest.fn() |
|||
}; |
|||
|
|||
const mockSymbolService = { |
|||
get: jest.fn() |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
jest.clearAllMocks(); |
|||
|
|||
benchmarksService = new BenchmarksService( |
|||
mockBenchmarkService as unknown as BenchmarkService, |
|||
mockExchangeRateDataService as unknown as ExchangeRateDataService, |
|||
mockMarketDataService as unknown as MarketDataService, |
|||
mockPortfolioService as unknown as PortfolioService, |
|||
mockSymbolService as unknown as SymbolService |
|||
); |
|||
}); |
|||
|
|||
describe('getMarketDataForUser', () => { |
|||
it('should return benchmark data for a calendar year date range', async () => { |
|||
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999); // 2024-12-31T23:59:59.999
|
|||
const endDate = new Date(2025, 11, 31, 23, 59, 59, 999); // 2025-12-31T23:59:59.999
|
|||
|
|||
// Portfolio chart starts from 2025-01-01 (exclusive boundary excludes 2024-12-31)
|
|||
mockPortfolioService.getPerformance.mockResolvedValue({ |
|||
chart: [ |
|||
{ date: '2025-01-01', value: 100 }, |
|||
{ date: '2025-06-30', value: 110 }, |
|||
{ date: '2025-12-31', value: 120 } |
|||
] |
|||
}); |
|||
|
|||
mockSymbolService.get.mockResolvedValue({ |
|||
currency: 'USD', |
|||
marketPrice: 150 |
|||
}); |
|||
|
|||
// Market data includes 2024-12-31 (start date) because our fix adds it to the query
|
|||
mockMarketDataService.marketDataItems.mockResolvedValue([ |
|||
{ |
|||
date: new Date(Date.UTC(2024, 11, 31)), |
|||
marketPrice: 100 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2025, 0, 1)), |
|||
marketPrice: 100 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2025, 5, 30)), |
|||
marketPrice: 110 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2025, 11, 31)), |
|||
marketPrice: 120 |
|||
} |
|||
]); |
|||
|
|||
const result = await benchmarksService.getMarketDataForUser({ |
|||
dataSource: 'YAHOO', |
|||
dateRange: '2025', |
|||
endDate, |
|||
filters: [], |
|||
startDate, |
|||
symbol: 'VOO', |
|||
userId: 'user-1', |
|||
userSettings: { baseCurrency: 'USD' } |
|||
}); |
|||
|
|||
// Verify the market data query includes resetHours(startDate)
|
|||
const marketDataCall = |
|||
mockMarketDataService.marketDataItems.mock.calls[0][0]; |
|||
const dateInArray = marketDataCall.where.date.in; |
|||
|
|||
// The start date (2024-12-31) should be included in the query
|
|||
const startDateNormalized = resetHours(startDate); |
|||
expect(dateInArray).toContainEqual(startDateNormalized); |
|||
|
|||
// Chart dates should also be included
|
|||
expect(dateInArray.length).toBeGreaterThanOrEqual(4); // startDate + 3 chart dates
|
|||
|
|||
// Should return non-empty market data
|
|||
expect(result.marketData.length).toBeGreaterThan(0); |
|||
expect(result.marketData[0].date).toBe('2024-12-31'); |
|||
}); |
|||
|
|||
it('should return benchmark data for a named date range (ytd)', async () => { |
|||
const startDate = new Date(Date.UTC(2025, 11, 31)); // 2025-12-31T00:00:00.000Z (ytd start)
|
|||
const endDate = new Date(); // now
|
|||
|
|||
// Portfolio chart includes 2025-12-31 (midnight, inclusive boundary)
|
|||
mockPortfolioService.getPerformance.mockResolvedValue({ |
|||
chart: [ |
|||
{ date: '2025-12-31', value: 100 }, |
|||
{ date: '2026-01-15', value: 105 }, |
|||
{ date: '2026-08-15', value: 120 } |
|||
] |
|||
}); |
|||
|
|||
mockSymbolService.get.mockResolvedValue({ |
|||
currency: 'USD', |
|||
marketPrice: 150 |
|||
}); |
|||
|
|||
mockMarketDataService.marketDataItems.mockResolvedValue([ |
|||
{ |
|||
date: new Date(Date.UTC(2025, 11, 31)), |
|||
marketPrice: 100 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2026, 0, 15)), |
|||
marketPrice: 105 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2026, 7, 15)), |
|||
marketPrice: 120 |
|||
} |
|||
]); |
|||
|
|||
const result = await benchmarksService.getMarketDataForUser({ |
|||
dataSource: 'YAHOO', |
|||
dateRange: 'ytd', |
|||
endDate, |
|||
filters: [], |
|||
startDate, |
|||
symbol: 'VOO', |
|||
userId: 'user-1', |
|||
userSettings: { baseCurrency: 'USD' } |
|||
}); |
|||
|
|||
expect(result.marketData.length).toBeGreaterThan(0); |
|||
expect(result.marketData[0].date).toBe('2025-12-31'); |
|||
}); |
|||
|
|||
it('should return empty market data when market price at start date is unavailable', async () => { |
|||
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999); |
|||
|
|||
mockPortfolioService.getPerformance.mockResolvedValue({ |
|||
chart: [{ date: '2025-01-01', value: 100 }] |
|||
}); |
|||
|
|||
mockSymbolService.get.mockResolvedValue({ |
|||
currency: 'USD', |
|||
marketPrice: 150 |
|||
}); |
|||
|
|||
// No market data at all
|
|||
mockMarketDataService.marketDataItems.mockResolvedValue([]); |
|||
|
|||
const result = await benchmarksService.getMarketDataForUser({ |
|||
dataSource: 'YAHOO', |
|||
dateRange: '2025', |
|||
filters: [], |
|||
startDate, |
|||
symbol: 'VOO', |
|||
userId: 'user-1', |
|||
userSettings: { baseCurrency: 'USD' } |
|||
}); |
|||
|
|||
expect(result.marketData).toEqual([]); |
|||
}); |
|||
|
|||
it('should return empty market data when symbol profile is not found', async () => { |
|||
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999); |
|||
|
|||
mockPortfolioService.getPerformance.mockResolvedValue({ |
|||
chart: [{ date: '2025-01-01', value: 100 }] |
|||
}); |
|||
|
|||
mockSymbolService.get.mockResolvedValue(null); |
|||
|
|||
mockMarketDataService.marketDataItems.mockResolvedValue([ |
|||
{ |
|||
date: new Date(Date.UTC(2024, 11, 31)), |
|||
marketPrice: 100 |
|||
} |
|||
]); |
|||
|
|||
const result = await benchmarksService.getMarketDataForUser({ |
|||
dataSource: 'YAHOO', |
|||
dateRange: '2025', |
|||
filters: [], |
|||
startDate, |
|||
symbol: 'VOO', |
|||
userId: 'user-1', |
|||
userSettings: { baseCurrency: 'USD' } |
|||
}); |
|||
|
|||
expect(result.marketData).toEqual([]); |
|||
}); |
|||
|
|||
it('should include the start date in the market data query for all date ranges', async () => { |
|||
const testCases = [ |
|||
{ |
|||
dateRange: '2025' as const, |
|||
startDate: new Date(2024, 11, 31, 23, 59, 59, 999), |
|||
expectedQueryDate: new Date(Date.UTC(2024, 11, 31)) |
|||
}, |
|||
{ |
|||
dateRange: 'ytd' as const, |
|||
startDate: new Date(Date.UTC(2025, 11, 31)), |
|||
expectedQueryDate: new Date(Date.UTC(2025, 11, 31)) |
|||
}, |
|||
{ |
|||
dateRange: '1y' as const, |
|||
startDate: new Date(Date.UTC(2025, 7, 15)), |
|||
expectedQueryDate: new Date(Date.UTC(2025, 7, 15)) |
|||
} |
|||
]; |
|||
|
|||
for (const { dateRange, startDate, expectedQueryDate } of testCases) { |
|||
jest.clearAllMocks(); |
|||
|
|||
mockPortfolioService.getPerformance.mockResolvedValue({ |
|||
chart: [{ date: '2025-01-01', value: 100 }] |
|||
}); |
|||
|
|||
mockSymbolService.get.mockResolvedValue({ |
|||
currency: 'USD', |
|||
marketPrice: 150 |
|||
}); |
|||
|
|||
mockMarketDataService.marketDataItems.mockResolvedValue([ |
|||
{ |
|||
date: expectedQueryDate, |
|||
marketPrice: 100 |
|||
}, |
|||
{ |
|||
date: new Date(Date.UTC(2025, 0, 1)), |
|||
marketPrice: 105 |
|||
} |
|||
]); |
|||
|
|||
await benchmarksService.getMarketDataForUser({ |
|||
dataSource: 'YAHOO', |
|||
dateRange, |
|||
filters: [], |
|||
startDate, |
|||
symbol: 'VOO', |
|||
userId: 'user-1', |
|||
userSettings: { baseCurrency: 'USD' } |
|||
}); |
|||
|
|||
const marketDataCall = |
|||
mockMarketDataService.marketDataItems.mock.calls[0][0]; |
|||
const dateInArray = marketDataCall.where.date.in; |
|||
|
|||
expect(dateInArray).toContainEqual(resetHours(startDate)); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue