import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test"; import { questService } from "@shared/modules/quest/quest.service"; import { userQuests } from "@db/schema"; import { eq, and } from "drizzle-orm"; import { economyService } from "@shared/modules/economy/economy.service"; import { levelingService } from "@shared/modules/leveling/leveling.service"; // Mock dependencies const mockFindFirst = mock(); const mockFindMany = mock(); const mockInsert = mock(); const mockUpdate = mock(); const mockDelete = mock(); const mockValues = mock(); const mockReturning = mock(); const mockSet = mock(); const mockWhere = mock(); const mockOnConflictDoNothing = mock(); // Chain setup mockInsert.mockReturnValue({ values: mockValues }); mockValues.mockReturnValue({ onConflictDoNothing: mockOnConflictDoNothing }); mockOnConflictDoNothing.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: { userQuests: { findFirst: mockFindFirst, findMany: mockFindMany }, quests: { findMany: mockFindMany }, }, insert: mockInsert, update: mockUpdate, delete: mockDelete, }); return { DrizzleClient: { ...createMockTx(), transaction: async (cb: any) => cb(createMockTx()), } }; }); describe("questService", () => { let mockModifyUserBalance: any; let mockAddXp: any; beforeEach(() => { mockFindFirst.mockReset(); mockFindMany.mockReset(); mockInsert.mockClear(); mockUpdate.mockClear(); mockValues.mockClear(); mockReturning.mockClear(); mockSet.mockClear(); mockWhere.mockClear(); mockOnConflictDoNothing.mockClear(); // Setup Spies mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any); mockAddXp = spyOn(levelingService, 'addXp').mockResolvedValue({} as any); }); afterEach(() => { mockModifyUserBalance.mockRestore(); mockAddXp.mockRestore(); }); describe("assignQuest", () => { it("should assign quest", async () => { mockReturning.mockResolvedValue([{ userId: 1n, questId: 101 }]); const result = await questService.assignQuest("1", 101); expect(result).toEqual([{ userId: 1n, questId: 101 }] as any); expect(mockInsert).toHaveBeenCalledWith(userQuests); expect(mockValues).toHaveBeenCalledWith({ userId: 1n, questId: 101, progress: 0 }); }); }); describe("updateProgress", () => { it("should update progress", async () => { mockReturning.mockResolvedValue([{ userId: 1n, questId: 101, progress: 50 }]); const result = await questService.updateProgress("1", 101, 50); expect(result).toEqual([{ userId: 1n, questId: 101, progress: 50 }] as any); expect(mockUpdate).toHaveBeenCalledWith(userQuests); expect(mockSet).toHaveBeenCalledWith({ progress: 50 }); }); }); describe("completeQuest", () => { it("should complete quest and grant rewards", async () => { const mockUserQuest = { userId: 1n, questId: 101, completedAt: null, quest: { rewards: { balance: 100, xp: 50 } } }; mockFindFirst.mockResolvedValue(mockUserQuest); const result = await questService.completeQuest("1", 101); expect(result.success).toBe(true); expect(result.rewards.balance).toBe(100n); expect(result.rewards.xp).toBe(50n); // Check updates expect(mockUpdate).toHaveBeenCalledWith(userQuests); expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) }); // Check service calls expect(mockModifyUserBalance).toHaveBeenCalledWith("1", 100n, 'QUEST_REWARD', expect.any(String), null, expect.anything()); expect(mockAddXp).toHaveBeenCalledWith("1", 50n, expect.anything()); }); it("should throw if quest not assigned", async () => { mockFindFirst.mockResolvedValue(null); expect(questService.completeQuest("1", 101)).rejects.toThrow("Quest not assigned"); }); it("should throw if already completed", async () => { mockFindFirst.mockResolvedValue({ completedAt: new Date() }); expect(questService.completeQuest("1", 101)).rejects.toThrow("Quest already completed"); }); }); describe("getUserQuests", () => { it("should return user quests", async () => { const mockData = [{ questId: 1 }, { questId: 2 }]; mockFindMany.mockResolvedValue(mockData); const result = await questService.getUserQuests("1"); expect(result).toEqual(mockData as any); }); }); describe("getAvailableQuests", () => { it("should return quests not yet accepted by user", async () => { // First call to findMany (userQuests) returns accepted quest IDs // Second call to findMany (quests) returns available quests mockFindMany .mockResolvedValueOnce([{ questId: 1 }]) // userQuests .mockResolvedValueOnce([{ id: 2, name: "New Quest" }]); // quests const result = await questService.getAvailableQuests("1"); expect(result).toEqual([{ id: 2, name: "New Quest" }] as any); expect(mockFindMany).toHaveBeenCalledTimes(2); }); it("should return all quests if user has no assigned quests", async () => { mockFindMany .mockResolvedValueOnce([]) // userQuests .mockResolvedValueOnce([{ id: 1 }, { id: 2 }]); // quests const result = await questService.getAvailableQuests("1"); expect(result).toEqual([{ id: 1 }, { id: 2 }] as any); }); }); describe("handleEvent", () => { it("should progress a quest with sub-events", async () => { const mockUserQuest = { userId: 1n, questId: 101, progress: 0, completedAt: null, quest: { triggerEvent: "ITEM_USE:101", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); mockReturning.mockResolvedValue([{ userId: 1n, questId: 101, progress: 1 }]); await questService.handleEvent("1", "ITEM_USE:101", 1); expect(mockUpdate).toHaveBeenCalled(); expect(mockSet).toHaveBeenCalledWith({ progress: 1 }); }); it("should complete a quest when target reached using sub-events", async () => { const mockUserQuest = { userId: 1n, questId: 101, progress: 4, completedAt: null, quest: { triggerEvent: "ITEM_COLLECT:505", requirements: { target: 5 }, rewards: { balance: 100 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); mockFindFirst.mockResolvedValue(mockUserQuest); // For completeQuest await questService.handleEvent("1", "ITEM_COLLECT:505", 1); // Verify completeQuest was called (it will update completedAt) expect(mockUpdate).toHaveBeenCalled(); expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) }); }); it("should progress a quest with generic events", async () => { const mockUserQuest = { userId: 1n, questId: 102, progress: 0, completedAt: null, quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); mockReturning.mockResolvedValue([{ userId: 1n, questId: 102, progress: 1 }]); await questService.handleEvent("1", "ITEM_COLLECT:505", 1); expect(mockUpdate).toHaveBeenCalled(); expect(mockSet).toHaveBeenCalledWith({ progress: 1 }); }); it("should ignore events that are not prefix matches", async () => { const mockUserQuest = { userId: 1n, questId: 103, progress: 0, completedAt: null, quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); await questService.handleEvent("1", "ITEM_COLLECT_UNRELATED", 1); expect(mockUpdate).not.toHaveBeenCalled(); }); it("should not progress a specific quest with a different specific event", async () => { const mockUserQuest = { userId: 1n, questId: 104, progress: 0, completedAt: null, quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); await questService.handleEvent("1", "ITEM_COLLECT:202", 1); expect(mockUpdate).not.toHaveBeenCalled(); }); it("should not progress a specific quest with a generic event", async () => { const mockUserQuest = { userId: 1n, questId: 105, progress: 0, completedAt: null, quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); await questService.handleEvent("1", "ITEM_COLLECT", 1); expect(mockUpdate).not.toHaveBeenCalled(); }); it("should ignore irrelevant events", async () => { const mockUserQuest = { userId: 1n, questId: 101, progress: 0, completedAt: null, quest: { triggerEvent: "DIFFERENT_EVENT", requirements: { target: 5 } } }; mockFindMany.mockResolvedValue([mockUserQuest]); await questService.handleEvent("1", "TEST_EVENT", 1); expect(mockUpdate).not.toHaveBeenCalled(); }); }); });