import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test"; import { tradeService } from "./trade.service"; import { itemTransactions } from "@/db/schema"; import { economyService } from "@/modules/economy/economy.service"; import { inventoryService } from "@/modules/inventory/inventory.service"; // 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); } }, }; }); describe("TradeService", () => { const userA = { id: "1", username: "UserA" }; const userB = { id: "2", username: "UserB" }; let mockModifyUserBalance: any; let mockAddItem: any; let mockRemoveItem: any; beforeEach(() => { mockInsert.mockClear(); mockValues.mockClear(); // Clear sessions (tradeService as any)._sessions.clear(); // Spies mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any); mockAddItem = spyOn(inventoryService, 'addItem').mockResolvedValue({} as any); mockRemoveItem = spyOn(inventoryService, 'removeItem').mockResolvedValue({} as any); }); afterEach(() => { mockModifyUserBalance.mockRestore(); mockAddItem.mockRestore(); mockRemoveItem.mockRestore(); }); 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"); }); }); });