forked from syntaxbullet/AuroraBot-discord
test: add tests for lootdrop service
This commit is contained in:
222
src/modules/economy/lootdrop.service.test.ts
Normal file
222
src/modules/economy/lootdrop.service.test.ts
Normal file
@@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user