Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af92ac980e |
@@ -1,9 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -19,41 +16,9 @@ describe('portfolio publication (SQLite)', () => {
|
|||||||
totalsByCurrency: [],
|
totalsByCurrency: [],
|
||||||
positions: [],
|
positions: [],
|
||||||
cashBalances: [],
|
cashBalances: [],
|
||||||
structure: { currencies: [], unvaluedCategories: [] },
|
structure: {},
|
||||||
} satisfies PortfolioDto;
|
} as never;
|
||||||
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();
|
||||||
@@ -62,9 +27,7 @@ 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: {
|
||||||
@@ -85,7 +48,20 @@ 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 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(
|
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,
|
||||||
@@ -94,99 +70,4 @@ 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