forked from syntaxbullet/aurorabot
238 lines
8.4 KiB
TypeScript
238 lines
8.4 KiB
TypeScript
import { describe, it, expect, mock, beforeEach, setSystemTime } from "bun:test";
|
|
import { examService, ExamStatus } from "@shared/modules/economy/exam.service";
|
|
import { users, userTimers, transactions } from "@db/schema";
|
|
|
|
// Define mock functions
|
|
const mockFindFirst = mock();
|
|
const mockInsert = mock();
|
|
const mockUpdate = mock();
|
|
const mockValues = mock();
|
|
const mockReturning = mock();
|
|
const mockSet = mock();
|
|
const mockWhere = mock();
|
|
|
|
// Chainable mock setup
|
|
mockInsert.mockReturnValue({ values: mockValues });
|
|
mockValues.mockReturnValue({ returning: mockReturning });
|
|
mockUpdate.mockReturnValue({ set: mockSet });
|
|
mockSet.mockReturnValue({ where: mockWhere });
|
|
mockWhere.mockReturnValue({ returning: mockReturning });
|
|
|
|
// Mock DrizzleClient
|
|
mock.module("@shared/db/DrizzleClient", () => {
|
|
const createMockTx = () => ({
|
|
query: {
|
|
users: { findFirst: mockFindFirst },
|
|
userTimers: { findFirst: mockFindFirst },
|
|
},
|
|
insert: mockInsert,
|
|
update: mockUpdate,
|
|
});
|
|
|
|
return {
|
|
DrizzleClient: {
|
|
query: {
|
|
users: { findFirst: mockFindFirst },
|
|
userTimers: { findFirst: mockFindFirst },
|
|
},
|
|
insert: mockInsert,
|
|
update: mockUpdate,
|
|
transaction: async (cb: any) => {
|
|
return cb(createMockTx());
|
|
}
|
|
},
|
|
};
|
|
});
|
|
|
|
// Mock withTransaction
|
|
mock.module("@/lib/db", () => ({
|
|
withTransaction: async (cb: any, tx?: any) => {
|
|
if (tx) return cb(tx);
|
|
return cb({
|
|
query: {
|
|
users: { findFirst: mockFindFirst },
|
|
userTimers: { findFirst: mockFindFirst },
|
|
},
|
|
insert: mockInsert,
|
|
update: mockUpdate,
|
|
});
|
|
}
|
|
}));
|
|
|
|
// Mock Config
|
|
mock.module("@shared/lib/config", () => ({
|
|
config: {
|
|
economy: {
|
|
exam: {
|
|
multMin: 1.0,
|
|
multMax: 2.0,
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
// Mock User Service
|
|
mock.module("@shared/modules/user/user.service", () => ({
|
|
userService: {
|
|
getOrCreateUser: mock()
|
|
}
|
|
}));
|
|
|
|
// Mock Dashboard Service
|
|
mock.module("@shared/modules/dashboard/dashboard.service", () => ({
|
|
dashboardService: {
|
|
recordEvent: mock()
|
|
}
|
|
}));
|
|
|
|
describe("ExamService", () => {
|
|
beforeEach(() => {
|
|
mockFindFirst.mockReset();
|
|
mockInsert.mockClear();
|
|
mockUpdate.mockClear();
|
|
mockValues.mockClear();
|
|
mockReturning.mockClear();
|
|
mockSet.mockClear();
|
|
mockWhere.mockClear();
|
|
});
|
|
|
|
describe("getExamStatus", () => {
|
|
it("should return NOT_REGISTERED if no timer exists", async () => {
|
|
mockFindFirst.mockResolvedValue(undefined);
|
|
const status = await examService.getExamStatus("1");
|
|
expect(status.status).toBe(ExamStatus.NOT_REGISTERED);
|
|
});
|
|
|
|
it("should return COOLDOWN if now < expiresAt", async () => {
|
|
const now = new Date("2024-01-10T12:00:00Z");
|
|
setSystemTime(now);
|
|
const future = new Date("2024-01-11T00:00:00Z");
|
|
|
|
mockFindFirst.mockResolvedValue({
|
|
expiresAt: future,
|
|
metadata: { examDay: 3, lastXp: "100" }
|
|
});
|
|
|
|
const status = await examService.getExamStatus("1");
|
|
expect(status.status).toBe(ExamStatus.COOLDOWN);
|
|
expect(status.nextExamAt?.getTime()).toBe(future.setHours(0,0,0,0));
|
|
});
|
|
|
|
it("should return MISSED if it is the wrong day", async () => {
|
|
const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
|
|
setSystemTime(now);
|
|
const past = new Date("2024-01-10T00:00:00Z"); // Wednesday (3) last week
|
|
|
|
mockFindFirst.mockResolvedValue({
|
|
expiresAt: past,
|
|
metadata: { examDay: 3, lastXp: "100" } // Registered for Wednesday
|
|
});
|
|
|
|
const status = await examService.getExamStatus("1");
|
|
expect(status.status).toBe(ExamStatus.MISSED);
|
|
expect(status.examDay).toBe(3);
|
|
});
|
|
|
|
it("should return AVAILABLE if it is the correct day", async () => {
|
|
const now = new Date("2024-01-17T12:00:00Z"); // Wednesday (3)
|
|
setSystemTime(now);
|
|
const past = new Date("2024-01-10T00:00:00Z");
|
|
|
|
mockFindFirst.mockResolvedValue({
|
|
expiresAt: past,
|
|
metadata: { examDay: 3, lastXp: "100" }
|
|
});
|
|
|
|
const status = await examService.getExamStatus("1");
|
|
expect(status.status).toBe(ExamStatus.AVAILABLE);
|
|
expect(status.examDay).toBe(3);
|
|
expect(status.lastXp).toBe(100n);
|
|
});
|
|
});
|
|
|
|
describe("registerForExam", () => {
|
|
it("should create user and timer correctly", async () => {
|
|
const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
|
|
setSystemTime(now);
|
|
|
|
const { userService } = await import("@shared/modules/user/user.service");
|
|
(userService.getOrCreateUser as any).mockResolvedValue({ id: 1n, xp: 500n });
|
|
|
|
const result = await examService.registerForExam("1", "testuser");
|
|
|
|
expect(result.status).toBe(ExamStatus.NOT_REGISTERED);
|
|
expect(result.examDay).toBe(1);
|
|
|
|
expect(mockInsert).toHaveBeenCalledWith(userTimers);
|
|
expect(mockInsert).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("takeExam", () => {
|
|
it("should return NOT_REGISTERED if not registered", async () => {
|
|
mockFindFirst.mockResolvedValueOnce({ id: 1n }) // user check
|
|
.mockResolvedValueOnce(undefined); // timer check
|
|
|
|
const result = await examService.takeExam("1");
|
|
expect(result.status).toBe(ExamStatus.NOT_REGISTERED);
|
|
});
|
|
|
|
it("should handle missed exam and schedule for next exam day", async () => {
|
|
const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
|
|
setSystemTime(now);
|
|
const past = new Date("2024-01-10T00:00:00Z");
|
|
|
|
mockFindFirst.mockResolvedValueOnce({ id: 1n, xp: 600n }) // user
|
|
.mockResolvedValueOnce({
|
|
expiresAt: past,
|
|
metadata: { examDay: 3, lastXp: "500" } // Registered for Wednesday
|
|
}); // timer
|
|
|
|
const result = await examService.takeExam("1");
|
|
|
|
expect(result.status).toBe(ExamStatus.MISSED);
|
|
expect(result.examDay).toBe(3);
|
|
|
|
// Should set next exam to next Wednesday
|
|
// Monday (1) + 2 days = Wednesday (3)
|
|
const expected = new Date("2024-01-17T00:00:00Z");
|
|
expect(result.nextExamAt!.getTime()).toBe(expected.getTime());
|
|
|
|
expect(mockUpdate).toHaveBeenCalledWith(userTimers);
|
|
});
|
|
|
|
it("should calculate rewards and update state when passed", async () => {
|
|
const now = new Date("2024-01-17T12:00:00Z"); // Wednesday (3)
|
|
setSystemTime(now);
|
|
const past = new Date("2024-01-10T00:00:00Z");
|
|
|
|
mockFindFirst.mockResolvedValueOnce({ id: 1n, username: "testuser", xp: 1000n, balance: 0n }) // user
|
|
.mockResolvedValueOnce({
|
|
expiresAt: past,
|
|
metadata: { examDay: 3, lastXp: "500" }
|
|
}); // timer
|
|
|
|
const result = await examService.takeExam("1");
|
|
|
|
expect(result.status).toBe(ExamStatus.AVAILABLE);
|
|
expect(result.xpDiff).toBe(500n);
|
|
// Multiplier is between 1.0 and 2.0 based on mock config
|
|
expect(result.multiplier).toBeGreaterThanOrEqual(1.0);
|
|
expect(result.multiplier).toBeLessThanOrEqual(2.0);
|
|
expect(result.reward).toBeGreaterThanOrEqual(500n);
|
|
expect(result.reward).toBeLessThanOrEqual(1000n);
|
|
|
|
expect(mockUpdate).toHaveBeenCalledWith(userTimers);
|
|
expect(mockUpdate).toHaveBeenCalledWith(users);
|
|
|
|
// Verify transaction
|
|
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
|
expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
|
|
amount: result.reward,
|
|
userId: 1n,
|
|
type: expect.anything()
|
|
}));
|
|
});
|
|
});
|
|
});
|