fix: translate permanent invalid-token errors on online entrypoints (WealthLedger/backend#59)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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 }],
|
||||
['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);
|
||||
}
|
||||
Reference in New Issue
Block a user