fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
Resolve A01-A10 and C01-C07 from the backend audit. Preserve auth failures, forced candle reconciliation and forecast freshness. Validate MOEX responses and nested HTTP errors; remove unused legacy paths. Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability. Record the remaining macOS native SQLite abort in the audit.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { MoexClient } from './moex.client';
|
||||
|
||||
const emptyEvents = {
|
||||
coupons: { columns: ['coupondate', 'value', 'faceunit'], data: [] },
|
||||
offers: { columns: ['offerdate'], data: [] },
|
||||
description: { columns: ['name', 'value'], data: [] },
|
||||
};
|
||||
|
||||
describe('MoexClient', () => {
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
it('uses effective yield only for the exact ISIN and board and preserves its own price and date', async () => {
|
||||
@@ -92,17 +98,174 @@ describe('MoexClient', () => {
|
||||
expect(result?.value ?? null).toBe(value === null ? null : '0');
|
||||
},
|
||||
);
|
||||
it('treats a successful response without the requested block as no events', async () => {
|
||||
const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
description: { columns: ['name', 'value'], data: [] },
|
||||
boards: { columns: ['secid'], data: [] },
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
it('accepts confirmed empty blocks', async () => {
|
||||
jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockImplementation(() => Promise.resolve(Response.json(emptyEvents)));
|
||||
await expect(new MoexClient().bondEvents('BOND')).resolves.toEqual([]);
|
||||
fetchMock.mockRestore();
|
||||
});
|
||||
|
||||
it('preserves not-found semantics independently of malformed HTTP 200', async () => {
|
||||
jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(new Response(null, { status: 404 })),
|
||||
);
|
||||
const client = new MoexClient();
|
||||
await expect(client.bondEvents('MISSING')).resolves.toEqual([]);
|
||||
await expect(
|
||||
client.bondYield('MISSING', 'ISIN', 'TQOB'),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['missing block', { coupons: undefined }],
|
||||
['missing columns', { coupons: { data: [] } }],
|
||||
['invalid column', { coupons: { columns: [null], data: [] } }],
|
||||
['invalid row', { coupons: { columns: ['coupondate'], data: [null] } }],
|
||||
[
|
||||
'short row',
|
||||
{ coupons: { columns: ['coupondate', 'value'], data: [['2099-02-01']] } },
|
||||
],
|
||||
[
|
||||
'long row',
|
||||
{ coupons: { columns: ['coupondate'], data: [['2099-02-01', 2]] } },
|
||||
],
|
||||
[
|
||||
'empty cursor',
|
||||
{
|
||||
'coupons.cursor': { columns: ['INDEX', 'TOTAL', 'PAGESIZE'], data: [] },
|
||||
},
|
||||
],
|
||||
[
|
||||
'missing cursor field',
|
||||
{ 'coupons.cursor': { columns: ['INDEX', 'TOTAL'], data: [[0, 1]] } },
|
||||
],
|
||||
[
|
||||
'null cursor index',
|
||||
{
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[null, 0, 1]],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'boolean cursor total',
|
||||
{
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[0, false, 1]],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'empty cursor total',
|
||||
{
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[0, '', 1]],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'short page',
|
||||
{
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[0, 1, 1]],
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
'rejects %s without returning a partial calendar',
|
||||
async (_name, invalid) => {
|
||||
jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(Response.json({ ...emptyEvents, ...invalid })),
|
||||
);
|
||||
await expect(new MoexClient().bondEvents('BOND')).rejects.toMatchObject({
|
||||
response: { code: 'MOEX_UNAVAILABLE' },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, -1])(
|
||||
'rejects nonpositive page size %s before repeating a fetch',
|
||||
async (pageSize) => {
|
||||
const starts: string[] = [];
|
||||
jest.spyOn(global, 'fetch').mockImplementation((input) => {
|
||||
const url = new URL(input instanceof Request ? input.url : input);
|
||||
if (url.searchParams.get('iss.only')?.startsWith('coupons,')) {
|
||||
starts.push(url.searchParams.get('start') ?? '');
|
||||
if (starts.length > 1)
|
||||
return Promise.reject(new Error('Repeated page'));
|
||||
}
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
...emptyEvents,
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[0, 1, pageSize]],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
await expect(new MoexClient().bondEvents('BOND')).rejects.toMatchObject({
|
||||
response: { code: 'MOEX_UNAVAILABLE' },
|
||||
});
|
||||
expect(starts).toEqual(['0']);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'reads all cursor pages and rejects a repeated index: %s',
|
||||
async (repeatIndex) => {
|
||||
const starts: string[] = [];
|
||||
jest.spyOn(global, 'fetch').mockImplementation((input) => {
|
||||
const url = new URL(input instanceof Request ? input.url : input);
|
||||
const start = Number(url.searchParams.get('start'));
|
||||
if (url.searchParams.get('iss.only')?.startsWith('coupons,')) {
|
||||
starts.push(String(start));
|
||||
if (starts.length > 2)
|
||||
return Promise.reject(new Error('Repeated page'));
|
||||
}
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
...emptyEvents,
|
||||
coupons: {
|
||||
columns: ['coupondate', 'value', 'faceunit'],
|
||||
data: [[`2099-02-0${start + 1}`, '2.000000001', 'SUR']],
|
||||
},
|
||||
'coupons.cursor': {
|
||||
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
|
||||
data: [[repeatIndex ? 0 : start, 2, 1]],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
const result = new MoexClient().bondEvents('BOND');
|
||||
if (repeatIndex)
|
||||
await expect(result).rejects.toMatchObject({
|
||||
response: { code: 'MOEX_UNAVAILABLE' },
|
||||
});
|
||||
else
|
||||
await expect(result).resolves.toEqual([
|
||||
{
|
||||
type: 'coupon',
|
||||
date: '2099-02-01',
|
||||
value: '2.000000001',
|
||||
currency: 'RUB',
|
||||
},
|
||||
{
|
||||
type: 'coupon',
|
||||
date: '2099-02-02',
|
||||
value: '2.000000001',
|
||||
currency: 'RUB',
|
||||
},
|
||||
]);
|
||||
expect(starts).toEqual(['0', '1']);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user