Filter non-RUB data at ingest and collapse per-currency collections into a single RUB Money. Drop currency from TbankPortfolioValuePoint, remove NON_RUB_ACTIVITY and the screener currency filter. BREAKING CHANGE: per-currency API fields replaced with single RUB Money.
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
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<unknown>) => 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<Server> = 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<string, unknown>([
|
|
['bondBy', { instrument: { ...instrument, currency: 'rub' } }],
|
|
['findInstrument', { instruments: [instrument] }],
|
|
['getAssetBy', { asset: { description: 'description' } }],
|
|
['getAssetFundamentals', { fundamentals: [] }],
|
|
['getLastPrices', { lastPrices: [] }],
|
|
['shareBy', { instrument: { ...instrument, currency: 'rub' } }],
|
|
]);
|
|
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);
|
|
}
|