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

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:
2026-09-27 14:09:05 +03:00
parent 5d3835e796
commit 4796d9d3f0
55 changed files with 1954 additions and 569 deletions
@@ -0,0 +1,113 @@
jest.mock('../infrastructure/candle.repository', () => ({
CandleRepository: class {},
}));
jest.mock('./catalog-enrichment.service', () => ({
CatalogEnrichmentService: class {},
}));
jest.mock('../../../../integrations/tbank/tbank.api', () => ({
TbankAccessService: class {},
TbankClient: class {},
}));
import { CandleService } from './candle.service';
describe('CandleService forced demand', () => {
it.each([false, true])(
'reconciles the full range through the latest forced timestamp while force=%s is loading',
async (initialForce) => {
let entered = () => {};
let release = () => {};
let complete = () => {};
const started = new Promise<void>((resolve) => (entered = resolve));
const blocked = new Promise<void>((resolve) => (release = resolve));
const finished = new Promise<void>((resolve) => (complete = resolve));
let completed = 0;
const work = { id: 'work', attemptId: 'attempt' };
const repository = {
reserve: jest
.fn()
.mockResolvedValueOnce({ work, launch: true })
.mockResolvedValueOnce({
work: { id: 'other', attemptId: 'other-attempt' },
launch: true,
})
.mockResolvedValue({ work, launch: false }),
read: jest.fn().mockResolvedValue({
coverage: [
{
from: new Date('2026-01-01Z'),
to: new Date('2026-02-01Z'),
isComplete: true,
fetchedAt: new Date('2026-02-01Z'),
},
],
}),
stage: jest.fn().mockResolvedValue(true),
publish: jest.fn((_attempt, _intent, done = true) => {
if (done && ++completed === 2) complete();
return Promise.resolve(true);
}),
fail: jest.fn(),
};
const sources = {
ensureDetail: jest.fn().mockResolvedValue({
first_1dayCandleDate: {
seconds: String(Date.parse('2026-01-01Z') / 1000),
},
}),
};
const upstream = { getCandles: jest.fn().mockResolvedValue([]) };
upstream.getCandles.mockImplementationOnce(async () => {
entered();
await blocked;
return [];
});
const service = new CandleService(
repository as never,
sources as never,
{ tokenForAttempt: jest.fn().mockResolvedValue('token') } as never,
upstream as never,
{ event: jest.fn() } as never,
);
const input = {
uid: 'uid',
instrumentType: 'share' as const,
period: 'all' as const,
};
await service.request(
'owner',
'revision',
input,
new Date('2026-02-02T12:00:00Z'),
initialForce,
2,
);
await started;
const latest = new Date('2026-02-02T13:00:00Z');
await service.request(
'owner',
'revision',
{ ...input, uid: 'other' },
latest,
false,
1,
);
expect(
await service.request('owner', 'revision', input, latest, true, 0),
).toBe('work');
release();
await finished;
expect(upstream.getCandles).toHaveBeenCalledWith(
'token',
'uid',
new Date('2026-01-01Z'),
latest,
);
expect(repository.fail).not.toHaveBeenCalled();
expect(
upstream.getCandles.mock.calls.map((call: unknown[]) => call[1]),
).toEqual(['uid', 'uid', 'other']);
},
);
});