99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
import { ServiceUnavailableException } from '@nestjs/common';
|
|
|
|
import { NcrRatingsClient } from './ncr-ratings.client';
|
|
const isin = 'RU000A10EW44';
|
|
const row = (value = 'AAA.ru', code = isin, href = '/ratings/issues/123/') =>
|
|
`<tr><td>Эмитент</td><td>Облигация</td><td>${value}</td><td><a href="${href}">${code}</a></td><td>REG</td><td>17.04.2026</td></tr>`;
|
|
const table = (rows: string) =>
|
|
`<table id="issues-table"><thead><tr><th>Лицо</th><th>Выпуск</th><th>Рейтинг</th><th>ISIN</th><th>Регномер</th><th>Дата</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
const response = (html: string) =>
|
|
({ ok: true, text: () => Promise.resolve(html) }) as Response;
|
|
const cacheClock = performance;
|
|
describe('public NCR issue ratings', () => {
|
|
beforeEach(() => {
|
|
jest.useFakeTimers().setSystemTime(new Date('2026-09-10T10:00:00Z'));
|
|
jest.spyOn(cacheClock, 'now').mockImplementation(() => Date.now());
|
|
});
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
jest.useRealTimers();
|
|
});
|
|
it('matches only exact ISIN and shares one public registry download', async () => {
|
|
const fetchMock = jest
|
|
.spyOn(global, 'fetch')
|
|
.mockResolvedValue(response(table(row())));
|
|
const client = new NcrRatingsClient();
|
|
const [rating, missing] = await Promise.all([
|
|
client.rating(isin),
|
|
client.rating('RU000A1065B1'),
|
|
]);
|
|
expect(rating).toEqual({
|
|
agency: 'NCR',
|
|
value: 'AAA.ru',
|
|
status: 'active',
|
|
date: '2026-04-17',
|
|
sourceUrl: 'https://ratings.ru/ratings/issues/123/',
|
|
observedAt: '2026-09-10T10:00:00.000Z',
|
|
});
|
|
expect(missing).toBeNull();
|
|
expect(await client.rating('issuer-name')).toBeNull();
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
await jest.advanceTimersByTimeAsync(6 * 60 * 60_000 + 1);
|
|
await client.rating(isin);
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
it('preserves withdrawal and never displays an issuer rating as an issue rating', async () => {
|
|
jest
|
|
.spyOn(global, 'fetch')
|
|
.mockResolvedValue(response(table(row('рейтинг отозван'))));
|
|
expect(await new NcrRatingsClient().rating(isin)).toMatchObject({
|
|
status: 'withdrawn',
|
|
value: null,
|
|
});
|
|
});
|
|
it.each([
|
|
'<html>Service unavailable</html>',
|
|
table(row('AAA.ru', isin, 'https://untrusted.test/')),
|
|
table(row('unrecognized')),
|
|
table(row() + row('AA.ru')),
|
|
table(''),
|
|
])(
|
|
'reports changed or ambiguous registry as unavailable, not unrated',
|
|
async (html) => {
|
|
jest.spyOn(global, 'fetch').mockResolvedValue(response(html));
|
|
await expect(new NcrRatingsClient().rating(isin)).rejects.toBeInstanceOf(
|
|
ServiceUnavailableException,
|
|
);
|
|
},
|
|
);
|
|
it.each(['AA+.ru', 'B-.ru', 'BBB+.ru', 'CCC-.ru', 'CC.ru', 'D.ru'])(
|
|
'accepts the public rating grammar: %s', async (value) => {
|
|
jest.spyOn(global, 'fetch').mockResolvedValue(response(table(row(value))));
|
|
expect(await new NcrRatingsClient().rating(isin)).toMatchObject({ value });
|
|
},
|
|
);
|
|
it.each(['AAA+.ru', 'CC-.ru', 'BBBB.ru', 'BBB++.ru'])(
|
|
'rejects invalid rating grades: %s', async (value) => {
|
|
jest.spyOn(global, 'fetch').mockResolvedValue(response(table(row(value))));
|
|
await expect(new NcrRatingsClient().rating(isin)).rejects.toBeInstanceOf(ServiceUnavailableException);
|
|
},
|
|
);
|
|
it('bounds outage retries then recovers without retaining a false missing rating', async () => {
|
|
const fetchMock = jest
|
|
.spyOn(global, 'fetch')
|
|
.mockRejectedValueOnce(new Error('offline'))
|
|
.mockResolvedValue(response(table(row())));
|
|
const client = new NcrRatingsClient();
|
|
await expect(client.rating(isin)).rejects.toBeInstanceOf(
|
|
ServiceUnavailableException,
|
|
);
|
|
await expect(client.rating(isin)).rejects.toBeInstanceOf(
|
|
ServiceUnavailableException,
|
|
);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
await jest.advanceTimersByTimeAsync(60_001);
|
|
expect(await client.rating(isin)).toMatchObject({ value: 'AAA.ru' });
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|