test: make backend service specs deterministic
This commit is contained in:
parent
c994b6a2fb
commit
974c83d67e
@ -1,41 +1,147 @@
|
|||||||
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { BondsService } from './bonds.service';
|
import { BondsService } from './bonds.service';
|
||||||
import { MoexClientService } from '../moex-client/moex-client.service';
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import configuration from '../../config/configuration';
|
|
||||||
|
|
||||||
describe('BondsService', () => {
|
describe('BondsService', () => {
|
||||||
let service: BondsService;
|
let service: BondsService;
|
||||||
|
let moexClient: Pick<MoexClientService, 'getBondData' | 'getBondMarketData'>;
|
||||||
|
let cache: Pick<CacheService, 'getOrFetch'>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
moexClient = {
|
||||||
|
getBondData: vi.fn(),
|
||||||
|
getBondMarketData: vi.fn(),
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
getOrFetch: vi.fn(async (_keyPrefix, _keyParts, fetchFn) => ({
|
||||||
|
data: await fetchFn(),
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
|
||||||
providers: [
|
providers: [
|
||||||
BondsService,
|
BondsService,
|
||||||
MoexClientService,
|
{ provide: MoexClientService, useValue: moexClient },
|
||||||
{
|
{ provide: CacheService, useValue: cache },
|
||||||
provide: 'CACHE_MANAGER',
|
|
||||||
useValue: {
|
|
||||||
get: () => undefined,
|
|
||||||
set: () => Promise.resolve(),
|
|
||||||
del: () => Promise.resolve(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CacheService,
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<BondsService>(BondsService);
|
service = module.get<BondsService>(BondsService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('returns normalized SU26238RMFS5 bond spec and market data without live MOEX dependency', async () => {
|
||||||
expect(service).toBeDefined();
|
vi.mocked(moexClient.getBondData).mockResolvedValue({
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
boardid: 'TQCB',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
prevWaprice: 73.2,
|
||||||
|
yieldAtPrevWaprice: 14.1,
|
||||||
|
couponValue: 35.4,
|
||||||
|
nextCoupon: '2026-06-24',
|
||||||
|
accruedInt: 34.1,
|
||||||
|
prevPrice: 73,
|
||||||
|
lotSize: 1,
|
||||||
|
faceValue: 1000,
|
||||||
|
matDate: '2041-05-15',
|
||||||
|
couponPeriod: 182,
|
||||||
|
issueSize: 150000000,
|
||||||
|
isin: 'RU000A1038V6',
|
||||||
|
couponPercent: 7.1,
|
||||||
|
offerDate: null,
|
||||||
|
buybackDate: null,
|
||||||
|
bondType: 'ofz',
|
||||||
|
bondSubType: 'fixed',
|
||||||
|
listLevel: 1,
|
||||||
|
});
|
||||||
|
vi.mocked(moexClient.getBondMarketData).mockResolvedValue({
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
bid: 72.9,
|
||||||
|
offer: 73.1,
|
||||||
|
open: 72.8,
|
||||||
|
low: 72.5,
|
||||||
|
high: 73.4,
|
||||||
|
last: 73.05,
|
||||||
|
yield: 14.2,
|
||||||
|
waprice: 73,
|
||||||
|
yieldAtWaprice: 14.15,
|
||||||
|
duration: 2250,
|
||||||
|
volume: 10000,
|
||||||
|
value: 7305000,
|
||||||
|
numtrades: 450,
|
||||||
|
tradingStatus: 'T',
|
||||||
|
updateTime: '18:45:00',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getBond('SU26238RMFS5');
|
||||||
|
|
||||||
|
expect(cache.getOrFetch).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'bond',
|
||||||
|
['SU26238RMFS5'],
|
||||||
|
expect.any(Function),
|
||||||
|
'securityTtl',
|
||||||
|
);
|
||||||
|
expect(cache.getOrFetch).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'marketdata',
|
||||||
|
['bonds', 'SU26238RMFS5'],
|
||||||
|
expect.any(Function),
|
||||||
|
'marketDataTtl',
|
||||||
|
);
|
||||||
|
expect(moexClient.getBondData).toHaveBeenCalledWith('SU26238RMFS5');
|
||||||
|
expect(moexClient.getBondMarketData).toHaveBeenCalledWith('SU26238RMFS5');
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
isin: 'RU000A1038V6',
|
||||||
|
name: 'ОФЗ 26238',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
latName: null,
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 150000000,
|
||||||
|
faceValue: 1000,
|
||||||
|
faceUnit: 'RUB',
|
||||||
|
matDate: '2041-05-15',
|
||||||
|
couponValue: 35.4,
|
||||||
|
couponPercent: 7.1,
|
||||||
|
couponPeriod: 182,
|
||||||
|
nextCoupon: '2026-06-24',
|
||||||
|
accruedInt: 34.1,
|
||||||
|
bondType: 'ofz',
|
||||||
|
bondSubType: 'fixed',
|
||||||
|
offerDate: null,
|
||||||
|
buybackDate: null,
|
||||||
|
marketData: {
|
||||||
|
price: 73.05,
|
||||||
|
yieldToMaturity: 14.2,
|
||||||
|
duration: 2250,
|
||||||
|
accruedInt: 34.1,
|
||||||
|
couponValue: 35.4,
|
||||||
|
couponPercent: 7.1,
|
||||||
|
nextCouponDate: '2026-06-24',
|
||||||
|
open: 72.8,
|
||||||
|
high: 73.4,
|
||||||
|
low: 72.5,
|
||||||
|
volume: 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.data.marketData.updatedAt).toMatch(/T18:45:00$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return OFZ bond data for SU26207RMFS9', async () => {
|
it('throws NotFoundException when bond data is missing', async () => {
|
||||||
const result = await service.getBond('SU26207RMFS9');
|
vi.mocked(moexClient.getBondData).mockResolvedValue(null);
|
||||||
expect(result.data.secid).toBe('SU26207RMFS9');
|
|
||||||
expect(result.data.marketData).toBeDefined();
|
await expect(service.getBond('UNKNOWN')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
}, 15000);
|
expect(cache.getOrFetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(moexClient.getBondMarketData).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,40 +1,51 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { CandlesService } from './candles.service';
|
import { CandlesService } from './candles.service';
|
||||||
import { MoexClientService } from '../moex-client/moex-client.service';
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import configuration from '../../config/configuration';
|
|
||||||
import { CandleInterval } from './dto/candles-query.dto';
|
import { CandleInterval } from './dto/candles-query.dto';
|
||||||
|
|
||||||
describe('CandlesService', () => {
|
describe('CandlesService', () => {
|
||||||
let service: CandlesService;
|
let service: CandlesService;
|
||||||
|
let moexClient: Pick<MoexClientService, 'getCandles'>;
|
||||||
|
let cache: Pick<CacheService, 'getOrFetch'>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
moexClient = {
|
||||||
|
getCandles: vi.fn(),
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
getOrFetch: vi.fn(async (_keyPrefix, _keyParts, fetchFn) => ({
|
||||||
|
data: await fetchFn(),
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
|
||||||
providers: [
|
providers: [
|
||||||
CandlesService,
|
CandlesService,
|
||||||
MoexClientService,
|
{ provide: MoexClientService, useValue: moexClient },
|
||||||
{
|
{ provide: CacheService, useValue: cache },
|
||||||
provide: 'CACHE_MANAGER',
|
|
||||||
useValue: {
|
|
||||||
get: () => undefined,
|
|
||||||
set: () => Promise.resolve(),
|
|
||||||
del: () => Promise.resolve(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CacheService,
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<CandlesService>(CandlesService);
|
service = module.get<CandlesService>(CandlesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('uses MOEX interval 24 for daily share candles and maps output envelope', async () => {
|
||||||
expect(service).toBeDefined();
|
vi.mocked(moexClient.getCandles).mockResolvedValue([
|
||||||
});
|
{
|
||||||
|
open: 320,
|
||||||
|
high: 325,
|
||||||
|
low: 318,
|
||||||
|
close: 323,
|
||||||
|
volume: 1500000,
|
||||||
|
value: 480000000,
|
||||||
|
begin: '2026-05-01 00:00:00',
|
||||||
|
end: '2026-05-01 23:59:59',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
it('should return daily candles for SBER', async () => {
|
|
||||||
const result = await service.getCandles(
|
const result = await service.getCandles(
|
||||||
'shares',
|
'shares',
|
||||||
'SBER',
|
'SBER',
|
||||||
@ -42,7 +53,65 @@ describe('CandlesService', () => {
|
|||||||
'2026-05-01',
|
'2026-05-01',
|
||||||
'2026-06-01',
|
'2026-06-01',
|
||||||
);
|
);
|
||||||
expect(result.data.length).toBeGreaterThan(0);
|
|
||||||
expect(result.data[0].open).toBeDefined();
|
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||||
}, 15000);
|
'candles',
|
||||||
|
['shares', 'SBER', '24', '2026-05-01', '2026-06-01'],
|
||||||
|
expect.any(Function),
|
||||||
|
'candlesTtl',
|
||||||
|
);
|
||||||
|
expect(moexClient.getCandles).toHaveBeenCalledWith(
|
||||||
|
'stock',
|
||||||
|
'shares',
|
||||||
|
'SBER',
|
||||||
|
24,
|
||||||
|
'2026-05-01',
|
||||||
|
'2026-06-01',
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
open: 320,
|
||||||
|
high: 325,
|
||||||
|
low: 318,
|
||||||
|
close: 323,
|
||||||
|
volume: 1500000,
|
||||||
|
value: 480000000,
|
||||||
|
begin: '2026-05-01 00:00:00',
|
||||||
|
end: '2026-05-01 23:59:59',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses MOEX interval 60 for hourly bond candles without live MOEX dependency', async () => {
|
||||||
|
vi.mocked(moexClient.getCandles).mockResolvedValue([]);
|
||||||
|
|
||||||
|
await service.getCandles(
|
||||||
|
'bonds',
|
||||||
|
'SU26238RMFS5',
|
||||||
|
CandleInterval.HOUR,
|
||||||
|
'2026-05-01',
|
||||||
|
'2026-06-01',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||||
|
'candles',
|
||||||
|
['bonds', 'SU26238RMFS5', '60', '2026-05-01', '2026-06-01'],
|
||||||
|
expect.any(Function),
|
||||||
|
'candlesTtl',
|
||||||
|
);
|
||||||
|
expect(moexClient.getCandles).toHaveBeenCalledWith(
|
||||||
|
'stock',
|
||||||
|
'bonds',
|
||||||
|
'SU26238RMFS5',
|
||||||
|
60,
|
||||||
|
'2026-05-01',
|
||||||
|
'2026-06-01',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { ScreenerType } from './dto/screener-query.dto';
|
|||||||
|
|
||||||
describe('ScreenerService', () => {
|
describe('ScreenerService', () => {
|
||||||
let service: ScreenerService;
|
let service: ScreenerService;
|
||||||
let moexClient: MoexClientService;
|
|
||||||
let cache: CacheService;
|
let cache: CacheService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -30,7 +29,6 @@ describe('ScreenerService', () => {
|
|||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<ScreenerService>(ScreenerService);
|
service = module.get<ScreenerService>(ScreenerService);
|
||||||
moexClient = module.get<MoexClientService>(MoexClientService);
|
|
||||||
cache = module.get<CacheService>(CacheService);
|
cache = module.get<CacheService>(CacheService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +1,174 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { SecuritiesService } from './securities.service';
|
import { SecuritiesService } from './securities.service';
|
||||||
import { MoexClientService } from '../moex-client/moex-client.service';
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import configuration from '../../config/configuration';
|
|
||||||
import { SecurityType } from './dto/search-query.dto';
|
import { SecurityType } from './dto/search-query.dto';
|
||||||
|
|
||||||
describe('SecuritiesService', () => {
|
describe('SecuritiesService', () => {
|
||||||
let service: SecuritiesService;
|
let service: SecuritiesService;
|
||||||
|
let moexClient: Pick<MoexClientService, 'searchSecurities'>;
|
||||||
|
let cache: Pick<CacheService, 'getOrFetch'>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
moexClient = {
|
||||||
|
searchSecurities: vi.fn(),
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
getOrFetch: vi.fn(async (_keyPrefix, _keyParts, fetchFn) => ({
|
||||||
|
data: await fetchFn(),
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
|
||||||
providers: [
|
providers: [
|
||||||
SecuritiesService,
|
SecuritiesService,
|
||||||
MoexClientService,
|
{ provide: MoexClientService, useValue: moexClient },
|
||||||
{
|
{ provide: CacheService, useValue: cache },
|
||||||
provide: 'CACHE_MANAGER',
|
|
||||||
useValue: {
|
|
||||||
get: () => undefined,
|
|
||||||
set: () => Promise.resolve(),
|
|
||||||
del: () => Promise.resolve(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CacheService,
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<SecuritiesService>(SecuritiesService);
|
service = module.get<SecuritiesService>(SecuritiesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return search results for SBER', async () => {
|
it('returns supported securities only and normalizes SUR currency to RUB', async () => {
|
||||||
const results = await service.search('SBER', SecurityType.ALL, 5);
|
vi.mocked(moexClient.searchSecurities).mockResolvedValue([
|
||||||
expect(results.length).toBeGreaterThan(0);
|
{
|
||||||
expect(results[0].secid).toBeDefined();
|
secid: 'SBER',
|
||||||
}, 15000);
|
isin: 'RU0009029540',
|
||||||
|
name: 'Сбербанк России ПАО ао',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
latName: 'Sberbank',
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 21586948000,
|
||||||
|
faceValue: 3,
|
||||||
|
faceUnit: 'SUR',
|
||||||
|
issueDate: '2007-07-20',
|
||||||
|
typeName: 'Акция обыкновенная',
|
||||||
|
group: 'stock_shares',
|
||||||
|
type: 'common_share',
|
||||||
|
isQualifiedInvestors: false,
|
||||||
|
morningSession: true,
|
||||||
|
eveningSession: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
isin: 'RU000A1038V6',
|
||||||
|
name: 'ОФЗ 26238',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
latName: null,
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 100000000,
|
||||||
|
faceValue: 1000,
|
||||||
|
faceUnit: 'RUB',
|
||||||
|
issueDate: '2021-06-23',
|
||||||
|
typeName: 'Государственная облигация',
|
||||||
|
group: 'stock_bonds',
|
||||||
|
type: 'ofz_bond',
|
||||||
|
isQualifiedInvestors: false,
|
||||||
|
morningSession: false,
|
||||||
|
eveningSession: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secid: 'SiM6',
|
||||||
|
isin: '',
|
||||||
|
name: 'USD/RUB Futures',
|
||||||
|
shortName: 'SiM6',
|
||||||
|
latName: null,
|
||||||
|
listLevel: 0,
|
||||||
|
issueSize: 0,
|
||||||
|
faceValue: 0,
|
||||||
|
faceUnit: '',
|
||||||
|
issueDate: '',
|
||||||
|
typeName: 'Фьючерс',
|
||||||
|
group: 'futures',
|
||||||
|
type: 'futures',
|
||||||
|
isQualifiedInvestors: false,
|
||||||
|
morningSession: false,
|
||||||
|
eveningSession: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const results = await service.search('SbEr', SecurityType.ALL, 10);
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
secid: 'SBER',
|
||||||
|
isin: 'RU0009029540',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
type: 'share',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
isin: 'RU000A1038V6',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
type: 'bond',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||||
|
'search',
|
||||||
|
['sber'],
|
||||||
|
expect.any(Function),
|
||||||
|
'searchTtl',
|
||||||
|
);
|
||||||
|
expect(moexClient.searchSecurities).toHaveBeenCalledWith('SbEr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by type and applies limit without live MOEX dependency', async () => {
|
||||||
|
vi.mocked(cache.getOrFetch).mockResolvedValue({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
secid: 'SBER',
|
||||||
|
isin: 'RU0009029540',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
type: 'share',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secid: 'GAZP',
|
||||||
|
isin: 'RU0007661625',
|
||||||
|
shortName: 'Газпром',
|
||||||
|
type: 'share',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secid: 'SU26238RMFS5',
|
||||||
|
isin: 'RU000A1038V6',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
type: 'bond',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fromCache: true,
|
||||||
|
cachedAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await service.search('ru', SecurityType.SHARE, 1);
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
secid: 'SBER',
|
||||||
|
isin: 'RU0009029540',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
type: 'share',
|
||||||
|
listLevel: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
price: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(moexClient.searchSecurities).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,41 +1,135 @@
|
|||||||
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { SharesService } from './shares.service';
|
import { SharesService } from './shares.service';
|
||||||
import { MoexClientService } from '../moex-client/moex-client.service';
|
import { MoexClientService } from '../moex-client/moex-client.service';
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import configuration from '../../config/configuration';
|
|
||||||
|
|
||||||
describe('SharesService', () => {
|
describe('SharesService', () => {
|
||||||
let service: SharesService;
|
let service: SharesService;
|
||||||
|
let moexClient: Pick<MoexClientService, 'getSecurityDescription' | 'getShareMarketData'>;
|
||||||
|
let cache: Pick<CacheService, 'getOrFetch'>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
moexClient = {
|
||||||
|
getSecurityDescription: vi.fn(),
|
||||||
|
getShareMarketData: vi.fn(),
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
getOrFetch: vi.fn(async (_keyPrefix, _keyParts, fetchFn) => ({
|
||||||
|
data: await fetchFn(),
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-15T00:00:00.000Z',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
imports: [ConfigModule.forRoot({ load: [configuration] })],
|
|
||||||
providers: [
|
providers: [
|
||||||
SharesService,
|
SharesService,
|
||||||
MoexClientService,
|
{ provide: MoexClientService, useValue: moexClient },
|
||||||
{
|
{ provide: CacheService, useValue: cache },
|
||||||
provide: 'CACHE_MANAGER',
|
|
||||||
useValue: {
|
|
||||||
get: () => undefined,
|
|
||||||
set: () => Promise.resolve(),
|
|
||||||
del: () => Promise.resolve(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CacheService,
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<SharesService>(SharesService);
|
service = module.get<SharesService>(SharesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('returns normalized SBER share spec and market data without live MOEX dependency', async () => {
|
||||||
expect(service).toBeDefined();
|
vi.mocked(moexClient.getSecurityDescription).mockResolvedValue({
|
||||||
|
secid: 'SBER',
|
||||||
|
isin: 'RU0009029540',
|
||||||
|
name: 'Сбербанк России ПАО ао',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
latName: 'Sberbank',
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 21586948000,
|
||||||
|
faceValue: 3,
|
||||||
|
faceUnit: 'SUR',
|
||||||
|
issueDate: '2007-07-20',
|
||||||
|
typeName: 'Акция обыкновенная',
|
||||||
|
group: 'stock_shares',
|
||||||
|
type: 'common_share',
|
||||||
|
isQualifiedInvestors: false,
|
||||||
|
morningSession: true,
|
||||||
|
eveningSession: true,
|
||||||
|
});
|
||||||
|
vi.mocked(moexClient.getShareMarketData).mockResolvedValue({
|
||||||
|
secid: 'SBER',
|
||||||
|
boardid: 'TQBR',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
bid: 320,
|
||||||
|
offer: 321,
|
||||||
|
open: 318,
|
||||||
|
low: 317,
|
||||||
|
high: 325,
|
||||||
|
last: 323,
|
||||||
|
lastChange: 4,
|
||||||
|
lastChangePrcnt: 1.25,
|
||||||
|
volume: 1500000,
|
||||||
|
value: 480000000,
|
||||||
|
waprice: 321,
|
||||||
|
numtrades: 4200,
|
||||||
|
issueCapitalization: 6900000000000,
|
||||||
|
tradingStatus: 'T',
|
||||||
|
updateTime: '18:45:00',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getShare('SBER');
|
||||||
|
|
||||||
|
expect(moexClient.getSecurityDescription).toHaveBeenCalledWith('SBER');
|
||||||
|
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||||
|
'marketdata',
|
||||||
|
['shares', 'SBER'],
|
||||||
|
expect.any(Function),
|
||||||
|
'marketDataTtl',
|
||||||
|
);
|
||||||
|
expect(moexClient.getShareMarketData).toHaveBeenCalledWith('SBER');
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
secid: 'SBER',
|
||||||
|
isin: 'RU0009029540',
|
||||||
|
name: 'Сбербанк России ПАО ао',
|
||||||
|
shortName: 'Сбербанк',
|
||||||
|
latName: 'Sberbank',
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 21586948000,
|
||||||
|
faceValue: 3,
|
||||||
|
faceUnit: 'RUB',
|
||||||
|
type: 'common_share',
|
||||||
|
marketData: {
|
||||||
|
price: 323,
|
||||||
|
change: 4,
|
||||||
|
changePercent: 1.25,
|
||||||
|
open: 318,
|
||||||
|
high: 325,
|
||||||
|
low: 317,
|
||||||
|
volume: 1500000,
|
||||||
|
value: 480000000,
|
||||||
|
issueCapitalization: 6900000000000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.marketData.updatedAt).toMatch(/T18:45:00$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return SBER share data', async () => {
|
it('throws NotFoundException for non-share security', async () => {
|
||||||
const share = await service.getShare('SBER');
|
vi.mocked(moexClient.getSecurityDescription).mockResolvedValue({
|
||||||
expect(share.secid).toBe('SBER');
|
secid: 'SU26238RMFS5',
|
||||||
expect(share.marketData).toBeDefined();
|
isin: 'RU000A1038V6',
|
||||||
}, 15000);
|
name: 'ОФЗ 26238',
|
||||||
|
shortName: 'ОФЗ 26238',
|
||||||
|
latName: null,
|
||||||
|
listLevel: 1,
|
||||||
|
issueSize: 100000000,
|
||||||
|
faceValue: 1000,
|
||||||
|
faceUnit: 'RUB',
|
||||||
|
issueDate: '2021-06-23',
|
||||||
|
typeName: 'Государственная облигация',
|
||||||
|
group: 'stock_bonds',
|
||||||
|
type: 'ofz_bond',
|
||||||
|
isQualifiedInvestors: false,
|
||||||
|
morningSession: false,
|
||||||
|
eveningSession: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.getShare('SU26238RMFS5')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
expect(cache.getOrFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user