Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions backend/tests/integration/top-up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,61 @@ describe('POST /v1/streams/:streamId/top-up', () => {
expect(res.status).toBe(400);
expect(mockPrisma.stream.update).not.toHaveBeenCalled();
});

it('handles concurrent top-ups correctly without lost updates', async () => {
// Both requests read the same initial state
vi.mocked(mockPrisma.stream.findUnique).mockResolvedValue(mockStream as any);

// Simulate some delay in the on-chain transaction
vi.mocked(topUpStream).mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
return 'txhash-mock';
});

// We also need to implement $executeRaw to mock the atomic DB update
// because that's what we will change the implementation to use.
// However, if it's the OLD implementation, it calls update().
// We should make the mocked executeRaw update a local variable and
// also make update() do the same, so we can test both the failure and success.

let currentDeposited = BigInt(mockStream.depositedAmount);

vi.mocked(mockPrisma.$queryRaw).mockImplementation(async (queryArgs: any, ...values: any[]) => {
// values[0] is the amount, values[1] is nowTs, values[2] is streamId
const amountToAdd = BigInt(values[0]);
currentDeposited += amountToAdd;
return [{ depositedAmount: currentDeposited.toString() }] as any;
});

// Actually, to make the test pass after the fix, the best way to do atomic update is to do a transaction where we re-fetch the stream.
// Let's first just test if it works with the old implementation (should fail).

const req1 = request(app)
.post('/v1/streams/42/top-up')
.set('Authorization', 'Bearer dummy')
.send({ amount: '1000' });

const req2 = request(app)
.post('/v1/streams/42/top-up')
.set('Authorization', 'Bearer dummy')
.send({ amount: '2000' });

const [res1, res2] = await Promise.all([req1, req2]);

expect(res1.status).toBe(200);
expect(res2.status).toBe(200);

// The initial amount is 86400. We add 1000 and 2000.
// 86400 + 1000 + 2000 = 89400.
// Because we mock Prisma in vitest, the controller's Prisma calls will hit our mock.
// We will assert on the final 'depositedAmount' returned in the response, or what was sent to Prisma.
// Since the API returns depositedAmount, let's just check the response body.

// If it lost update, one response will be 87400 and the other 88400.
// If it didn't lose update, the second one should be 89400.
const amt1 = parseInt(res1.body.depositedAmount);
const amt2 = parseInt(res2.body.depositedAmount);
expect(Math.max(amt1, amt2)).toBe(89400);
});

});
Loading