refactor: organize backend by business responsibility
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user