- MoexHttpClient: infrastructure (axios, rate limiter, circuit breaker) - MoexSecuritiesClient: search and security descriptions - MoexMarketDataClient: share/bond market data and batch queries - MoexCandlesClient: candle data - MoexHistoryClient: share/bond history - MoexDividendsClient: dividend data - Removed @Global() from MoexClientModule - Updated all 7 consumers with explicit DI - All 141 tests passing
145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
|
||
import { SharesService } from './shares.service';
|
||
import { MoexSecuritiesClient } from '../moex-client/moex-securities.client';
|
||
import { MoexMarketDataClient } from '../moex-client/moex-market-data.client';
|
||
import { MoexDividendsClient } from '../moex-client/moex-dividends.client';
|
||
import { MoexHistoryClient } from '../moex-client/moex-history.client';
|
||
import { CacheService } from '../cache/cache.service';
|
||
|
||
describe('SharesService', () => {
|
||
let service: SharesService;
|
||
let moexSecurities: Pick<MoexSecuritiesClient, 'getSecurityDescription'>;
|
||
let moexMarketData: Pick<MoexMarketDataClient, 'getShareMarketData'>;
|
||
let cache: Pick<CacheService, 'getOrFetch'>;
|
||
|
||
beforeEach(async () => {
|
||
moexSecurities = {
|
||
getSecurityDescription: vi.fn(),
|
||
};
|
||
moexMarketData = {
|
||
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({
|
||
providers: [
|
||
SharesService,
|
||
{ provide: MoexSecuritiesClient, useValue: moexSecurities },
|
||
{ provide: MoexMarketDataClient, useValue: moexMarketData },
|
||
{ provide: MoexDividendsClient, useValue: { getDividends: vi.fn() } },
|
||
{ provide: MoexHistoryClient, useValue: { getHistory: vi.fn() } },
|
||
{ provide: CacheService, useValue: cache },
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<SharesService>(SharesService);
|
||
});
|
||
|
||
it('returns normalized SBER share spec and market data without live MOEX dependency', async () => {
|
||
vi.mocked(moexSecurities.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(moexMarketData.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(moexSecurities.getSecurityDescription).toHaveBeenCalledWith('SBER');
|
||
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||
'marketdata',
|
||
['shares', 'SBER'],
|
||
expect.any(Function),
|
||
'marketDataTtl',
|
||
);
|
||
expect(moexMarketData.getShareMarketData).toHaveBeenCalledWith('SBER');
|
||
expect(result.data).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.data.marketData.updatedAt).toMatch(/T18:45:00$/);
|
||
});
|
||
|
||
it('throws EntityNotFoundException for non-share security', async () => {
|
||
vi.mocked(moexSecurities.getSecurityDescription).mockResolvedValue({
|
||
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,
|
||
});
|
||
|
||
await expect(service.getShare('SU26238RMFS5')).rejects.toBeInstanceOf(EntityNotFoundException);
|
||
expect(cache.getOrFetch).not.toHaveBeenCalled();
|
||
});
|
||
});
|