1 Commits
Author SHA1 Message Date
ksv741 af92ac980e fix: scope income and ledger sync to requested intervals (WealthLedger/backend#57)
Backend verify / commits (pull_request) Successful in 32s
Backend verify / security (pull_request) Successful in 10s
Backend verify / verify (pull_request) Canceled after 3m55s
2026-10-28 20:55:38 +03:00
+16 -135
View File
@@ -1,9 +1,6 @@
import { ConfigService } from '@nestjs/config';
import assert from 'node:assert/strict';
import type { Prisma } from '../generated/prisma/client';
import type { PortfolioDto } from '../src/modules/investments/portfolio/presentation/portfolio.dto';
import { PortfolioRepository } from '../src/modules/investments/portfolio/infrastructure/portfolio.repository';
import { PrismaService } from '../src/prisma/prisma.service';
@@ -19,41 +16,9 @@ describe('portfolio publication (SQLite)', () => {
totalsByCurrency: [],
positions: [],
cashBalances: [],
structure: { currencies: [], unvaluedCategories: [] },
} satisfies PortfolioDto;
structure: {},
} as never;
const accountId = 'account';
const valuedSnapshot = {
...snapshot,
totalsByCurrency: [
{ currency: 'RUB', value: '200' },
{ currency: 'USD', value: '10' },
],
} satisfies PortfolioDto;
const createRunningAttempt = () =>
prisma.tbankSyncWork.create({
data: {
id: 'live',
userId: 'owner',
revision: 'current',
scopeKey: `portfolio:${accountId}`,
intent: {},
state: 'running',
attemptId: 'live-attempt',
processRunId: 'run',
priority: 1,
progress: {},
},
});
const interceptTransaction = (intercept: (tx: Prisma.TransactionClient) => void) => {
const transaction = prisma.$transaction.bind(prisma);
jest.spyOn(prisma, '$transaction').mockImplementation(
(callback: (tx: Prisma.TransactionClient) => Promise<unknown>) =>
transaction((tx) => {
intercept(tx);
return callback(tx);
}),
);
};
beforeAll(async () => {
await prisma.$connect();
@@ -62,9 +27,7 @@ describe('portfolio publication (SQLite)', () => {
});
});
afterAll(async () => prisma.$disconnect());
afterEach(() => jest.restoreAllMocks());
beforeEach(async () => {
await prisma.tbankPortfolioValueHistory.deleteMany();
await prisma.tbankConnection.deleteMany();
await prisma.tbankConnection.create({
data: {
@@ -85,7 +48,20 @@ describe('portfolio publication (SQLite)', () => {
});
it('publishes the same snapshot for the running attempt of the current revision', async () => {
await createRunningAttempt();
await prisma.tbankSyncWork.create({
data: {
id: 'live',
userId: 'owner',
revision: 'current',
scopeKey: `portfolio:${accountId}`,
intent: {},
state: 'running',
attemptId: 'live-attempt',
processRunId: 'run',
priority: 1,
progress: {},
},
});
const published = await repository.publish(
{ id: 'live', attemptId: 'live-attempt', userId: 'owner', revision: 'current' },
accountId,
@@ -94,99 +70,4 @@ describe('portfolio publication (SQLite)', () => {
assert.equal(published, true);
expect(await prisma.tbankPortfolio.count()).toBe(1);
});
it('rolls back the snapshot, first value point and claim when the second point fails', async () => {
const previousFetchedAt = new Date('2025-12-31T00:00:00.000Z');
const previousCapturedAt = new Date('2025-12-31T01:00:00.000Z');
await prisma.tbankPortfolio.create({
data: {
userId: 'owner',
revision: 'current',
accountId,
payload: snapshot,
fetchedAt: previousFetchedAt,
},
});
await prisma.tbankPortfolioValueHistory.create({
data: {
userId: 'owner',
accountId,
points: {
create: {
attemptId: 'previous-attempt',
currency: 'RUB',
value: '100',
capturedAt: previousCapturedAt,
},
},
},
});
await createRunningAttempt();
interceptTransaction((tx) => {
const upsert = tx.tbankPortfolioValuePoint.upsert.bind(tx.tbankPortfolioValuePoint);
jest
.spyOn(tx.tbankPortfolioValuePoint, 'upsert')
.mockImplementationOnce((args) => upsert(args))
.mockRejectedValueOnce(new Error('injected second point failure'));
});
await expect(
repository.publish(
{ id: 'live', attemptId: 'live-attempt', userId: 'owner', revision: 'current' },
accountId,
valuedSnapshot,
),
).rejects.toThrow('injected second point failure');
expect(
await prisma.tbankPortfolio.findUnique({
where: { userId_revision_accountId: { userId: 'owner', revision: 'current', accountId } },
}),
).toMatchObject({ payload: snapshot, fetchedAt: previousFetchedAt, dataRevision: 1 });
expect(
await prisma.tbankPortfolioValuePoint.findMany({
select: { attemptId: true, value: true, capturedAt: true },
}),
).toEqual([
{ attemptId: 'previous-attempt', value: '100', capturedAt: previousCapturedAt },
]);
expect(await prisma.tbankSyncWork.findUniqueOrThrow({ where: { id: 'live' } }))
.toMatchObject({ state: 'running', attemptId: 'live-attempt' });
});
it.each(['stale attempt', 'complete work', 'invalid token'] as const)(
'does not publish for %s',
async (scenario) => {
await createRunningAttempt();
if (scenario === 'complete work') {
await prisma.tbankSyncWork.update({
where: { id: 'live' },
data: { state: 'complete' },
});
}
if (scenario === 'invalid token') {
await prisma.tbankConnection.update({
where: { userId: 'owner' },
data: { invalidToken: true },
});
}
await expect(
repository.publish(
{
id: 'live',
attemptId: scenario === 'stale attempt' ? 'stale-attempt' : 'live-attempt',
userId: 'owner',
revision: 'current',
},
accountId,
valuedSnapshot,
),
).resolves.toBe(false);
expect(await prisma.tbankPortfolio.count()).toBe(0);
expect(await prisma.tbankPortfolioValueHistory.count()).toBe(0);
expect(await prisma.tbankPortfolioValuePoint.count()).toBe(0);
},
);
});