Compare commits
2
Commits
main
...
e7827dcac6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7827dcac6 | ||
|
|
5cf914752d |
@@ -1,6 +1,9 @@
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import assert from 'node:assert/strict';
|
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 { PortfolioRepository } from '../src/modules/investments/portfolio/infrastructure/portfolio.repository';
|
||||||
import { PrismaService } from '../src/prisma/prisma.service';
|
import { PrismaService } from '../src/prisma/prisma.service';
|
||||||
|
|
||||||
@@ -16,9 +19,41 @@ describe('portfolio publication (SQLite)', () => {
|
|||||||
totalsByCurrency: [],
|
totalsByCurrency: [],
|
||||||
positions: [],
|
positions: [],
|
||||||
cashBalances: [],
|
cashBalances: [],
|
||||||
structure: {},
|
structure: { currencies: [], unvaluedCategories: [] },
|
||||||
} as never;
|
} satisfies PortfolioDto;
|
||||||
const accountId = 'account';
|
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 () => {
|
beforeAll(async () => {
|
||||||
await prisma.$connect();
|
await prisma.$connect();
|
||||||
@@ -27,7 +62,9 @@ describe('portfolio publication (SQLite)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
afterAll(async () => prisma.$disconnect());
|
afterAll(async () => prisma.$disconnect());
|
||||||
|
afterEach(() => jest.restoreAllMocks());
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await prisma.tbankPortfolioValueHistory.deleteMany();
|
||||||
await prisma.tbankConnection.deleteMany();
|
await prisma.tbankConnection.deleteMany();
|
||||||
await prisma.tbankConnection.create({
|
await prisma.tbankConnection.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -48,20 +85,7 @@ describe('portfolio publication (SQLite)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('publishes the same snapshot for the running attempt of the current revision', async () => {
|
it('publishes the same snapshot for the running attempt of the current revision', async () => {
|
||||||
await prisma.tbankSyncWork.create({
|
await createRunningAttempt();
|
||||||
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(
|
const published = await repository.publish(
|
||||||
{ id: 'live', attemptId: 'live-attempt', userId: 'owner', revision: 'current' },
|
{ id: 'live', attemptId: 'live-attempt', userId: 'owner', revision: 'current' },
|
||||||
accountId,
|
accountId,
|
||||||
@@ -70,4 +94,99 @@ describe('portfolio publication (SQLite)', () => {
|
|||||||
assert.equal(published, true);
|
assert.equal(published, true);
|
||||||
expect(await prisma.tbankPortfolio.count()).toBe(1);
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user