Compare commits
1
Commits
v2.4.1
...
efc13a9f67
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efc13a9f67 |
@@ -3,6 +3,7 @@ import type { Server } from 'node:http';
|
|||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
import { setTimeout as delay } from 'node:timers/promises';
|
import { setTimeout as delay } from 'node:timers/promises';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
@@ -573,16 +574,30 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
|
|||||||
await put().expect(200);
|
await put().expect(200);
|
||||||
await seedMembership();
|
await seedMembership();
|
||||||
const blocked = deferred<TbankPortfolio>();
|
const blocked = deferred<TbankPortfolio>();
|
||||||
upstream.getPortfolio.mockReturnValueOnce(blocked.promise);
|
const started = deferred<void>();
|
||||||
await get('brokerage-accounts/account/portfolio').expect(200);
|
upstream.getPortfolio.mockImplementationOnce(() => {
|
||||||
|
started.resolve();
|
||||||
|
return blocked.promise;
|
||||||
|
});
|
||||||
|
// Starts the real portfolio producer, the same path the hourly walk uses.
|
||||||
|
const late = await portfolioService.request(
|
||||||
|
'owner',
|
||||||
|
(await prisma.tbankConnection.findUniqueOrThrow({ where: { userId: 'owner' } })).revision,
|
||||||
|
'account',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert(late);
|
||||||
|
// The late upstream call must have begun before the connection is replaced.
|
||||||
|
await started.promise;
|
||||||
await put().expect(200);
|
await put().expect(200);
|
||||||
await seedMembership();
|
await seedMembership();
|
||||||
blocked.resolve({ positions: [] });
|
blocked.resolve({ positions: [] });
|
||||||
await delay(20);
|
// Barrier: the fenced producer's completion, not a timer.
|
||||||
|
await late.done;
|
||||||
|
expect(await prisma.tbankPortfolio.count()).toBe(0);
|
||||||
const replaced = await get(
|
const replaced = await get(
|
||||||
'brokerage-accounts/account/portfolio?sync=observe',
|
'brokerage-accounts/account/portfolio?sync=observe',
|
||||||
).expect(200);
|
).expect(200);
|
||||||
expect((replaced.body as PortfolioResourceDto).data).toBeNull();
|
expect((replaced.body as PortfolioResourceDto).data).toBeNull();
|
||||||
expect(await prisma.tbankPortfolio.count()).toBe(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { PortfolioRepository } from '../src/modules/investments/portfolio/infrastructure/portfolio.repository';
|
||||||
|
import { PrismaService } from '../src/prisma/prisma.service';
|
||||||
|
|
||||||
|
// Real SQLite transaction: a running attempt of the current revision publishes its snapshot.
|
||||||
|
describe('portfolio publication (SQLite)', () => {
|
||||||
|
const prisma = new PrismaService(
|
||||||
|
new ConfigService({ DATABASE_URL: process.env.DATABASE_URL }),
|
||||||
|
);
|
||||||
|
const repository = new PortfolioRepository(prisma);
|
||||||
|
const snapshot = {
|
||||||
|
lastSuccessfulAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
isStale: false,
|
||||||
|
totalsByCurrency: [],
|
||||||
|
positions: [],
|
||||||
|
cashBalances: [],
|
||||||
|
structure: {},
|
||||||
|
} as never;
|
||||||
|
const accountId = 'account';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await prisma.$connect();
|
||||||
|
await prisma.user.create({
|
||||||
|
data: { id: 'owner', email: 'owner@example.invalid', passwordHash: 'synthetic' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterAll(async () => prisma.$disconnect());
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prisma.tbankConnection.deleteMany();
|
||||||
|
await prisma.tbankConnection.create({
|
||||||
|
data: {
|
||||||
|
userId: 'owner',
|
||||||
|
revision: 'current',
|
||||||
|
encryptedToken: 'synthetic',
|
||||||
|
connectedAt: new Date(),
|
||||||
|
accounts: {
|
||||||
|
create: {
|
||||||
|
accountId,
|
||||||
|
type: 'brokerage',
|
||||||
|
name: 'Synthetic',
|
||||||
|
upstreamOrder: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes the same snapshot for the running attempt of the current revision', async () => {
|
||||||
|
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,
|
||||||
|
snapshot,
|
||||||
|
);
|
||||||
|
assert.equal(published, true);
|
||||||
|
expect(await prisma.tbankPortfolio.count()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user