fix bruno collection
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { FinanceController } from '../server/domains/finance/finance.controller.js';
|
||||
import { PortfolioAdvisor } from '../server/domains/portfolio/PortfolioAdvisor.js';
|
||||
import type { PortfolioHolding } from '../server/domains/shared/types/portfolio.model.js';
|
||||
|
||||
class MockScreenerEngine {
|
||||
async screenTickers() {
|
||||
return {
|
||||
STOCK: [],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: {
|
||||
sp500Price: 5500,
|
||||
riskFreeRate: 4.5,
|
||||
vixLevel: 12.5,
|
||||
rateRegime: 'NORMAL' as const,
|
||||
volatilityRegime: 'LOW' as const,
|
||||
benchmarks: {
|
||||
marketPE: 20,
|
||||
techPE: 28,
|
||||
reitYield: 3.5,
|
||||
igSpread: 1.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MockPortfolioRepository {
|
||||
async getHoldings() {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
ticker: 'AAPL',
|
||||
shares: 10,
|
||||
costBasis: 150,
|
||||
type: 'stock' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async addHolding(holding: PortfolioHolding) {
|
||||
return { id: '1', ...holding };
|
||||
}
|
||||
|
||||
async deleteHolding(_id: string) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class MockYahooClient {
|
||||
async fetchSummary() {
|
||||
return {
|
||||
price: { regularMarketPrice: 189.5 },
|
||||
summaryDetail: { fiftyTwoWeekHigh: 199, fiftyTwoWeekLow: 164 },
|
||||
quoteType: { quoteType: 'EQUITY' },
|
||||
defaultKeyStatistics: { trailingPE: 28.5 },
|
||||
financialData: {
|
||||
returnOnEquity: 95.2 / 100,
|
||||
operatingMargins: 30.7 / 100,
|
||||
grossMargins: 46.2 / 100,
|
||||
freeCashflow: 110e9,
|
||||
totalRevenue: 383.3e9,
|
||||
debtToEquity: 75.56,
|
||||
currentRatio: 0.95,
|
||||
},
|
||||
incomeStatementHistoryQuarterly: {
|
||||
incomeStatementHistory: [
|
||||
{
|
||||
commonStockSharesOutstanding: 15.6e9,
|
||||
netIncome: 25e9,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test('FinanceController', async (t) => {
|
||||
await t.test('registers portfolio endpoint', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
let portfolioEndpointRegistered = false;
|
||||
const mockApp = {
|
||||
get: (path: string) => {
|
||||
if (path === '/api/finance/portfolio') portfolioEndpointRegistered = true;
|
||||
},
|
||||
post: () => {},
|
||||
delete: () => {},
|
||||
};
|
||||
|
||||
controller.register(mockApp as any);
|
||||
assert.ok(portfolioEndpointRegistered);
|
||||
});
|
||||
|
||||
await t.test('registers market context endpoint', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
let contextEndpointRegistered = false;
|
||||
const mockApp = {
|
||||
get: (path: string) => {
|
||||
if (path === '/api/finance/market-context') contextEndpointRegistered = true;
|
||||
},
|
||||
post: () => {},
|
||||
delete: () => {},
|
||||
};
|
||||
|
||||
controller.register(mockApp as any);
|
||||
assert.ok(contextEndpointRegistered);
|
||||
});
|
||||
|
||||
await t.test('registers holdings endpoints', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
let addHoldingRegistered = false;
|
||||
let deleteHoldingRegistered = false;
|
||||
|
||||
const mockApp = {
|
||||
get: () => {},
|
||||
post: (path: string) => {
|
||||
if (path === '/api/finance/holdings') addHoldingRegistered = true;
|
||||
},
|
||||
delete: (path: string) => {
|
||||
if (path === '/api/finance/holdings/:ticker') deleteHoldingRegistered = true;
|
||||
},
|
||||
};
|
||||
|
||||
controller.register(mockApp as any);
|
||||
assert.ok(addHoldingRegistered);
|
||||
assert.ok(deleteHoldingRegistered);
|
||||
});
|
||||
|
||||
await t.test('returns portfolio with holdings', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const holdings = await repository.getHoldings();
|
||||
assert.ok(Array.isArray(holdings));
|
||||
assert.equal(holdings.length, 1);
|
||||
assert.equal(holdings[0].ticker, 'AAPL');
|
||||
});
|
||||
|
||||
await t.test('returns market context data', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const results = await engine.screenTickers([]);
|
||||
assert.ok(results.marketContext);
|
||||
assert.ok(results.marketContext.sp500Price);
|
||||
assert.ok(results.marketContext.benchmarks);
|
||||
});
|
||||
|
||||
await t.test('adds holding to portfolio', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const newHolding: PortfolioHolding = {
|
||||
ticker: 'MSFT',
|
||||
shares: 5,
|
||||
costBasis: 400,
|
||||
source: 'manual',
|
||||
type: 'stock',
|
||||
};
|
||||
|
||||
const added = await repository.addHolding(newHolding);
|
||||
assert.ok(added);
|
||||
assert.equal(added.ticker, 'MSFT');
|
||||
});
|
||||
|
||||
await t.test('deletes holding from portfolio', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const deleted = await repository.deleteHolding('1');
|
||||
assert.ok(deleted);
|
||||
});
|
||||
|
||||
await t.test('handles empty portfolio gracefully', async () => {
|
||||
class EmptyRepository {
|
||||
async getHoldings() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async addHolding(holding: PortfolioHolding) {
|
||||
return { id: '1', ...holding };
|
||||
}
|
||||
|
||||
async deleteHolding(_id: string) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new EmptyRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const holdings = await repository.getHoldings();
|
||||
assert.equal(holdings.length, 0);
|
||||
});
|
||||
|
||||
await t.test('analyzes portfolio advice', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const holdings = await repository.getHoldings();
|
||||
const advice = await advisor.advise(holdings, {
|
||||
STOCK: [],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
});
|
||||
assert.ok(advice);
|
||||
});
|
||||
|
||||
await t.test('includes holdings in portfolio response', async () => {
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MockPortfolioRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const holdings = await repository.getHoldings();
|
||||
const advice = await advisor.advise(holdings, {
|
||||
STOCK: [],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
});
|
||||
|
||||
assert.ok(holdings);
|
||||
assert.ok(advice);
|
||||
});
|
||||
|
||||
await t.test('handles multiple holdings in portfolio', async () => {
|
||||
class MultiHoldingRepository {
|
||||
async getHoldings() {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
ticker: 'AAPL',
|
||||
shares: 10,
|
||||
costBasis: 150,
|
||||
type: 'stock' as const,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
ticker: 'MSFT',
|
||||
shares: 5,
|
||||
costBasis: 400,
|
||||
type: 'stock' as const,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
ticker: 'VOO',
|
||||
shares: 20,
|
||||
costBasis: 350,
|
||||
type: 'etf' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async addHolding(holding: PortfolioHolding) {
|
||||
return { id: '4', ...holding };
|
||||
}
|
||||
|
||||
async deleteHolding(_id: string) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new MockScreenerEngine() as any;
|
||||
const repository = new MultiHoldingRepository() as any;
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const _controller = new FinanceController(engine, repository, advisor);
|
||||
|
||||
const holdings = await repository.getHoldings();
|
||||
assert.equal(holdings.length, 3);
|
||||
assert.ok(holdings.some((h: any) => h.ticker === 'AAPL'));
|
||||
assert.ok(holdings.some((h: any) => h.ticker === 'MSFT'));
|
||||
assert.ok(holdings.some((h: any) => h.type === 'etf'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user