import { describe, it, expect, beforeAll, afterAll, beforeEach } from "bun:test"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { users, transactions } from "@db/schema"; import { eq } from "drizzle-orm"; import { economyService } from "./economy.service"; // Use high IDs to avoid conflicts with real data 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))); await DrizzleClient.delete(users).where(eq(users.id, BigInt(RECEIVER_ID))); } describe("Economy Integration Tests", () => { beforeAll(async () => { await cleanupTestUsers(); await DrizzleClient.insert(users).values([ { id: BigInt(SENDER_ID), username: "test_sender_integration", balance: 1000n }, { id: BigInt(RECEIVER_ID), username: "test_receiver_integration", balance: 500n }, ]); }); beforeEach(async () => { // Reset balances and clear transaction history before each test await DrizzleClient.delete(transactions).where( eq(transactions.userId, BigInt(SENDER_ID)) ); await DrizzleClient.delete(transactions).where( eq(transactions.userId, BigInt(RECEIVER_ID)) ); await DrizzleClient.update(users) .set({ balance: 1000n }) .where(eq(users.id, BigInt(SENDER_ID))); await DrizzleClient.update(users) .set({ balance: 500n }) .where(eq(users.id, BigInt(RECEIVER_ID))); }); afterAll(async () => { await cleanupTestUsers(); }); describe("transfer", () => { it("should debit sender, credit receiver, and create transaction records", async () => { const result = await economyService.transfer(SENDER_ID, RECEIVER_ID, 200n); expect(result.success).toBe(true); expect(result.amount).toBe(200n); 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(800n); expect(receiver!.balance).toBe(700n); const senderTxs = await DrizzleClient.query.transactions.findMany({ where: eq(transactions.userId, BigInt(SENDER_ID)), }); const receiverTxs = await DrizzleClient.query.transactions.findMany({ where: eq(transactions.userId, BigInt(RECEIVER_ID)), }); expect(senderTxs.length).toBe(1); expect(senderTxs[0]!.amount).toBe(-200n); expect(senderTxs[0]!.type).toBe("TRANSFER_OUT"); expect(receiverTxs.length).toBe(1); expect(receiverTxs[0]!.amount).toBe(200n); expect(receiverTxs[0]!.type).toBe("TRANSFER_IN"); }); 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 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 for either party const senderTxs = await DrizzleClient.query.transactions.findMany({ where: eq(transactions.userId, BigInt(SENDER_ID)), }); const receiverTxs = await DrizzleClient.query.transactions.findMany({ where: eq(transactions.userId, BigInt(RECEIVER_ID)), }); expect(senderTxs.length).toBe(0); expect(receiverTxs.length).toBe(0); }); it("should reject a self-transfer", async () => { await expect( economyService.transfer(SENDER_ID, SENDER_ID, 100n) ).rejects.toThrow("Cannot transfer to self"); }); it("should reject a non-positive amount", async () => { await expect( economyService.transfer(SENDER_ID, RECEIVER_ID, 0n) ).rejects.toThrow("Amount must be positive"); await expect( economyService.transfer(SENDER_ID, RECEIVER_ID, -100n) ).rejects.toThrow("Amount must be positive"); }); it("should apply multiple sequential transfers correctly", async () => { await economyService.transfer(SENDER_ID, RECEIVER_ID, 100n); await economyService.transfer(SENDER_ID, RECEIVER_ID, 200n); await economyService.transfer(RECEIVER_ID, SENDER_ID, 50n); 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)), }); // Sender: 1000 - 100 - 200 + 50 = 750 expect(sender!.balance).toBe(750n); // Receiver: 500 + 100 + 200 - 50 = 750 expect(receiver!.balance).toBe(750n); }); }); });