205 lines
6.5 KiB
TypeScript
205 lines
6.5 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
ServiceUnavailableException,
|
|
} from '@nestjs/common';
|
|
|
|
jest.mock('../prisma/prisma.service', () => ({
|
|
PrismaService: class PrismaService {},
|
|
}));
|
|
|
|
import { BrokerageAccountsService } from './brokerage-accounts.service';
|
|
import { TbankAccessService } from './tbank-access.service';
|
|
import { TbankTokenService } from './tbank-token.service';
|
|
|
|
describe('BrokerageAccountsService', () => {
|
|
const encryptionKey = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
|
|
const prisma = { tbankConnection: { findUnique: jest.fn() } };
|
|
const config = { getOrThrow: jest.fn(() => encryptionKey) };
|
|
const tbank = {
|
|
getOpenAccounts: jest.fn(),
|
|
getPortfolio: jest.fn(),
|
|
getPositions: jest.fn(),
|
|
};
|
|
const tokens = new TbankTokenService(config as never);
|
|
const access = new TbankAccessService(
|
|
prisma as never,
|
|
tokens,
|
|
tbank as never,
|
|
);
|
|
const service = new BrokerageAccountsService(access, tbank as never);
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
tbank.getPortfolio.mockResolvedValue({ positions: [] });
|
|
tbank.getPositions.mockResolvedValue({ money: [] });
|
|
});
|
|
|
|
it('does not call T-Bank without a connection', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.list('user-1')).rejects.toEqual(
|
|
expect.any(ConflictException),
|
|
);
|
|
expect(tbank.getOpenAccounts).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('normalizes supported accounts and ignores unsupported ones', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockResolvedValue([
|
|
{
|
|
id: 'brokerage',
|
|
type: 'ACCOUNT_TYPE_TINKOFF',
|
|
name: ' Основной ',
|
|
openedDate: { seconds: '1723507200' },
|
|
},
|
|
{
|
|
id: 'debit',
|
|
type: 'ACCOUNT_TYPE_DEBIT',
|
|
name: 'Дебетовый',
|
|
},
|
|
{
|
|
id: 'iis',
|
|
type: 'ACCOUNT_TYPE_TINKOFF_IIS',
|
|
name: ' ',
|
|
},
|
|
]);
|
|
|
|
await expect(service.list('user-1')).resolves.toEqual([
|
|
{
|
|
id: 'brokerage',
|
|
type: 'BROKERAGE',
|
|
name: 'Основной',
|
|
openedOn: '2024-08-13',
|
|
portfolioSummary: { totalsByCurrency: [] },
|
|
},
|
|
{
|
|
id: 'iis',
|
|
type: 'INDIVIDUAL_INVESTMENT',
|
|
name: 'ИИС',
|
|
openedOn: null,
|
|
portfolioSummary: { totalsByCurrency: [] },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('keeps upstream order and adds exact summaries without loading metadata', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockResolvedValue([
|
|
{ id: 'first', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Первый' },
|
|
{ id: 'second', type: 'ACCOUNT_TYPE_TINKOFF_IIS', name: 'Второй' },
|
|
]);
|
|
tbank.getPortfolio
|
|
.mockResolvedValueOnce({
|
|
positions: [
|
|
{
|
|
quantity: { units: 2 },
|
|
currentPrice: { currency: 'rub', units: 3 },
|
|
},
|
|
],
|
|
})
|
|
.mockResolvedValueOnce({ positions: [] });
|
|
tbank.getPositions
|
|
.mockResolvedValueOnce({ money: [{ currency: 'RUB', nano: 500000000 }] })
|
|
.mockResolvedValueOnce({ money: [{ currency: 'USD', units: 4 }] });
|
|
|
|
await expect(service.list('user-1')).resolves.toEqual([
|
|
{
|
|
id: 'first',
|
|
type: 'BROKERAGE',
|
|
name: 'Первый',
|
|
openedOn: null,
|
|
portfolioSummary: {
|
|
totalsByCurrency: [{ currency: 'RUB', value: '6.5' }],
|
|
},
|
|
},
|
|
{
|
|
id: 'second',
|
|
type: 'INDIVIDUAL_INVESTMENT',
|
|
name: 'Второй',
|
|
openedOn: null,
|
|
portfolioSummary: {
|
|
totalsByCurrency: [{ currency: 'USD', value: '4' }],
|
|
},
|
|
},
|
|
]);
|
|
expect(tbank.getPortfolio).toHaveBeenCalledWith('token', 'first');
|
|
expect(tbank.getPositions).toHaveBeenCalledWith('token', 'second');
|
|
});
|
|
|
|
it('localizes an unavailable summary without suppressing another account', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockResolvedValue([
|
|
{ id: 'unavailable', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Первый' },
|
|
{ id: 'available', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Второй' },
|
|
]);
|
|
tbank.getPortfolio
|
|
.mockRejectedValueOnce(
|
|
new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' }),
|
|
)
|
|
.mockResolvedValueOnce({ positions: [] });
|
|
tbank.getPositions.mockResolvedValue({ money: [] });
|
|
|
|
await expect(service.list('user-1')).resolves.toMatchObject([
|
|
{ id: 'unavailable', portfolioSummary: null },
|
|
{ id: 'available', portfolioSummary: { totalsByCurrency: [] } },
|
|
]);
|
|
});
|
|
|
|
it('does not mask an invalid token when the other summary input is unavailable', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockResolvedValue([
|
|
{ id: 'account', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Основной' },
|
|
]);
|
|
tbank.getPortfolio.mockRejectedValue(
|
|
new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' }),
|
|
);
|
|
tbank.getPositions.mockRejectedValue(new Error('INVALID_TBANK_TOKEN'));
|
|
|
|
await expect(service.list('user-1')).rejects.toEqual(
|
|
expect.any(BadRequestException),
|
|
);
|
|
});
|
|
|
|
it('retains the connection when T-Bank rejects its token', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockRejectedValue(new Error('INVALID_TBANK_TOKEN'));
|
|
|
|
await expect(service.list('user-1')).rejects.toEqual(
|
|
expect.any(BadRequestException),
|
|
);
|
|
});
|
|
|
|
it('returns an empty list from T-Bank unchanged', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockResolvedValue([]);
|
|
|
|
await expect(service.list('user-1')).resolves.toEqual([]);
|
|
});
|
|
|
|
it('propagates an unavailable T-Bank response', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: tokens.encrypt('token'),
|
|
});
|
|
tbank.getOpenAccounts.mockRejectedValue(
|
|
new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' }),
|
|
);
|
|
|
|
await expect(service.list('user-1')).rejects.toEqual(
|
|
expect.any(ServiceUnavailableException),
|
|
);
|
|
});
|
|
});
|