163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
NotFoundException,
|
|
ServiceUnavailableException,
|
|
} from '@nestjs/common';
|
|
|
|
jest.mock('../../prisma/prisma.service', () => ({
|
|
PrismaService: class PrismaService {},
|
|
}));
|
|
|
|
import type { TbankAccountContext } from './tbank-access.service';
|
|
import { TbankAccessService } from './tbank-access.service';
|
|
import { TbankTokenService } from './tbank-token.service';
|
|
|
|
describe('TbankAccessService', () => {
|
|
const prisma = { tbankConnection: { findUnique: jest.fn() } };
|
|
const token = {
|
|
decrypt: jest.fn(),
|
|
translateInvalidToken: jest.fn((operation: () => Promise<unknown>) =>
|
|
new TbankTokenService({
|
|
getOrThrow: () => 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
|
} as never).translateInvalidToken(operation),
|
|
),
|
|
};
|
|
const tbank = { getOpenAccounts: jest.fn() };
|
|
const service = new TbankAccessService(
|
|
prisma as never,
|
|
token as never,
|
|
tbank as never,
|
|
);
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
prisma.tbankConnection.findUnique.mockResolvedValue({
|
|
encryptedToken: 'encrypted',
|
|
});
|
|
token.decrypt.mockReturnValue('plain-token');
|
|
tbank.getOpenAccounts.mockResolvedValue([
|
|
{
|
|
id: 'account',
|
|
type: 'ACCOUNT_TYPE_TINKOFF',
|
|
name: ' Основной ',
|
|
openedDate: { seconds: '1723507200' },
|
|
},
|
|
{ id: 'debit', type: 'ACCOUNT_TYPE_DEBIT', name: 'Debit' },
|
|
]);
|
|
});
|
|
|
|
it('lists supported accounts through the saved decrypted token', async () => {
|
|
await expect(service.listSupportedAccounts('user')).resolves.toEqual([
|
|
{
|
|
id: 'account',
|
|
type: 'BROKERAGE',
|
|
name: 'Основной',
|
|
openedOn: '2024-08-13',
|
|
},
|
|
]);
|
|
|
|
expect(prisma.tbankConnection.findUnique).toHaveBeenCalledWith({
|
|
where: { userId: 'user' },
|
|
});
|
|
expect(token.decrypt).toHaveBeenCalledWith('encrypted');
|
|
expect(tbank.getOpenAccounts).toHaveBeenCalledWith('plain-token');
|
|
});
|
|
|
|
it('rejects missing connections before calling T-Bank', async () => {
|
|
prisma.tbankConnection.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.listSupportedAccounts('user')).rejects.toEqual(
|
|
expect.any(ConflictException),
|
|
);
|
|
expect(tbank.getOpenAccounts).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('resolves the persisted connection generation without an upstream account call', async () => {
|
|
const context = await service.withConnection('user', (value) => value);
|
|
|
|
expect(context).toEqual({
|
|
token: 'plain-token',
|
|
generation: expect.any(String) as string,
|
|
});
|
|
expect(tbank.getOpenAccounts).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('translates invalid tokens and preserves unavailable upstream errors', async () => {
|
|
tbank.getOpenAccounts.mockRejectedValueOnce(
|
|
new Error('INVALID_TBANK_TOKEN'),
|
|
);
|
|
await expect(service.listSupportedAccounts('user')).rejects.toEqual(
|
|
expect.any(BadRequestException),
|
|
);
|
|
|
|
const unavailable = new ServiceUnavailableException({
|
|
code: 'TBANK_UNAVAILABLE',
|
|
});
|
|
tbank.getOpenAccounts.mockRejectedValueOnce(unavailable);
|
|
await expect(service.listSupportedAccounts('user')).rejects.toBe(
|
|
unavailable,
|
|
);
|
|
});
|
|
|
|
it('wraps account-scoped handlers with resolved account access', async () => {
|
|
const handler = jest.fn((context: TbankAccountContext) =>
|
|
Promise.resolve({
|
|
resolvedToken: context.token,
|
|
generation: context.generation,
|
|
accountId: context.account.id,
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
service.withAccount('user', 'account', handler),
|
|
).resolves.toEqual({
|
|
resolvedToken: 'plain-token',
|
|
generation: expect.any(String) as string,
|
|
accountId: 'account',
|
|
});
|
|
});
|
|
|
|
it('derives a stable opaque generation from each persisted encrypted payload', async () => {
|
|
const generation = () =>
|
|
service.withAccount('user', 'account', ({ generation: value }) =>
|
|
Promise.resolve(value),
|
|
);
|
|
prisma.tbankConnection.findUnique
|
|
.mockResolvedValueOnce({ encryptedToken: 'encrypted:first-nonce' })
|
|
.mockResolvedValueOnce({ encryptedToken: 'encrypted:first-nonce' })
|
|
.mockResolvedValueOnce({ encryptedToken: 'encrypted:second-nonce' });
|
|
|
|
const first = await generation();
|
|
const sameRecord = await generation();
|
|
const replaced = await generation();
|
|
|
|
expect(sameRecord).toBe(first);
|
|
expect(replaced).not.toBe(first);
|
|
expect(first).not.toContain('plain-token');
|
|
expect(first).not.toContain('encrypted:first-nonce');
|
|
});
|
|
|
|
it('rejects inaccessible accounts before running the handler', async () => {
|
|
const handler = jest.fn();
|
|
|
|
await expect(
|
|
service.withAccount('user', 'missing', handler),
|
|
).rejects.toEqual(expect.any(NotFoundException));
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('translates invalid-token handler failures and preserves unrelated handler failures', async () => {
|
|
await expect(
|
|
service.withAccount('user', 'account', () =>
|
|
Promise.reject(new Error('INVALID_TBANK_TOKEN')),
|
|
),
|
|
).rejects.toEqual(expect.any(BadRequestException));
|
|
|
|
const failure = new Error('source failed');
|
|
await expect(
|
|
service.withAccount('user', 'account', () => Promise.reject(failure)),
|
|
).rejects.toBe(failure);
|
|
});
|
|
});
|