From ae5ef4c80211c400231cb15ea7529693412fb474 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 19 Dec 2025 11:05:25 +0100 Subject: [PATCH] test: add tests for lootdrop service --- src/modules/economy/lootdrop.service.test.ts | 222 +++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/modules/economy/lootdrop.service.test.ts diff --git a/src/modules/economy/lootdrop.service.test.ts b/src/modules/economy/lootdrop.service.test.ts new file mode 100644 index 0000000..6c039aa --- /dev/null +++ b/src/modules/economy/lootdrop.service.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"; +import { lootdropService } from "./lootdrop.service"; +import { lootdrops } from "@/db/schema"; +import { eq, and, isNull } from "drizzle-orm"; + +// Mock dependencies BEFORE using service functionality +const mockInsert = mock(); +const mockUpdate = mock(); +const mockDelete = mock(); +const mockSelect = mock(); +const mockValues = mock(); +const mockReturning = mock(); +const mockSet = mock(); +const mockWhere = mock(); +const mockFrom = mock(); + +// Mock setup +mockInsert.mockReturnValue({ values: mockValues }); +mockUpdate.mockReturnValue({ set: mockSet }); +mockSet.mockReturnValue({ where: mockWhere }); +mockWhere.mockReturnValue({ returning: mockReturning }); +mockSelect.mockReturnValue({ from: mockFrom }); +mockFrom.mockReturnValue({ where: mockWhere }); + +// Mock DrizzleClient +mock.module("@/lib/DrizzleClient", () => { + return { + DrizzleClient: { + insert: mockInsert, + update: mockUpdate, + delete: mockDelete, + select: mockSelect, + } + }; +}); + +// Mock EconomyService +const mockModifyUserBalance = mock(); +mock.module("./economy.service", () => { + return { + economyService: { + modifyUserBalance: mockModifyUserBalance + } + }; +}); + +// Mock Config +mock.module("@/lib/config", () => ({ + config: { + lootdrop: { + activityWindowMs: 60000, + minMessages: 3, + spawnChance: 0.5, + cooldownMs: 10000, + reward: { + min: 10, + max: 100, + currency: "GOLD" + } + } + } +})); + +describe("lootdropService", () => { + let originalRandom: any; + + beforeEach(() => { + mockInsert.mockClear(); + mockUpdate.mockClear(); + mockDelete.mockClear(); + mockValues.mockClear(); + mockReturning.mockClear(); + mockSet.mockClear(); + mockWhere.mockClear(); + mockSelect.mockClear(); + mockFrom.mockClear(); + mockModifyUserBalance.mockClear(); + + // Reset internal state + (lootdropService as any).channelActivity = new Map(); + (lootdropService as any).channelCooldowns = new Map(); + + // Mock Math.random + originalRandom = Math.random; + }); + + afterEach(() => { + Math.random = originalRandom; + }); + + describe("processMessage", () => { + it("should track activity but not spawn if minMessages not reached", async () => { + const mockChannel = { id: "chan1", send: mock() }; + const mockMessage = { + author: { bot: false }, + guild: {}, + channel: mockChannel + }; + + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + + // Expect no spawn attempt + expect(mockChannel.send).not.toHaveBeenCalled(); + // Internal state check if possible, or just behavior + }); + + it("should spawn lootdrop if minMessages reached and chance hits", async () => { + const mockChannel = { id: "chan1", send: mock() }; + const mockMessage = { + author: { bot: false }, + guild: {}, + channel: mockChannel + }; + + mockChannel.send.mockResolvedValue({ id: "msg1" }); + Math.random = () => 0.01; // Force hit (0.01 < 0.5) + + // Send 3 messages + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + + expect(mockChannel.send).toHaveBeenCalled(); + expect(mockInsert).toHaveBeenCalledWith(lootdrops); + + // Verify DB insert + expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({ + channelId: "chan1", + messageId: "msg1", + currency: "GOLD" + })); + }); + + it("should not spawn if chance fails", async () => { + const mockChannel = { id: "chan1", send: mock() }; + const mockMessage = { + author: { bot: false }, + guild: {}, + channel: mockChannel + }; + + Math.random = () => 0.99; // Force fail (0.99 > 0.5) + + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + + expect(mockChannel.send).not.toHaveBeenCalled(); + }); + + it("should respect cooldowns", async () => { + const mockChannel = { id: "chan1", send: mock() }; + const mockMessage = { + author: { bot: false }, + guild: {}, + channel: mockChannel + }; + mockChannel.send.mockResolvedValue({ id: "msg1" }); + + Math.random = () => 0.01; // Force hit + + // Trigger spawn + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + + expect(mockChannel.send).toHaveBeenCalledTimes(1); + mockChannel.send.mockClear(); + + // Try again immediately (cooldown active) + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + await lootdropService.processMessage(mockMessage as any); + + expect(mockChannel.send).not.toHaveBeenCalled(); + }); + }); + + describe("tryClaim", () => { + it("should claim successfully if available", async () => { + mockReturning.mockResolvedValue([{ + messageId: "1001", + rewardAmount: 50, + currency: "GOLD", + channelId: "100" + }]); + + const result = await lootdropService.tryClaim("1001", "123", "UserOne"); + + expect(result.success).toBe(true); + expect(result.amount).toBe(50); + expect(mockModifyUserBalance).toHaveBeenCalledWith("123", 50n, "LOOTDROP_CLAIM", expect.any(String)); + }); + + it("should fail if already claimed", async () => { + // Update returns empty (failed condition) + mockReturning.mockResolvedValue([]); + // Select check returns non-empty (exists) + + const mockWhereSelect = mock().mockResolvedValue([{ messageId: "1001", claimedBy: 123n }]); + mockFrom.mockReturnValue({ where: mockWhereSelect }); + + const result = await lootdropService.tryClaim("1001", "123", "UserOne"); + + expect(result.success).toBe(false); + expect(result.error).toBe("This lootdrop has already been claimed."); + }); + + it("should fail if expired/not found", async () => { + mockReturning.mockResolvedValue([]); + + const mockWhereSelect = mock().mockResolvedValue([]); // Empty result + mockFrom.mockReturnValue({ where: mockWhereSelect }); + + const result = await lootdropService.tryClaim("1001", "123", "UserOne"); + + expect(result.success).toBe(false); + expect(result.error).toBe("This lootdrop has expired."); + }); + }); +});