refactor(tbank): centralize access rules

This commit is contained in:
2026-08-25 12:06:41 +03:00
parent 5375e5598a
commit 130b303ec1
18 changed files with 573 additions and 327 deletions
+21 -24
View File
@@ -4,41 +4,41 @@ import {
ServiceUnavailableException,
} from '@nestjs/common';
jest.mock('../prisma/prisma.service', () => ({
PrismaService: class PrismaService {},
jest.mock('./tbank-access.service', () => ({
TbankAccessService: class TbankAccessService {},
}));
import { BrokerageOperationsService } from './brokerage-operations.service';
import { TbankCrypto } from './tbank.crypto';
import type { TbankAccountContext } from './tbank-access.service';
describe('BrokerageOperationsService', () => {
const key = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
const prisma = { tbankConnection: { findUnique: jest.fn() } };
const config = { getOrThrow: jest.fn(() => key) };
const access = { withAccount: jest.fn() };
const tbank = {
getOpenAccounts: jest.fn(),
getOperationsByCursor: jest.fn(),
};
const requestContext = { set: jest.fn() };
const service = new BrokerageOperationsService(
prisma as never,
config as never,
access as never,
tbank as never,
requestContext as never,
);
const crypto = TbankCrypto.fromBase64(key);
let operationsRequest: unknown;
beforeEach(() => {
jest.clearAllMocks();
tbank.getOpenAccounts.mockReset();
access.withAccount.mockReset();
tbank.getOperationsByCursor.mockReset();
prisma.tbankConnection.findUnique.mockResolvedValue({
encryptedToken: crypto.encrypt('token'),
});
tbank.getOpenAccounts.mockResolvedValue([
{ id: 'account', type: 'ACCOUNT_TYPE_TINKOFF' },
]);
access.withAccount.mockImplementation(
(
_userId: string,
_accountId: string,
handler: (context: TbankAccountContext) => Promise<unknown>,
) =>
handler({
token: 'token',
account: { id: 'account', type: 'ACCOUNT_TYPE_TINKOFF', name: '' },
}),
);
tbank.getOperationsByCursor.mockImplementation(
(_token: string, request: unknown) => {
operationsRequest = request;
@@ -116,7 +116,7 @@ describe('BrokerageOperationsService', () => {
});
it('rejects inaccessible accounts before reading operations', async () => {
tbank.getOpenAccounts.mockResolvedValue([]);
access.withAccount.mockRejectedValueOnce(new NotFoundException());
await expect(service.get('user', 'account', {})).rejects.toEqual(
expect.any(NotFoundException),
@@ -125,15 +125,12 @@ describe('BrokerageOperationsService', () => {
});
it('retains existing T-Bank error semantics', async () => {
tbank.getOpenAccounts.mockRejectedValueOnce(
new Error('INVALID_TBANK_TOKEN'),
access.withAccount.mockRejectedValueOnce(
new BadRequestException({ code: 'INVALID_TBANK_TOKEN' }),
);
await expect(service.get('user', 'account', {})).rejects.toEqual(
expect.any(BadRequestException),
);
tbank.getOpenAccounts.mockResolvedValueOnce([
{ id: 'account', type: 'ACCOUNT_TYPE_TINKOFF' },
]);
tbank.getOperationsByCursor.mockRejectedValueOnce(
new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' }),
);
@@ -158,7 +155,7 @@ describe('BrokerageOperationsService', () => {
it('does not let a cached page bypass account ownership checks', async () => {
const query = { from: '2025-01-01', to: '2025-01-31' };
await service.get('user', 'account', query);
tbank.getOpenAccounts.mockResolvedValueOnce([]);
access.withAccount.mockRejectedValueOnce(new NotFoundException());
await expect(service.get('user', 'account', query)).rejects.toEqual(
expect.any(NotFoundException),