diff --git a/apps/backend/package.json b/apps/backend/package.json index 1d8a57e..c194228 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -8,8 +8,9 @@ "start:dev": "nest start --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,test}/**/*.ts\"", - "test": "VITE_CJS_IGNORE_WARNING=1 vitest run", - "test:watch": "vitest" + "test": "VITE_CJS_IGNORE_WARNING=1 vitest run --exclude \"src/**/*.integration.spec.ts\"", + "test:watch": "vitest --exclude \"src/**/*.integration.spec.ts\"", + "test:integration": "MOEX_LIVE_TESTS=1 VITE_CJS_IGNORE_WARNING=1 vitest run \"src/**/*.integration.spec.ts\"" }, "dependencies": { "@libsql/client": "^0.17.3", diff --git a/apps/backend/src/modules/moex-client/moex-client.service.integration.spec.ts b/apps/backend/src/modules/moex-client/moex-client.service.integration.spec.ts new file mode 100644 index 0000000..ff2ae26 --- /dev/null +++ b/apps/backend/src/modules/moex-client/moex-client.service.integration.spec.ts @@ -0,0 +1,35 @@ +import 'reflect-metadata'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import { MoexClientService } from './moex-client.service'; +import configuration from '../../config/configuration'; + +describe.skipIf(process.env.MOEX_LIVE_TESTS !== '1')( + 'MoexClientService live MOEX integration', + () => { + let service: MoexClientService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot({ load: [configuration] })], + providers: [MoexClientService], + }).compile(); + + service = module.get(MoexClientService); + }); + + it('возвращает результаты поиска для SBER из live MOEX', async () => { + const results = await service.searchSecurities('SBER'); + + expect(results.length).toBeGreaterThan(0); + expect(results[0].secid).toBeDefined(); + }, 15000); + + it('возвращает рыночные данные SBER из live MOEX', async () => { + const data = await service.getShareMarketData('SBER'); + + expect(data).toBeDefined(); + expect(data!.secid).toBe('SBER'); + }, 15000); + }, +); diff --git a/apps/backend/src/modules/moex-client/moex-client.service.spec.ts b/apps/backend/src/modules/moex-client/moex-client.service.spec.ts index 5921972..3b13bb7 100644 --- a/apps/backend/src/modules/moex-client/moex-client.service.spec.ts +++ b/apps/backend/src/modules/moex-client/moex-client.service.spec.ts @@ -1,38 +1,186 @@ import 'reflect-metadata'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigModule } from '@nestjs/config'; +import axios from 'axios'; +import { ConfigService } from '@nestjs/config'; import { MoexClientService } from './moex-client.service'; -import configuration from '../../config/configuration'; + +vi.mock('axios', () => ({ + default: { + create: vi.fn(), + }, +})); describe('MoexClientService', () => { let service: MoexClientService; + let getMock: ReturnType; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ load: [configuration] })], - providers: [MoexClientService], - }).compile(); + beforeEach(() => { + getMock = vi.fn(); + vi.mocked(axios.create).mockReturnValue({ get: getMock } as never); - service = module.get(MoexClientService); + service = new MoexClientService({ + get: vi.fn((key: string, fallback?: unknown) => { + const values: Record = { + 'app.moex.baseUrl': 'https://iss.moex.test/iss', + 'app.moex.circuitBreakerThreshold': 5, + 'app.moex.circuitBreakerResetSeconds': 30, + 'app.moex.rateLimit': 10, + }; + return values[key] ?? fallback; + }), + } as unknown as ConfigService); }); - it('should be defined', () => { + it('создаётся с настроенным MOEX client', () => { expect(service).toBeDefined(); + expect(axios.create).toHaveBeenCalledWith({ + baseURL: 'https://iss.moex.test/iss', + timeout: 10000, + paramsSerializer: { indexes: null }, + }); }); - describe('searchSecurities', () => { - it('should return results for SBER query', async () => { - const results = await service.searchSecurities('SBER'); - expect(results.length).toBeGreaterThan(0); - expect(results[0].secid).toBeDefined(); - }, 15000); + it('нормализует результаты поиска из ISS table format', async () => { + getMock.mockResolvedValueOnce({ + data: { + securities: { + columns: [ + 'secid', + 'isin', + 'name', + 'shortName', + 'latName', + 'listLevel', + 'issuesize', + 'facevalue', + 'faceunit', + 'issuedate', + 'typename', + 'group', + 'type', + 'isqualifiedinvestors', + 'morningsession', + 'eveningsession', + ], + data: [ + [ + 'SBER', + 'RU0009029540', + 'Сбербанк России ПАО ао', + 'Сбербанк', + 'Sberbank', + '1', + '21586948000', + '3', + 'SUR', + '2007-07-20', + 'Акция обыкновенная', + 'stock_shares', + 'common_share', + '0', + '1', + '1', + ], + ], + }, + }, + }); + + const results = await service.searchSecurities('SBER'); + + expect(getMock).toHaveBeenCalledWith('/securities.json', { + params: { q: 'SBER', 'iss.meta': 'off' }, + }); + expect(results).toEqual([ + { + 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, + }, + ]); }); - describe('getShareMarketData', () => { - it('should return market data for SBER', async () => { - const data = await service.getShareMarketData('SBER'); - expect(data).toBeDefined(); - expect(data!.secid).toBe('SBER'); - }, 15000); + it('нормализует market data акции без live MOEX запроса', async () => { + getMock.mockResolvedValueOnce({ + data: { + securities: { + columns: ['SECID', 'BOARDID', 'SHORTNAME', 'PREVPRICE'], + data: [['SBER', 'TQBR', 'Сбербанк', '320.10']], + }, + marketdata: { + columns: [ + 'SECID', + 'BOARDID', + 'BID', + 'OFFER', + 'OPEN', + 'LOW', + 'HIGH', + 'LAST', + 'LASTCHANGE', + 'LASTCHANGEPRCNT', + 'VOLTODAY', + 'VALTODAY', + 'WAPRICE', + 'NUMTRADES', + 'ISSUECAPITALIZATION', + 'TRADINGSTATUS', + 'UPDATETIME', + ], + data: [ + [ + 'SBER', + 'TQBR', + '321', + '322', + '320', + '319', + '323', + '322.35', + '1.15', + '0.36', + '1925163', + '620184479', + '321.9', + '12345', + '6958336818320', + 'T', + '10:30:00', + ], + ], + }, + }, + }); + + const data = await service.getShareMarketData('SBER'); + + expect(getMock).toHaveBeenCalledWith('/engines/stock/markets/shares/securities/SBER.json', { + params: { boards: 'TQBR', 'iss.meta': 'off' }, + }); + expect(data).toMatchObject({ + secid: 'SBER', + boardid: 'TQBR', + shortName: 'Сбербанк', + last: 322.35, + lastChange: 1.15, + lastChangePrcnt: 0.36, + volume: 1925163, + value: 620184479, + issueCapitalization: 6958336818320, + tradingStatus: 'T', + updateTime: '10:30:00', + }); }); });