From fc058effd53e581abdeed80216d8fef34754efa2 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Wed, 18 Mar 2026 13:24:49 +0100 Subject: [PATCH] feat: add integration test for economy transfer flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests the full transfer cycle against a real database: debit/credit, transaction records, insufficient funds rejection, self-transfer rejection, non-positive amounts, and sequential transfers. Uses *.integration.test.ts convention — excluded from default test runs, included with --integration flag in CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../economy/economy.integration.test.ts | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/shared/modules/economy/economy.integration.test.ts b/shared/modules/economy/economy.integration.test.ts index 28c6572..4baac92 100644 --- a/shared/modules/economy/economy.integration.test.ts +++ b/shared/modules/economy/economy.integration.test.ts @@ -8,6 +8,10 @@ import { economyService } from "./economy.service"; const SENDER_ID = "999000000000000001"; const RECEIVER_ID = "999000000000000002"; +// This test requires a real database connection. +// It is excluded from `bun test` by default (*.integration.test.ts pattern) +// and only runs in CI with `--integration` flag, which provides PostgreSQL. + async function cleanupTestUsers() { // transactions.userId has onDelete: 'cascade', so deleting users removes their transactions await DrizzleClient.delete(users).where(eq(users.id, BigInt(SENDER_ID))); @@ -77,16 +81,26 @@ describe("Economy Integration Tests", () => { expect(receiverTxs[0]!.type).toBe("TRANSFER_IN"); }); - it("should reject a transfer when sender has insufficient funds", async () => { + it("should reject transfer with insufficient funds and leave balances unchanged", async () => { await expect( economyService.transfer(SENDER_ID, RECEIVER_ID, 2000n) ).rejects.toThrow("Insufficient funds"); - // Verify balances are unchanged + // Verify balances unchanged const sender = await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(SENDER_ID)), }); + const receiver = await DrizzleClient.query.users.findFirst({ + where: eq(users.id, BigInt(RECEIVER_ID)), + }); expect(sender!.balance).toBe(1000n); + expect(receiver!.balance).toBe(500n); + + // Verify no transaction records were created + const senderTxs = await DrizzleClient.query.transactions.findMany({ + where: eq(transactions.userId, BigInt(SENDER_ID)), + }); + expect(senderTxs.length).toBe(0); }); it("should reject a self-transfer", async () => { @@ -122,30 +136,5 @@ describe("Economy Integration Tests", () => { // Receiver: 500 + 100 + 200 - 50 = 750 expect(receiver!.balance).toBe(750n); }); - - it("should roll back the entire transfer if an error occurs mid-transaction", async () => { - // Attempting to transfer more than balance ensures the transaction aborts - // after the sender check but before any SQL update is committed - const senderBefore = await DrizzleClient.query.users.findFirst({ - where: eq(users.id, BigInt(SENDER_ID)), - }); - - await expect( - economyService.transfer(SENDER_ID, RECEIVER_ID, 9999n) - ).rejects.toThrow("Insufficient funds"); - - const senderAfter = await DrizzleClient.query.users.findFirst({ - where: eq(users.id, BigInt(SENDER_ID)), - }); - - // Balance must be unchanged — no partial debit - expect(senderAfter!.balance).toBe(senderBefore!.balance); - - // No transaction records should have been written - const senderTxs = await DrizzleClient.query.transactions.findMany({ - where: eq(transactions.userId, BigInt(SENDER_ID)), - }); - expect(senderTxs.length).toBe(0); - }); }); });