Files
AuroraBot-discord/src/modules/economy/lootdrop.service.test.ts

217 lines
7.3 KiB
TypeScript

import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
import { lootdropService } from "./lootdrop.service";
import { lootdrops } from "@/db/schema";
import { economyService } from "./economy.service";
// 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 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;
let mockModifyUserBalance: any;
beforeEach(() => {
mockInsert.mockClear();
mockUpdate.mockClear();
mockDelete.mockClear();
mockValues.mockClear();
mockReturning.mockClear();
mockSet.mockClear();
mockWhere.mockClear();
mockSelect.mockClear();
mockFrom.mockClear();
// Reset internal state
(lootdropService as any).channelActivity = new Map();
(lootdropService as any).channelCooldowns = new Map();
// Mock Math.random
originalRandom = Math.random;
// Spy
mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any);
});
afterEach(() => {
Math.random = originalRandom;
mockModifyUserBalance.mockRestore();
});
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.");
});
});
});