import type { Server } from 'node:http'; import { status } from '@grpc/grpc-js'; import { INestApplication } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Test } from '@nestjs/testing'; 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'; export const onlineUserId = 'online-reader'; export const onlineToken = 'synthetic-online-token'; export const rawUpstreamText = 'raw upstream refusal text'; export const refusals = [status.UNAUTHENTICATED, status.PERMISSION_DENIED]; // Real TbankClient with only the transport stubbed: the gRPC status goes through its real error mapping. export async function bootOnlineApp(rpc: jest.Mock) { const upstream = Object.assign(Object.create(TbankClient.prototype) as TbankClient, { logger: { event: jest.fn() }, queue: { run: (operation: () => Promise) => operation(), size: 0 }, request: rpc, }); const module = await Test.createTestingModule({ imports: [AppModule] }) .overrideProvider(TbankClient) .useValue(upstream) .overrideProvider(TbankHourlyScheduler) .useValue({ tick: jest.fn() }) .compile(); const app: INestApplication = module.createNestApplication(); configureApp(app); await app.init(); await app.listen(0, '127.0.0.1'); const prisma = app.get(PrismaService); await prisma.user.create({ data: { id: onlineUserId, email: 'online@example.invalid', passwordHash: 'synthetic' }, }); const connection = await prisma.tbankConnection.create({ data: { userId: onlineUserId, encryptedToken: app.get(TbankTokenService).encrypt(onlineToken), connectedAt: new Date(), }, }); const bearer = `Bearer ${new JwtService().sign( { sub: onlineUserId, type: 'access' }, { secret: process.env.JWT_SECRET }, )}`; return { app, bearer, connection, prisma }; } // Healthy answers for every RPC an online entrypoint uses; `at` picks the single RPC that fails. export function failUpstream(rpc: jest.Mock, kind: 'bond' | 'share', code: number, at?: string) { const instrument = { apiTradeAvailableFlag: true, assetUid: 'asset-uid', instrumentKind: kind === 'bond' ? 'INSTRUMENT_TYPE_BOND' : 'INSTRUMENT_TYPE_SHARE', ticker: 'SBER', uid: 'e6123145-9665-43e0-8413-cd61b8aa9b13', }; const healthy = new Map([ ['bondBy', { instrument }], ['findInstrument', { instruments: [instrument] }], ['getAssetBy', { asset: { description: 'description' } }], ['getAssetFundamentals', { fundamentals: [] }], ['getLastPrices', { lastPrices: [] }], ['shareBy', { instrument }], ]); const failure = Object.assign(new Error('grpc failure'), { code, details: `${rawUpstreamText} Bearer ${onlineToken}`, }); rpc.mockReset(); rpc.mockImplementation((_token: string, _client: unknown, method: string) => !at || method === at ? Promise.reject(failure) : Promise.resolve(healthy.get(method) ?? {}), ); } // Safe public error: stable code only, no upstream text, no credentials. export function expectSafeBody(body: unknown, status: number, code: string) { expect(body).toEqual({ code, status }); expect(JSON.stringify(body)).not.toContain(rawUpstreamText); expect(JSON.stringify(body)).not.toContain(onlineToken); }