159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
import { ServiceUnavailableException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { ChannelCredentials, ClientUnaryCall, Metadata, ServiceError, status } from '@grpc/grpc-js';
|
|
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { TBankClientService } from './tbank-client.service';
|
|
|
|
describe('TBankClientService', () => {
|
|
const unaryCall = {
|
|
cancel: vi.fn(),
|
|
getPeer: vi.fn(),
|
|
getAuthContext: vi.fn(),
|
|
} as unknown as ClientUnaryCall;
|
|
|
|
const config = {
|
|
get: vi.fn((key: string, fallback?: unknown) => {
|
|
const values: Record<string, unknown> = {
|
|
'app.tbank.token': 'token-1',
|
|
'app.tbank.appName': 'ksv741.moex-vibe',
|
|
'app.tbank.rateLimitPerSecond': 5,
|
|
'app.tbank.requestTimeoutMs': 10000,
|
|
};
|
|
|
|
return values[key] ?? fallback;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
|
|
it('builds redacted authorization metadata', () => {
|
|
const service = new TBankClientService(config);
|
|
const metadata = service.createMetadata();
|
|
|
|
expect(metadata.get('Authorization')).toEqual(['Bearer token-1']);
|
|
expect(metadata.get('x-app-name')).toEqual(['ksv741.moex-vibe']);
|
|
expect(service.redactMetadata(metadata)).toEqual({
|
|
Authorization: '<redacted>',
|
|
'x-app-name': 'ksv741.moex-vibe',
|
|
});
|
|
});
|
|
|
|
it('creates service clients from vendored proto contracts', () => {
|
|
const service = new TBankClientService(config);
|
|
|
|
expect(() => service.getServiceClient('UsersService')).not.toThrow();
|
|
});
|
|
|
|
it('creates grpc SSL credentials with configured custom CA certificate', () => {
|
|
const caPath = join(mkdtempSync(join(tmpdir(), 'tbank-ca-')), 'root.pem');
|
|
writeFileSync(caPath, '-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----\n');
|
|
const createSslSpy = vi.spyOn(ChannelCredentials, 'createSsl');
|
|
const caConfig = {
|
|
get: vi.fn((key: string, fallback?: unknown) => {
|
|
const values: Record<string, unknown> = {
|
|
'app.tbank.token': 'token-1',
|
|
'app.tbank.caCertPath': caPath,
|
|
};
|
|
|
|
return values[key] ?? fallback;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
const service = new TBankClientService(caConfig);
|
|
|
|
service.getServiceClient('UsersService');
|
|
|
|
const [rootCerts] = createSslSpy.mock.calls.at(-1) ?? [];
|
|
expect(Buffer.isBuffer(rootCerts)).toBe(true);
|
|
expect((rootCerts as Buffer).toString('utf8')).toContain('test-ca');
|
|
createSslSpy.mockRestore();
|
|
});
|
|
|
|
it('throws integration unavailable when token is missing', async () => {
|
|
const missingConfig = {
|
|
get: vi.fn((key: string, fallback?: unknown) =>
|
|
key === 'app.tbank.token' ? '' : (fallback as unknown),
|
|
),
|
|
} as unknown as ConfigService;
|
|
const service = new TBankClientService(missingConfig);
|
|
|
|
await expect(
|
|
service.callUnary(
|
|
'UsersService/GetAccounts',
|
|
(_request, _metadata, _options, callback) => {
|
|
callback(null, {});
|
|
return unaryCall;
|
|
},
|
|
{},
|
|
),
|
|
).rejects.toThrow(ServiceUnavailableException);
|
|
});
|
|
|
|
it('wraps grpc errors with status code and tracking id', async () => {
|
|
const service = new TBankClientService(config);
|
|
const error = Object.assign(new Error('Too many requests'), {
|
|
code: status.RESOURCE_EXHAUSTED,
|
|
details: 'Too many requests',
|
|
metadata: new Metadata(),
|
|
}) as ServiceError;
|
|
error.metadata.set('x-tracking-id', 'tracking-1');
|
|
|
|
await expect(
|
|
service.callUnary(
|
|
'OperationsService/GetPortfolio',
|
|
(_request, _metadata, _options, callback) => {
|
|
callback(error, null);
|
|
return unaryCall;
|
|
},
|
|
{},
|
|
),
|
|
).rejects.toMatchObject({
|
|
response: expect.objectContaining({
|
|
message: expect.stringContaining('T-Bank upstream error'),
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('starts grpc deadline when queued call actually executes', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-06-17T07:00:00.000Z'));
|
|
|
|
try {
|
|
const queuedConfig = {
|
|
get: vi.fn((key: string, fallback?: unknown) => {
|
|
const values: Record<string, unknown> = {
|
|
'app.tbank.token': 'token-1',
|
|
'app.tbank.rateLimitPerSecond': 1,
|
|
'app.tbank.requestTimeoutMs': 1000,
|
|
};
|
|
|
|
return values[key] ?? fallback;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
const service = new TBankClientService(queuedConfig);
|
|
const deadlines: number[] = [];
|
|
const startedAt: number[] = [];
|
|
const method = vi.fn((_request, _metadata, options, callback) => {
|
|
deadlines.push((options.deadline as Date).getTime());
|
|
startedAt.push(Date.now());
|
|
callback(null, {});
|
|
return unaryCall;
|
|
});
|
|
|
|
const first = service.callUnary('UsersService/GetAccounts', method, {});
|
|
const second = service.callUnary('UsersService/GetAccounts', method, {});
|
|
|
|
await first;
|
|
expect(startedAt).toEqual([Date.parse('2026-06-17T07:00:00.000Z')]);
|
|
|
|
vi.setSystemTime(new Date('2026-06-17T07:00:01.000Z'));
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await second;
|
|
|
|
expect(startedAt[1]).toBeGreaterThan(startedAt[0]);
|
|
expect(deadlines).toEqual(startedAt.map((started) => started + 1000));
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|