Add authenticated all-account portfolio, operations, income and event endpoints. Preserve source accounts, exact currency totals and scoped cursor pagination.
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { ConflictException, INestApplication } from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { Test } from '@nestjs/testing';
|
|
import request from 'supertest';
|
|
import type { Server } from 'node:http';
|
|
import { AppModule } from '../src/app.module';
|
|
import { configureApp } from '../src/main';
|
|
import { TbankAccessService } from '../src/tbank/tbank-access.service';
|
|
|
|
describe('All accounts HTTP boundary', () => {
|
|
let app: INestApplication<Server>;
|
|
const withSupportedAccounts = jest.fn();
|
|
const token = new JwtService().sign(
|
|
{ sub: 'account-owner', type: 'access' },
|
|
{ secret: process.env.JWT_SECRET },
|
|
);
|
|
const paths = [
|
|
'portfolio',
|
|
'operations',
|
|
'income-summary',
|
|
'income-analytics',
|
|
'events',
|
|
];
|
|
|
|
beforeAll(async () => {
|
|
const module = await Test.createTestingModule({ imports: [AppModule] })
|
|
.overrideProvider(TbankAccessService)
|
|
.useValue({ withSupportedAccounts })
|
|
.compile();
|
|
app = module.createNestApplication();
|
|
configureApp(app);
|
|
await app.init();
|
|
});
|
|
beforeEach(() => {
|
|
withSupportedAccounts.mockReset();
|
|
withSupportedAccounts.mockImplementation(
|
|
(_user: string, handler: (context: unknown) => unknown) =>
|
|
handler({ token: 'unused', generation: 'first', accounts: [] }),
|
|
);
|
|
});
|
|
afterAll(() => app.close());
|
|
|
|
it.each(paths)('requires authentication for /all/%s', async (path) => {
|
|
await request(app.getHttpServer())
|
|
.get(`/api/v1/brokerage-accounts/all/${path}`)
|
|
.expect(401);
|
|
expect(withSupportedAccounts).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(paths)(
|
|
'routes /all/%s to the aggregate with verified identity',
|
|
async (path) => {
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/v1/brokerage-accounts/all/${path}`)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
expect(withSupportedAccounts).toHaveBeenCalledWith(
|
|
'account-owner',
|
|
expect.any(Function),
|
|
);
|
|
expect(response.body).not.toHaveProperty('token');
|
|
expect(response.body).not.toHaveProperty('generation');
|
|
if (path === 'portfolio')
|
|
expect(response.body).toMatchObject({ positions: [] });
|
|
else expect(response.body).toMatchObject({ accountId: 'all' });
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
'operations?limit=0',
|
|
'operations?limit=1001',
|
|
'operations?categories=unknown',
|
|
'operations?from=not-a-date',
|
|
'income-analytics?from=2026-08-01',
|
|
'income-analytics?from=2026-08-31&to=2026-08-01',
|
|
])('validates /all/%s in the configured pipeline', async (path) => {
|
|
await request(app.getHttpServer())
|
|
.get(`/api/v1/brokerage-accounts/all/${path}`)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(400);
|
|
});
|
|
|
|
it('returns a recoverable code for an expired or foreign continuation', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/brokerage-accounts/all/operations?cursor=unknown')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(400);
|
|
expect(response.body).toMatchObject({ code: 'INVALID_OPERATIONS_CURSOR' });
|
|
});
|
|
|
|
it('preserves connection errors without a partial success response', async () => {
|
|
withSupportedAccounts.mockRejectedValue(
|
|
new ConflictException({ code: 'TBANK_NOT_CONNECTED' }),
|
|
);
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/brokerage-accounts/all/portfolio')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(409);
|
|
expect(response.body).toMatchObject({ code: 'TBANK_NOT_CONNECTED' });
|
|
});
|
|
});
|