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:
@@ -11,6 +11,8 @@ import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { InstrumentMetadataRepository } from '../src/integrations/tbank/instrument-metadata.repository';
|
||||
import { TbankClient } from '../src/integrations/tbank/tbank.client';
|
||||
import { BrokerageOperationsResourceDto } from '../src/modules/investments/operations/presentation/brokerage-operation.dto';
|
||||
import { BrokerageOperationsRepository } from '../src/modules/investments/operations/infrastructure/brokerage-operations.repository';
|
||||
import type { IncomeAnalyticsResourceDto } from '../src/modules/investments/income/income-analytics.dto';
|
||||
|
||||
const uid = '11111111-1111-4111-8111-111111111111';
|
||||
const period = 'from=2025-01-01&to=2025-01-31';
|
||||
@@ -127,6 +129,97 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
|
||||
});
|
||||
it('uses the persisted subsecond timestamp for filtering, cursor order and income entries', async () => {
|
||||
await prisma.tbankOperation.deleteMany();
|
||||
const { revision } = await prisma.tbankConnection.findUniqueOrThrow({
|
||||
where: { userId: 'owner' },
|
||||
});
|
||||
const repository = app.get(BrokerageOperationsRepository);
|
||||
const interval = {
|
||||
accountId: 'a',
|
||||
from: new Date('2025-01-02T00:00:00.500Z'),
|
||||
to: new Date('2025-01-02T00:00:01.000Z'),
|
||||
};
|
||||
const reserved = await repository.request(
|
||||
'owner',
|
||||
revision,
|
||||
[interval],
|
||||
'subsecond-test',
|
||||
true,
|
||||
new Set(),
|
||||
1,
|
||||
);
|
||||
assert(reserved);
|
||||
const attempt = {
|
||||
userId: 'owner',
|
||||
revision,
|
||||
id: reserved.work.id,
|
||||
attemptId: reserved.work.attemptId,
|
||||
};
|
||||
await expect(
|
||||
repository.stagePage(attempt, interval, undefined, {
|
||||
hasNext: false,
|
||||
items: [{ id: 'invalid', date: {} }],
|
||||
}),
|
||||
).rejects.toThrow('Invalid operation timestamp');
|
||||
await repository.stagePage(attempt, interval, undefined, {
|
||||
hasNext: false,
|
||||
items: [400, 600, 800].map((milliseconds) => ({
|
||||
id: String(milliseconds),
|
||||
date: { seconds: '1735776000', nanos: milliseconds * 1_000_000 },
|
||||
type: 'OPERATION_TYPE_COUPON',
|
||||
payment: { currency: 'rub', units: '1', nano: 1 },
|
||||
})),
|
||||
});
|
||||
await repository.publish(attempt, [interval], new Date(), true);
|
||||
const saved = await prisma.tbankOperation.findMany({
|
||||
orderBy: { occurredAt: 'desc' },
|
||||
});
|
||||
const expected = [
|
||||
{ id: '800', occurredAt: '2025-01-02T00:00:00.800Z' },
|
||||
{ id: '600', occurredAt: '2025-01-02T00:00:00.600Z' },
|
||||
];
|
||||
expect(
|
||||
saved.map((row) => ({
|
||||
id: row.brokerOperationId,
|
||||
occurredAt: row.occurredAt.toISOString(),
|
||||
})),
|
||||
).toEqual(expected);
|
||||
const items: { id: string; occurredAt: string }[] = [];
|
||||
let cursor: string | null = null;
|
||||
do {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/a/operations?from=${interval.from.toISOString()}&to=2025-01-02T00:00:00.900Z&limit=1&sync=observe${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`,
|
||||
)
|
||||
.set('Authorization', auth())
|
||||
.expect(200);
|
||||
const result = response.body as BrokerageOperationsResourceDto;
|
||||
assert(result.data);
|
||||
expect(result.data.items).toHaveLength(1);
|
||||
items.push(
|
||||
...result.data.items.map(({ id, occurredAt }) => ({ id, occurredAt })),
|
||||
);
|
||||
cursor = result.data.nextCursor;
|
||||
} while (cursor);
|
||||
const income = (
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/a/income-analytics?${period}&sync=observe`,
|
||||
)
|
||||
.set('Authorization', auth())
|
||||
.expect(200)
|
||||
).body as IncomeAnalyticsResourceDto;
|
||||
expect(items).toEqual(expected);
|
||||
expect(
|
||||
income.data?.entries.map(({ id, occurredAt }) => ({ id, occurredAt })),
|
||||
).toEqual(expected);
|
||||
expect(income.data?.totalsByCurrency[0].total).toEqual({
|
||||
currency: 'RUB',
|
||||
value: '2.000000002',
|
||||
});
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(['default', 'today', 'today timestamp'])(
|
||||
'keeps observed current coverage complete through %s period pagination without RPC',
|
||||
async (periodKind) => {
|
||||
|
||||
Reference in New Issue
Block a user