test: add tests for economy service
This commit is contained in:
193
src/modules/economy/economy.service.test.ts
Normal file
193
src/modules/economy/economy.service.test.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { describe, it, expect, mock, beforeEach, setSystemTime } from "bun:test";
|
||||||
|
import { economyService } from "./economy.service";
|
||||||
|
import { users, userTimers, transactions } from "@/db/schema";
|
||||||
|
|
||||||
|
// Define mock functions
|
||||||
|
const mockFindMany = mock();
|
||||||
|
const mockFindFirst = mock();
|
||||||
|
const mockInsert = mock();
|
||||||
|
const mockUpdate = mock();
|
||||||
|
const mockDelete = mock();
|
||||||
|
const mockValues = mock();
|
||||||
|
const mockReturning = mock();
|
||||||
|
const mockSet = mock();
|
||||||
|
const mockWhere = mock();
|
||||||
|
const mockOnConflictDoUpdate = mock();
|
||||||
|
|
||||||
|
// Chainable mock setup
|
||||||
|
mockInsert.mockReturnValue({ values: mockValues });
|
||||||
|
mockValues.mockReturnValue({
|
||||||
|
returning: mockReturning,
|
||||||
|
onConflictDoUpdate: mockOnConflictDoUpdate // For claimDaily chain
|
||||||
|
});
|
||||||
|
mockOnConflictDoUpdate.mockResolvedValue({}); // Terminate the chain
|
||||||
|
|
||||||
|
mockUpdate.mockReturnValue({ set: mockSet });
|
||||||
|
mockSet.mockReturnValue({ where: mockWhere });
|
||||||
|
mockWhere.mockReturnValue({ returning: mockReturning });
|
||||||
|
|
||||||
|
// Mock DrizzleClient
|
||||||
|
mock.module("@/lib/DrizzleClient", () => {
|
||||||
|
// Mock Transaction Object Structure
|
||||||
|
const createMockTx = () => ({
|
||||||
|
query: {
|
||||||
|
users: { findFirst: mockFindFirst },
|
||||||
|
userTimers: { findFirst: mockFindFirst },
|
||||||
|
},
|
||||||
|
insert: mockInsert,
|
||||||
|
update: mockUpdate,
|
||||||
|
delete: mockDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
DrizzleClient: {
|
||||||
|
query: {
|
||||||
|
users: { findFirst: mockFindFirst },
|
||||||
|
userTimers: { findFirst: mockFindFirst },
|
||||||
|
},
|
||||||
|
insert: mockInsert,
|
||||||
|
update: mockUpdate,
|
||||||
|
delete: mockDelete,
|
||||||
|
transaction: async (cb: any) => {
|
||||||
|
return cb(createMockTx());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock Config
|
||||||
|
mock.module("@/lib/config", () => ({
|
||||||
|
config: {
|
||||||
|
economy: {
|
||||||
|
daily: {
|
||||||
|
amount: 100n,
|
||||||
|
streakBonus: 10n,
|
||||||
|
cooldownMs: 86400000, // 24 hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("economyService", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFindFirst.mockReset();
|
||||||
|
mockInsert.mockClear();
|
||||||
|
mockUpdate.mockClear();
|
||||||
|
mockDelete.mockClear();
|
||||||
|
mockValues.mockClear();
|
||||||
|
mockReturning.mockClear();
|
||||||
|
mockSet.mockClear();
|
||||||
|
mockWhere.mockClear();
|
||||||
|
mockOnConflictDoUpdate.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transfer", () => {
|
||||||
|
it("should transfer amount successfully", async () => {
|
||||||
|
const sender = { id: 1n, balance: 200n };
|
||||||
|
mockFindFirst.mockResolvedValue(sender);
|
||||||
|
|
||||||
|
const result = await economyService.transfer("1", "2", 50n);
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true, amount: 50n });
|
||||||
|
|
||||||
|
// Check sender update
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith(users);
|
||||||
|
// We can check if mockSet was called twice
|
||||||
|
expect(mockSet).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// Check transactions created
|
||||||
|
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if amount is non-positive", async () => {
|
||||||
|
expect(economyService.transfer("1", "2", 0n)).rejects.toThrow("Amount must be positive");
|
||||||
|
expect(economyService.transfer("1", "2", -10n)).rejects.toThrow("Amount must be positive");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if transferring to self", async () => {
|
||||||
|
expect(economyService.transfer("1", "1", 50n)).rejects.toThrow("Cannot transfer to self");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if sender not found", async () => {
|
||||||
|
mockFindFirst.mockResolvedValue(undefined);
|
||||||
|
expect(economyService.transfer("1", "2", 50n)).rejects.toThrow("Sender not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if insufficient funds", async () => {
|
||||||
|
mockFindFirst.mockResolvedValue({ id: 1n, balance: 20n });
|
||||||
|
expect(economyService.transfer("1", "2", 50n)).rejects.toThrow("Insufficient funds");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("claimDaily", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setSystemTime(new Date("2023-01-01T12:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should claim daily reward successfully", async () => {
|
||||||
|
const recentPast = new Date("2023-01-01T11:00:00Z"); // 1 hour ago
|
||||||
|
|
||||||
|
// First call finds cooldown (expired recently), second finds user
|
||||||
|
mockFindFirst
|
||||||
|
.mockResolvedValueOnce({ expiresAt: recentPast }) // Cooldown check - expired -> ready
|
||||||
|
.mockResolvedValueOnce({ id: 1n, dailyStreak: 5, balance: 1000n }); // User check
|
||||||
|
|
||||||
|
const result = await economyService.claimDaily("1");
|
||||||
|
|
||||||
|
expect(result.claimed).toBe(true);
|
||||||
|
// Streak should increase: 5 + 1 = 6
|
||||||
|
expect(result.streak).toBe(6);
|
||||||
|
// Base 100 + (6-1)*10 = 150
|
||||||
|
expect(result.amount).toBe(150n);
|
||||||
|
|
||||||
|
// Check updates
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith(users);
|
||||||
|
expect(mockInsert).toHaveBeenCalledWith(userTimers);
|
||||||
|
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if cooldown is active", async () => {
|
||||||
|
const future = new Date("2023-01-02T12:00:00Z"); // +24h
|
||||||
|
mockFindFirst.mockResolvedValue({ expiresAt: future });
|
||||||
|
|
||||||
|
expect(economyService.claimDaily("1")).rejects.toThrow("Daily already claimed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset streak if missed a day (long time gap)", async () => {
|
||||||
|
// Expired 3 days ago
|
||||||
|
const past = new Date("2023-01-01T00:00:00Z"); // now is 12:00
|
||||||
|
// Wait, logic says: if (timeSinceReady > 24h)
|
||||||
|
// now - expiresAt.
|
||||||
|
// If cooldown expired 2022-12-30. Now is 2023-01-01. Gap is > 24h.
|
||||||
|
|
||||||
|
const expiredAt = new Date("2022-12-30T12:00:00Z");
|
||||||
|
|
||||||
|
mockFindFirst
|
||||||
|
.mockResolvedValueOnce({ expiresAt: expiredAt })
|
||||||
|
.mockResolvedValueOnce({ id: 1n, dailyStreak: 5 });
|
||||||
|
|
||||||
|
const result = await economyService.claimDaily("1");
|
||||||
|
|
||||||
|
// timeSinceReady = 48h.
|
||||||
|
// streak = (5+1) - floor(48h / 24h) = 6 - 2 = 4.
|
||||||
|
expect(result.streak).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("modifyUserBalance", () => {
|
||||||
|
it("should add balance successfully", async () => {
|
||||||
|
mockReturning.mockResolvedValue([{ id: 1n, balance: 150n }]);
|
||||||
|
|
||||||
|
const result = await economyService.modifyUserBalance("1", 50n, "TEST", "Test add");
|
||||||
|
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith(users);
|
||||||
|
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if insufficient funds when negative", async () => {
|
||||||
|
mockFindFirst.mockResolvedValue({ id: 1n, balance: 20n });
|
||||||
|
|
||||||
|
expect(economyService.modifyUserBalance("1", -50n, "TEST", "Test sub")).rejects.toThrow("Insufficient funds");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user