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
+113 -1
View File
@@ -144,7 +144,12 @@ describe('screen sync HTTP and SQLite', () => {
{
screen: 'stock',
instrumentUid: '00000000-0000-4000-8000-000000000001',
candleRange: { from: '2026-02-01', to: '2026-01-01', jobs: [] },
candleRange: {
from: '2026-02-01',
to: '2026-01-01',
jobs: [],
'~bad/path': 'SYNTHETIC_VALIDATION_SECRET',
},
},
])('rejects malformed scope without reserving work: %j', async (scope) => {
const response = await post(scope).expect(400);
@@ -161,6 +166,20 @@ describe('screen sync HTTP and SQLite', () => {
expect(typeof error.pointer).toBe('string');
expect(typeof error.detail).toBe('string');
}
if ('candleRange' in scope) {
expect(
problem.errors.filter(
(error) => error.pointer === '#/candleRange/jobs',
),
).toHaveLength(1);
expect(problem.errors).toContainEqual({
pointer: '#/candleRange/~0bad~1path',
detail: expect.any(String) as unknown,
});
expect(JSON.stringify(response.body)).not.toContain(
'SYNTHETIC_VALIDATION_SECRET',
);
}
expect(await prisma.tbankSyncWork.count()).toBe(0);
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
});
@@ -287,6 +306,99 @@ describe('screen sync HTTP and SQLite', () => {
await status(last.meta.sync.id).expect(200);
expect(upstream.getOperationsByCursor).toHaveBeenCalledTimes(1);
});
it.each(['succeeded', 'failed'] as const)(
'observes the operations successor until it is %s',
async (outcome) => {
const accounts = [
{
id: 'a',
name: 'Synthetic',
type: 'ACCOUNT_TYPE_TINKOFF',
openedDate: { seconds: '1577836800' },
},
];
let releaseMembership: (value: typeof accounts) => void = () => {};
upstream.getOpenAccounts.mockReturnValue(
new Promise((resolve) => {
releaseMembership = resolve;
}),
);
const connection = await prisma.tbankConnection.findUniqueOrThrow({
where: { userId: 'owner' },
});
await prisma.tbankAccount.create({
data: {
userId: 'owner',
revision: connection.revision,
accountId: 'a',
name: 'Synthetic',
type: 'ACCOUNT_TYPE_TINKOFF',
openedOn: '2020-01-01',
upstreamOrder: 0,
},
});
let release: (value: {
hasNext: boolean;
items: never[];
}) => void = () => {};
let reject: (error: Error) => void = () => {};
upstream.getOperationsByCursor
.mockReturnValueOnce(
new Promise((resolve, rejectResult) => {
release = resolve;
reject = rejectResult;
}),
)
.mockImplementation(() =>
outcome === 'failed'
? Promise.reject(new Error('Synthetic outage'))
: Promise.resolve({ hasNext: false, items: [] }),
);
const accept = async (from: string, to: string) => {
const response = await post({
screen: 'operations',
accountId: 'a',
from,
to,
}).expect(202);
return (response.body as { id: string }).id;
};
const dispatched = async (id: string) => {
for (let i = 0; i < 300; i++) {
const work = await prisma.tbankSyncWork.findUniqueOrThrow({
where: { id },
});
if ((work.progress as { dispatched: boolean }).dispatched) return id;
await delay(5);
}
throw new Error('Screen work did not dispatch');
};
const first = await accept('2026-01-01', '2026-01-10');
const second = await accept('2026-01-20', '2026-01-31');
const merged = await accept('2026-01-01', '2026-01-31');
releaseMembership(accounts);
await Promise.all([first, second, merged].map(dispatched));
try {
expect((await status(second).expect(200)).body).toMatchObject({
state: 'running',
progress: { completed: 1, total: 2, discovering: false },
});
await status(second, 'other').expect(404);
} finally {
if (outcome === 'failed') reject(new Error('Synthetic outage'));
else release({ hasNext: false, items: [] });
await settled(merged);
await settled(first);
}
expect(await settled(second)).toMatchObject({
state: outcome,
errorCode: outcome === 'failed' ? 'TBANK_UNAVAILABLE' : null,
progress: { completed: 2, total: 2, discovering: false },
});
await prisma.tbankConnection.delete({ where: { userId: 'owner' } });
await status(second).expect(404);
},
);
it('rejects known foreign accounts locally and requires authentication', async () => {
await post({ screen: 'operations', accountId: 'foreign' }).expect(404);
await request(app.getHttpServer())