import { ConfigService } from '@nestjs/config'; import { TbankAccessService } from '../src/integrations/tbank/tbank-access.service'; import { TbankTokenService } from '../src/integrations/tbank/tbank-token.service'; import { PrismaService } from '../src/prisma/prisma.service'; // Characterizes the membership reserve protocol on real SQLite transactions. // Join by the same process while the attempt is active; launch after it is not. describe('membership reservation protocol (SQLite)', () => { const prisma = new PrismaService( new ConfigService({ DATABASE_URL: process.env.DATABASE_URL }), ); const tokens = new TbankTokenService( new ConfigService({ TOKEN_ENCRYPTION_KEY: process.env.TOKEN_ENCRYPTION_KEY, }) as never, ); const access = new TbankAccessService(prisma, tokens); const userId = 'synthetic-membership-reservation'; const revision = `${userId}-revision`; beforeAll(async () => { await prisma.$connect(); await prisma.tbankSyncWork.deleteMany({ where: { userId } }); await prisma.tbankConnection.deleteMany({ where: { userId } }); await prisma.user.deleteMany({ where: { id: userId } }); await prisma.user.create({ data: { id: userId, email: `${userId}@example.invalid`, passwordHash: 'synthetic', }, }); await prisma.tbankConnection.create({ data: { userId, revision, encryptedToken: tokens.encrypt('synthetic-membership'), connectedAt: new Date(), }, }); }); afterAll(async () => { await prisma.tbankSyncWork.deleteMany({ where: { userId } }); await prisma.tbankConnection.deleteMany({ where: { userId } }); await prisma.user.deleteMany({ where: { id: userId } }); await prisma.$disconnect(); }); it('launches once, joins the active attempt by the same process, and relaunches after it finishes', async () => { const processRunId = 'run-membership-a'; const first = await access.reserveMembership( userId, revision, processRunId, new Set(), true, ); expect(first?.launch).toBe(true); const attemptId = first?.work.attemptId ?? ''; const joined = await access.reserveMembership( userId, revision, processRunId, new Set([attemptId]), true, ); expect(joined?.launch).toBe(false); expect(joined?.work.id).toBe(first?.work.id); expect(joined?.work.attemptId).toBe(attemptId); // The attempt is no longer in the active set: this must not join it. const relaunched = await access.reserveMembership( userId, revision, processRunId, new Set(), true, ); expect(relaunched?.launch).toBe(true); expect(relaunched?.work.attemptId).not.toBe(attemptId); }); it('does not join an attempt started by another process', async () => { const other = await access.reserveMembership( userId, revision, 'run-membership-b', new Set(), true, ); expect(other?.launch).toBe(true); const attemptId = other?.work.attemptId ?? ''; const foreign = await access.reserveMembership( userId, revision, 'run-membership-c', new Set([attemptId]), true, ); expect(foreign?.launch).toBe(true); expect(foreign?.work.attemptId).not.toBe(attemptId); }); });