test(tbank): update portfolio tests, add getPositions tests, fix operation fixture name
This commit is contained in:
parent
8b202ef7d3
commit
4b87eccba4
@ -11,7 +11,7 @@ describe('portfolio.mapper', () => {
|
||||
accessLevel: 'ACCOUNT_ACCESS_LEVEL_FULL_ACCESS',
|
||||
};
|
||||
|
||||
it('combines portfolio totals, cash, and enriched positions', () => {
|
||||
it('combines portfolio totals and cash (positions removed)', () => {
|
||||
const result = mapBrokerPortfolio({
|
||||
account,
|
||||
portfolio: {
|
||||
@ -44,11 +44,5 @@ describe('portfolio.mapper', () => {
|
||||
expect(result.totals.shares?.value).toBe(1000);
|
||||
expect(result.cash[0].value).toBe(500);
|
||||
expect(result.blockedCash[0].value).toBe(10);
|
||||
expect(result.positions[0]).toMatchObject({
|
||||
ticker: 'SBER',
|
||||
name: 'Sberbank',
|
||||
quantity: 10,
|
||||
currentValue: { value: 2500 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,6 +32,7 @@ describe('BrokerOperationSyncService', () => {
|
||||
category: 'trade',
|
||||
description: null,
|
||||
state: 'OPERATION_STATE_EXECUTED',
|
||||
name: null,
|
||||
instrumentUid: 'uid-1',
|
||||
figi: null,
|
||||
ticker: 'SBER',
|
||||
@ -98,6 +99,7 @@ describe('BrokerOperationSyncService', () => {
|
||||
category: 'trade',
|
||||
description: null,
|
||||
state: null,
|
||||
name: null,
|
||||
instrumentUid: null,
|
||||
figi: null,
|
||||
ticker: null,
|
||||
|
||||
@ -22,7 +22,7 @@ describe('BrokerPortfolioService', () => {
|
||||
await expect(service.getPortfolio('missing')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('fetches portfolio and positions through cache', async () => {
|
||||
it('fetches portfolio through cache without positions', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
@ -60,6 +60,7 @@ describe('BrokerPortfolioService', () => {
|
||||
|
||||
expect(result.data.account.id).toBe('acc-1');
|
||||
expect(result.data.cash[0].value).toBe(1000);
|
||||
expect('positions' in result.data).toBe(false);
|
||||
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||
'tbank:portfolio',
|
||||
['acc-1'],
|
||||
@ -68,73 +69,189 @@ describe('BrokerPortfolioService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns portfolio when one instrument enrichment request fails', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
describe('getPositions', () => {
|
||||
it('throws 404 for missing account', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue(null);
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
|
||||
await expect(service.getPositions('missing')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
getPositions: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary)
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
it('returns first page of positions', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary).mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 },
|
||||
positions: [
|
||||
{
|
||||
figi: 'figi-1',
|
||||
instrumentUid: 'uid-1',
|
||||
quantity: { units: '1', nano: 0 },
|
||||
positionUid: 'pos-1',
|
||||
quantity: { units: '10', nano: 0 },
|
||||
},
|
||||
{
|
||||
figi: 'figi-2',
|
||||
instrumentUid: 'uid-2',
|
||||
quantity: { units: '2', nano: 0 },
|
||||
positionUid: 'pos-2',
|
||||
quantity: { units: '20', nano: 0 },
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
money: [],
|
||||
blocked: [],
|
||||
securities: [],
|
||||
});
|
||||
vi.mocked(instruments.findByInstrumentUid)
|
||||
.mockResolvedValueOnce({
|
||||
uid: 'uid-1',
|
||||
figi: 'figi-1',
|
||||
ticker: 'AAA',
|
||||
classCode: 'TQBR',
|
||||
name: 'First share',
|
||||
instrumentType: 'share',
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('instrument lookup failed'));
|
||||
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
const result = await service.getPortfolio('acc-1');
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
const result = await service.getPositions('acc-1', undefined, 1);
|
||||
|
||||
expect(result.data.positions).toHaveLength(2);
|
||||
expect(result.data.positions[0]).toMatchObject({
|
||||
instrumentUid: 'uid-1',
|
||||
ticker: 'AAA',
|
||||
name: 'First share',
|
||||
expect(result.data.accountId).toBe('acc-1');
|
||||
expect(result.data.items).toHaveLength(1);
|
||||
expect(result.data.items[0].positionUid).toBe('pos-1');
|
||||
expect(result.data.hasNext).toBe(true);
|
||||
expect(result.data.nextCursor).toBe('pos-1');
|
||||
});
|
||||
expect(result.data.positions[1]).toMatchObject({
|
||||
instrumentUid: 'uid-2',
|
||||
ticker: null,
|
||||
name: null,
|
||||
|
||||
it('paginates using cursor', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary).mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 },
|
||||
positions: [
|
||||
{
|
||||
figi: 'f1',
|
||||
instrumentUid: 'u1',
|
||||
positionUid: 'p1',
|
||||
quantity: { units: '10', nano: 0 },
|
||||
},
|
||||
{
|
||||
figi: 'f2',
|
||||
instrumentUid: 'u2',
|
||||
positionUid: 'p2',
|
||||
quantity: { units: '20', nano: 0 },
|
||||
},
|
||||
{
|
||||
figi: 'f3',
|
||||
instrumentUid: 'u3',
|
||||
positionUid: 'p3',
|
||||
quantity: { units: '30', nano: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
const result = await service.getPositions('acc-1', 'p1', 1);
|
||||
|
||||
expect(result.data.items).toHaveLength(1);
|
||||
expect(result.data.items[0].positionUid).toBe('p2');
|
||||
expect(result.data.nextCursor).toBe('p2');
|
||||
expect(result.data.hasNext).toBe(true);
|
||||
});
|
||||
|
||||
it('returns last page with hasNext=false', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary).mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 },
|
||||
positions: [
|
||||
{
|
||||
figi: 'f1',
|
||||
instrumentUid: 'u1',
|
||||
positionUid: 'p1',
|
||||
quantity: { units: '10', nano: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
const result = await service.getPositions('acc-1', undefined, 10);
|
||||
|
||||
expect(result.data.items).toHaveLength(1);
|
||||
expect(result.data.hasNext).toBe(false);
|
||||
expect(result.data.nextCursor).toBeNull();
|
||||
});
|
||||
|
||||
it('caches positions with cursor/limit in key and tbankPositionsTtl', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary).mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 },
|
||||
positions: [],
|
||||
});
|
||||
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
await service.getPositions('acc-1', 'some-cursor', 5);
|
||||
|
||||
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||
'tbank:positions',
|
||||
['acc-1', 'some-cursor', '5'],
|
||||
expect.any(Function),
|
||||
'tbankPositionsTtl',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,6 +21,7 @@ describe('T-Bank configuration', () => {
|
||||
expect(config.tbank.baseUrl).toBe('invest-public-api.tbank.ru:443');
|
||||
expect(config.tbank.rateLimitPerSecond).toBe(5);
|
||||
expect(config.cache.tbankPortfolioTtl).toBe(60);
|
||||
expect(config.cache.tbankPositionsTtl).toBe(60);
|
||||
});
|
||||
|
||||
it('reads T-Bank token and TTL overrides from environment', () => {
|
||||
@ -29,6 +30,7 @@ describe('T-Bank configuration', () => {
|
||||
process.env.T_BANK_CA_CERT_PATH = '/tmp/tbank-root-ca.pem';
|
||||
process.env.T_BANK_RATE_LIMIT_PER_SECOND = '2';
|
||||
process.env.CACHE_TBANK_ACCOUNTS_TTL = '120';
|
||||
process.env.CACHE_TBANK_POSITIONS_TTL = '45';
|
||||
|
||||
const config = configuration();
|
||||
|
||||
@ -37,5 +39,6 @@ describe('T-Bank configuration', () => {
|
||||
expect(config.tbank.caCertPath).toBe('/tmp/tbank-root-ca.pem');
|
||||
expect(config.tbank.rateLimitPerSecond).toBe(2);
|
||||
expect(config.cache.tbankAccountsTtl).toBe(120);
|
||||
expect(config.cache.tbankPositionsTtl).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user