From 4e228bb7a3f80aead2f39c624e837c1ca3c0fd02 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 19 Dec 2025 12:15:48 +0100 Subject: [PATCH] test: add tests for trade service --- src/modules/trade/trade.service.test.ts | 183 ++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 src/modules/trade/trade.service.test.ts diff --git a/src/modules/trade/trade.service.test.ts b/src/modules/trade/trade.service.test.ts new file mode 100644 index 0000000..5ba30f6 --- /dev/null +++ b/src/modules/trade/trade.service.test.ts @@ -0,0 +1,183 @@ +import { describe, it, expect, mock, beforeEach, setSystemTime } from "bun:test"; +import { TradeService } from "./trade.service"; +import { itemTransactions } from "@/db/schema"; + +// Mock dependencies +const mockInsert = mock(); +const mockValues = mock(); + +mockInsert.mockReturnValue({ values: mockValues }); + +// Mock DrizzleClient +mock.module("@/lib/DrizzleClient", () => { + return { + DrizzleClient: { + transaction: async (cb: any) => { + const txMock = { + insert: mockInsert, // For transaction logs + }; + return cb(txMock); + } + }, + }; +}); + +// Mock External Services +const mockModifyUserBalance = mock(); +mock.module("@/modules/economy/economy.service", () => ({ + economyService: { + modifyUserBalance: mockModifyUserBalance + } +})); + +const mockAddItem = mock(); +const mockRemoveItem = mock(); +mock.module("@/modules/inventory/inventory.service", () => ({ + inventoryService: { + addItem: mockAddItem, + removeItem: mockRemoveItem + } +})); + +describe("TradeService", () => { + const userA = { id: "1", username: "UserA" }; + const userB = { id: "2", username: "UserB" }; + + beforeEach(() => { + mockModifyUserBalance.mockClear(); + mockAddItem.mockClear(); + mockRemoveItem.mockClear(); + mockInsert.mockClear(); + mockValues.mockClear(); + + // Clear sessions + (TradeService as any).sessions.clear(); + }); + + describe("createSession", () => { + it("should create a new session", () => { + const session = TradeService.createSession("thread1", userA, userB); + + expect(session.threadId).toBe("thread1"); + expect(session.state).toBe("NEGOTIATING"); + expect(session.userA.id).toBe("1"); + expect(session.userB.id).toBe("2"); + expect(TradeService.getSession("thread1")).toBe(session); + }); + }); + + describe("updateMoney", () => { + it("should update money offer", () => { + TradeService.createSession("thread1", userA, userB); + TradeService.updateMoney("thread1", "1", 100n); + + const session = TradeService.getSession("thread1"); + expect(session?.userA.offer.money).toBe(100n); + }); + + it("should unlock participants when offer changes", () => { + const session = TradeService.createSession("thread1", userA, userB); + session.userA.locked = true; + session.userB.locked = true; + + TradeService.updateMoney("thread1", "1", 100n); + + expect(session.userA.locked).toBe(false); + expect(session.userB.locked).toBe(false); + }); + + it("should throw if not in trade", () => { + TradeService.createSession("thread1", userA, userB); + expect(() => TradeService.updateMoney("thread1", "3", 100n)).toThrow("User not in trade"); + }); + }); + + describe("addItem", () => { + it("should add item to offer", () => { + TradeService.createSession("thread1", userA, userB); + TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n); + + const session = TradeService.getSession("thread1"); + expect(session?.userA.offer.items).toHaveLength(1); + expect(session?.userA.offer.items[0]).toEqual({ id: 10, name: "Sword", quantity: 1n }); + }); + + it("should stack items if already offered", () => { + TradeService.createSession("thread1", userA, userB); + TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n); + TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 2n); + + const session = TradeService.getSession("thread1"); + expect(session?.userA.offer.items[0].quantity).toBe(3n); + }); + }); + + describe("removeItem", () => { + it("should remove item from offer", () => { + const session = TradeService.createSession("thread1", userA, userB); + session.userA.offer.items.push({ id: 10, name: "Sword", quantity: 1n }); + + TradeService.removeItem("thread1", "1", 10); + + expect(session.userA.offer.items).toHaveLength(0); + }); + }); + + describe("toggleLock", () => { + it("should toggle lock status", () => { + TradeService.createSession("thread1", userA, userB); + + const locked1 = TradeService.toggleLock("thread1", "1"); + expect(locked1).toBe(true); + + const locked2 = TradeService.toggleLock("thread1", "1"); + expect(locked2).toBe(false); + }); + }); + + describe("executeTrade", () => { + it("should execute trade successfully", async () => { + const session = TradeService.createSession("thread1", userA, userB); + + // Setup offers + session.userA.offer.money = 100n; + session.userA.offer.items = [{ id: 10, name: "Sword", quantity: 1n }]; + + session.userB.offer.money = 50n; // B paying 50 back? Or just swap. + session.userB.offer.items = []; + + // Lock both + session.userA.locked = true; + session.userB.locked = true; + + await TradeService.executeTrade("thread1"); + + expect(session.state).toBe("COMPLETED"); + + // Verify Money Transfer A -> B (100) + expect(mockModifyUserBalance).toHaveBeenCalledWith("1", -100n, 'TRADE_OUT', expect.any(String), "2", expect.anything()); + expect(mockModifyUserBalance).toHaveBeenCalledWith("2", 100n, 'TRADE_IN', expect.any(String), "1", expect.anything()); + + // Verify Money Transfer B -> A (50) + expect(mockModifyUserBalance).toHaveBeenCalledWith("2", -50n, 'TRADE_OUT', expect.any(String), "1", expect.anything()); + expect(mockModifyUserBalance).toHaveBeenCalledWith("1", 50n, 'TRADE_IN', expect.any(String), "2", expect.anything()); + + // Verify Item Transfer A -> B (Sword) + expect(mockRemoveItem).toHaveBeenCalledWith("1", 10, 1n, expect.anything()); + expect(mockAddItem).toHaveBeenCalledWith("2", 10, 1n, expect.anything()); + + // Verify DB Logs (Item Transaction) + // 2 calls (sender log, receiver log) for 1 item + expect(mockInsert).toHaveBeenCalledTimes(2); + expect(mockInsert).toHaveBeenCalledWith(itemTransactions); + }); + + it("should throw if not locked", async () => { + const session = TradeService.createSession("thread1", userA, userB); + session.userA.locked = true; + // B not locked + + expect(TradeService.executeTrade("thread1")).rejects.toThrow("Both players must accept"); + }); + }); +});