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