Files
backend/test/all-accounts.e2e-spec.ts
T
codex-bot 9e4f9d9cab
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 12s
Backend verify / verify (push) Failing after 5m59s
test: isolate HTTP suites from scheduler and bank-card case order (WealthLedger/backend#65) (#100)
2026-10-15 11:50:47 +03:00

126 lines
4.3 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 { 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<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)
.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}${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' });
});
});