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 { TbankHourlyScheduler } from '../src/modules/investments/tbank-sync/tbank-hourly.scheduler'; import { PrismaService } from '../src/prisma/prisma.service'; describe('All accounts HTTP boundary', () => { let app: INestApplication; 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) .overrideProvider(TbankHourlyScheduler) .useValue({ tick: jest.fn() }) .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}`, ) .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}`, ) .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}`, ) .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' }); }); });