mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
getSummary, and asserts the returned summary fields (including annualizedDividendYield and computed totals). The new test lives in apps/api/src/app/portfolio/portfolio.service.spec.ts.pull/6258/head
1 changed files with 160 additions and 61 deletions
@ -1,77 +1,176 @@ |
|||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
||||
|
import { |
||||
|
activityDummyData, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; |
||||
|
import { Activity } from '@ghostfolio/common/interfaces'; |
||||
|
import { RequestWithUser } from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { Type as ActivityType } from '@prisma/client'; |
||||
|
import { Big } from 'big.js'; |
||||
|
|
||||
|
import { PortfolioService } from './portfolio.service'; |
||||
|
|
||||
describe('PortfolioService', () => { |
describe('PortfolioService', () => { |
||||
describe('getSummary', () => { |
describe('getSummary', () => { |
||||
it('should include annualizedDividendYield from calculator snapshot', async () => { |
beforeEach(() => { |
||||
// This test verifies that getSummary() correctly extracts
|
jest.useFakeTimers().setSystemTime(new Date('2023-07-10')); |
||||
// annualizedDividendYield from the calculator snapshot
|
}); |
||||
// and includes it in the returned PortfolioSummary
|
|
||||
|
afterEach(() => { |
||||
// Mock calculator with annualizedDividendYield in snapshot
|
jest.useRealTimers(); |
||||
const mockSnapshot = { |
jest.restoreAllMocks(); |
||||
annualizedDividendYield: 0.0184, // 1.84%
|
}); |
||||
currentValueInBaseCurrency: { toNumber: () => 500 }, |
|
||||
totalInvestment: { toNumber: () => 500 }, |
it('returns annualizedDividendYield from the calculator snapshot', async () => { |
||||
totalInvestmentWithCurrencyEffect: { toNumber: () => 500 } |
const activities: Activity[] = [ |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
currency: 'USD', |
||||
|
date: new Date('2023-06-01'), |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
feeInBaseCurrency: 0, |
||||
|
quantity: 2, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: ActivityType.BUY, |
||||
|
unitPrice: 50, |
||||
|
unitPriceInAssetProfileCurrency: 50, |
||||
|
value: 100, |
||||
|
valueInBaseCurrency: 100 |
||||
|
}, |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
currency: 'USD', |
||||
|
date: new Date('2023-06-02'), |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
feeInBaseCurrency: 0, |
||||
|
quantity: 1, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: ActivityType.SELL, |
||||
|
unitPrice: 40, |
||||
|
unitPriceInAssetProfileCurrency: 40, |
||||
|
value: 40, |
||||
|
valueInBaseCurrency: 40 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const exchangeRateDataService = { |
||||
|
toCurrency: jest.fn((value: number) => value) |
||||
}; |
}; |
||||
|
|
||||
const mockCalculator = { |
const orderService = { |
||||
getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) |
getOrders: jest.fn().mockResolvedValue({ activities }) |
||||
} as unknown as PortfolioCalculator; |
}; |
||||
|
|
||||
// Verify that the snapshot has the annualizedDividendYield
|
const userService = { |
||||
const snapshot = await mockCalculator.getSnapshot(); |
user: jest.fn().mockResolvedValue({ |
||||
expect(snapshot).toHaveProperty('annualizedDividendYield'); |
id: userDummyData.id, |
||||
expect(snapshot.annualizedDividendYield).toBe(0.0184); |
settings: { |
||||
|
settings: { |
||||
// The actual PortfolioService.getSummary() implementation should:
|
baseCurrency: DEFAULT_CURRENCY, |
||||
// 1. Call portfolioCalculator.getSnapshot()
|
emergencyFund: 0 |
||||
// 2. Extract annualizedDividendYield from the snapshot
|
} |
||||
// 3. Include it in the returned PortfolioSummary
|
} |
||||
//
|
}) |
||||
// Implementation in portfolio.service.ts:1867-1869:
|
}; |
||||
// const { annualizedDividendYield, ... } = await portfolioCalculator.getSnapshot();
|
|
||||
//
|
const accountService = { |
||||
// And in the return statement at line 1965:
|
getCashDetails: jest.fn().mockResolvedValue({ |
||||
// return { annualizedDividendYield, ... }
|
balanceInBaseCurrency: 1000 |
||||
}); |
}) |
||||
|
}; |
||||
|
|
||||
it('should handle zero annualizedDividendYield for portfolios without dividends', async () => { |
const impersonationService = { |
||||
const mockSnapshot = { |
validateImpersonationId: jest.fn().mockResolvedValue(undefined) |
||||
annualizedDividendYield: 0, |
|
||||
currentValueInBaseCurrency: { toNumber: () => 1000 }, |
|
||||
totalInvestment: { toNumber: () => 1000 }, |
|
||||
totalInvestmentWithCurrencyEffect: { toNumber: () => 1000 } |
|
||||
}; |
}; |
||||
|
|
||||
const mockCalculator = { |
const request = { |
||||
getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) |
user: { |
||||
|
id: userDummyData.id, |
||||
|
settings: { settings: { baseCurrency: DEFAULT_CURRENCY } } |
||||
|
} |
||||
|
} as RequestWithUser; |
||||
|
|
||||
|
const portfolioCalculator = { |
||||
|
getDividendInBaseCurrency: jest.fn().mockResolvedValue(new Big(12)), |
||||
|
getFeesInBaseCurrency: jest.fn().mockResolvedValue(new Big(4)), |
||||
|
getInterestInBaseCurrency: jest.fn().mockResolvedValue(new Big(1)), |
||||
|
getLiabilitiesInBaseCurrency: jest.fn().mockResolvedValue(new Big(6)), |
||||
|
getSnapshot: jest.fn().mockResolvedValue({ |
||||
|
annualizedDividendYield: 0.0123, |
||||
|
currentValueInBaseCurrency: new Big(500), |
||||
|
totalInvestment: new Big(400) |
||||
|
}), |
||||
|
getStartDate: jest.fn().mockReturnValue(new Date('2023-01-01')) |
||||
} as unknown as PortfolioCalculator; |
} as unknown as PortfolioCalculator; |
||||
|
|
||||
const snapshot = await mockCalculator.getSnapshot(); |
const service = new PortfolioService( |
||||
expect(snapshot.annualizedDividendYield).toBe(0); |
{} as any, |
||||
}); |
accountService as any, |
||||
|
{} as any, |
||||
|
{} as any, |
||||
|
{} as any, |
||||
|
exchangeRateDataService as any, |
||||
|
{} as any, |
||||
|
impersonationService as any, |
||||
|
orderService as any, |
||||
|
request, |
||||
|
{} as any, |
||||
|
{} as any, |
||||
|
userService as any |
||||
|
); |
||||
|
|
||||
|
jest.spyOn(service, 'getPerformance').mockResolvedValue({ |
||||
|
performance: { |
||||
|
netPerformance: 20, |
||||
|
netPerformancePercentage: 0.05, |
||||
|
netPerformancePercentageWithCurrencyEffect: 0.05, |
||||
|
netPerformanceWithCurrencyEffect: 20 |
||||
|
} |
||||
|
} as any); |
||||
|
|
||||
|
const summary = await (service as any).getSummary({ |
||||
|
balanceInBaseCurrency: 1000, |
||||
|
emergencyFundHoldingsValueInBaseCurrency: 0, |
||||
|
filteredValueInBaseCurrency: new Big(200), |
||||
|
impersonationId: userDummyData.id, |
||||
|
portfolioCalculator, |
||||
|
userCurrency: DEFAULT_CURRENCY, |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
it('should verify the data flow from Calculator to Service', () => { |
expect(portfolioCalculator.getSnapshot).toHaveBeenCalledTimes(1); |
||||
// This test documents the expected data flow:
|
expect(summary).toMatchObject({ |
||||
//
|
annualizedDividendYield: 0.0123, |
||||
// 1. Calculator Level (portfolio-calculator.ts):
|
cash: 1000, |
||||
// - Calculates annualizedDividendYield for each position
|
committedFunds: 60, |
||||
// - Aggregates to portfolio-wide annualizedDividendYield in snapshot
|
dividendInBaseCurrency: 12, |
||||
//
|
fees: 4, |
||||
// 2. Service Level (portfolio.service.ts:getSummary):
|
grossPerformance: 24, |
||||
// - Calls: const { annualizedDividendYield } = await portfolioCalculator.getSnapshot()
|
grossPerformanceWithCurrencyEffect: 24, |
||||
// - Returns: { annualizedDividendYield, ...otherFields }
|
interestInBaseCurrency: 1, |
||||
//
|
liabilitiesInBaseCurrency: 6, |
||||
// 3. API Response (PortfolioSummary interface):
|
totalBuy: 100, |
||||
// - Client receives annualizedDividendYield as part of the summary
|
totalInvestment: 400, |
||||
//
|
totalSell: 40, |
||||
// This flow is verified by:
|
totalValueInBaseCurrency: 1494 |
||||
// - Calculator tests: portfolio-calculator-msft-buy-with-dividend.spec.ts
|
}); |
||||
// - This service test: verifies extraction from snapshot
|
expect(summary.activityCount).toBe(2); |
||||
// - Integration would be tested via E2E tests (if they existed)
|
expect(summary.dateOfFirstActivity).toEqual(new Date('2023-01-01')); |
||||
|
|
||||
expect(true).toBe(true); // Documentation test
|
|
||||
}); |
}); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
|||||
Loading…
Reference in new issue