108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
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);
|
|
});
|
|
});
|