123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import type { Server } from 'node:http';
|
|
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { Test } from '@nestjs/testing';
|
|
import request from 'supertest';
|
|
|
|
import { AppModule } from '../src/app.module';
|
|
import { TbankTokenService } from '../src/integrations/tbank/tbank-token.service';
|
|
import { TbankClient } from '../src/integrations/tbank/tbank.client';
|
|
import { configureApp } from '../src/main';
|
|
import { PrismaService } from '../src/prisma/prisma.service';
|
|
|
|
describe('All accounts HTTP boundary', () => {
|
|
let app: INestApplication<Server>;
|
|
let prisma: PrismaService;
|
|
const upstream = { getOpenAccounts: 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(TbankClient)
|
|
.useValue(upstream)
|
|
.compile();
|
|
app = module.createNestApplication();
|
|
configureApp(app);
|
|
await app.init();
|
|
await app.listen(0, '127.0.0.1');
|
|
prisma = app.get(PrismaService);
|
|
await prisma.user.create({
|
|
data: {
|
|
id: 'account-owner',
|
|
email: 'all@example.invalid',
|
|
passwordHash: 'synthetic',
|
|
},
|
|
});
|
|
});
|
|
beforeEach(async () => {
|
|
upstream.getOpenAccounts.mockReset().mockResolvedValue([]);
|
|
await prisma.tbankConnection.deleteMany();
|
|
await prisma.tbankConnection.create({
|
|
data: {
|
|
userId: 'account-owner',
|
|
encryptedToken: app.get(TbankTokenService).encrypt('synthetic'),
|
|
connectedAt: new Date(),
|
|
membershipAt: new Date(),
|
|
membershipRevision: 1,
|
|
},
|
|
});
|
|
});
|
|
afterAll(() => app.close());
|
|
|
|
it.each(paths)('requires authentication for /all/%s', async (path) => {
|
|
await request(app.getHttpServer())
|
|
.get(
|
|
`/api/v1/brokerage-accounts/all/${path}${path.includes('?') ? '&' : '?'}sync=observe`,
|
|
)
|
|
.expect(401);
|
|
expect(upstream.getOpenAccounts).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}${path.includes('?') ? '&' : '?'}sync=observe`,
|
|
)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
expect(response.body).not.toHaveProperty('token');
|
|
expect(response.body).not.toHaveProperty('generation');
|
|
expect(response.body).toMatchObject({
|
|
meta: { availability: 'available', completeness: 'complete' },
|
|
...(path === 'portfolio' ? { data: { positions: [] } } : {}),
|
|
});
|
|
},
|
|
);
|
|
|
|
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}${path.includes('?') ? '&' : '?'}sync=observe`,
|
|
)
|
|
.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 () => {
|
|
await prisma.tbankConnection.deleteMany();
|
|
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' });
|
|
});
|
|
});
|