Browse Source

Replaced the placeholder spec with a real unit test that instantiates PortfolioService, stubs dependencies, calls

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
Sven Günther 5 days ago
parent
commit
e88e4d292a
  1. 219
      apps/api/src/app/portfolio/portfolio.service.spec.ts

219
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -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…
Cancel
Save