diff --git a/backend/tests/integration/top-up.test.ts b/backend/tests/integration/top-up.test.ts index 26d4e086..7753e3d8 100644 --- a/backend/tests/integration/top-up.test.ts +++ b/backend/tests/integration/top-up.test.ts @@ -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); + }); + });