diff --git a/src/modules/class/class.service.test.ts b/src/modules/class/class.service.test.ts new file mode 100644 index 0000000..4e0e591 --- /dev/null +++ b/src/modules/class/class.service.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect, mock, beforeEach } from "bun:test"; +import { classService } from "./class.service"; +import { classes, users } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; + +// 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(); + +// Chainable mock setup +mockInsert.mockReturnValue({ values: mockValues }); +mockValues.mockReturnValue({ returning: mockReturning }); + +mockUpdate.mockReturnValue({ set: mockSet }); +mockSet.mockReturnValue({ where: mockWhere }); +mockWhere.mockReturnValue({ returning: mockReturning }); + +mockDelete.mockReturnValue({ where: mockWhere }); // Fix for delete chaining if needed, usually delete(table).where(...) + +// Mock DrizzleClient +mock.module("@/lib/DrizzleClient", () => { + return { + DrizzleClient: { + query: { + classes: { + findMany: mockFindMany, + findFirst: mockFindFirst, + }, + }, + insert: mockInsert, + update: mockUpdate, + delete: mockDelete, + transaction: async (cb: any) => { + return cb({ + query: { + classes: { + findMany: mockFindMany, + findFirst: mockFindFirst, + }, + }, + insert: mockInsert, + update: mockUpdate, + delete: mockDelete, + }); + } + }, + }; +}); + +describe("classService", () => { + beforeEach(() => { + mockFindMany.mockReset(); + mockFindFirst.mockReset(); + mockInsert.mockClear(); + mockUpdate.mockClear(); + mockDelete.mockClear(); + mockValues.mockClear(); + mockReturning.mockClear(); + mockSet.mockClear(); + mockWhere.mockClear(); + }); + + describe("getAllClasses", () => { + it("should return all classes", async () => { + const mockClasses = [{ id: 1n, name: "Warrior" }, { id: 2n, name: "Mage" }]; + mockFindMany.mockResolvedValue(mockClasses); + + const result = await classService.getAllClasses(); + + expect(result).toEqual(mockClasses as any); + expect(mockFindMany).toHaveBeenCalledTimes(1); + }); + }); + + describe("assignClass", () => { + it("should assign class to user if class exists", async () => { + const mockClass = { id: 1n, name: "Warrior" }; + const mockUser = { id: 123n, classId: 1n }; + + mockFindFirst.mockResolvedValue(mockClass); + mockReturning.mockResolvedValue([mockUser]); + + const result = await classService.assignClass("123", 1n); + + expect(result).toEqual(mockUser as any); + // Verify class check + expect(mockFindFirst).toHaveBeenCalledTimes(1); + // Verify update + expect(mockUpdate).toHaveBeenCalledWith(users); + expect(mockSet).toHaveBeenCalledWith({ classId: 1n }); + }); + + it("should throw error if class not found", async () => { + mockFindFirst.mockResolvedValue(undefined); + + expect(classService.assignClass("123", 99n)).rejects.toThrow("Class not found"); + }); + }); + + describe("getClassBalance", () => { + it("should return class balance", async () => { + const mockClass = { id: 1n, balance: 100n }; + mockFindFirst.mockResolvedValue(mockClass); + + const result = await classService.getClassBalance(1n); + + expect(result).toBe(100n); + }); + + it("should return 0n if class has no balance or not found", async () => { + mockFindFirst.mockResolvedValue(null); + const result = await classService.getClassBalance(1n); + expect(result).toBe(0n); + + mockFindFirst.mockResolvedValue({ id: 1n, balance: null }); + const result2 = await classService.getClassBalance(1n); + expect(result2).toBe(0n); + }); + }); + + describe("modifyClassBalance", () => { + it("should modify class balance successfully", async () => { + const mockClass = { id: 1n, balance: 100n }; + const updatedClass = { id: 1n, balance: 150n }; + + mockFindFirst.mockResolvedValue(mockClass); + mockReturning.mockResolvedValue([updatedClass]); + + const result = await classService.modifyClassBalance(1n, 50n); + + expect(result).toEqual(updatedClass as any); + expect(mockUpdate).toHaveBeenCalledWith(classes); + // Note: sql template literal matching might be tricky, checking strict call might fail if not exact object ref + // We verify at least mockSet was called + expect(mockSet).toHaveBeenCalled(); + }); + + it("should throw if class not found", async () => { + mockFindFirst.mockResolvedValue(undefined); + expect(classService.modifyClassBalance(1n, 50n)).rejects.toThrow("Class not found"); + }); + + it("should throw if insufficient funds", async () => { + const mockClass = { id: 1n, balance: 10n }; + mockFindFirst.mockResolvedValue(mockClass); + + expect(classService.modifyClassBalance(1n, -20n)).rejects.toThrow("Insufficient class funds"); + }); + }); + + describe("updateClass", () => { + it("should update class details", async () => { + const updateData = { name: "Super Warrior" }; + const updatedClass = { id: 1n, name: "Super Warrior" }; + + mockReturning.mockResolvedValue([updatedClass]); + + const result = await classService.updateClass(1n, updateData); + + expect(result).toEqual(updatedClass as any); + expect(mockUpdate).toHaveBeenCalledWith(classes); + expect(mockSet).toHaveBeenCalledWith(updateData); + }); + }); + + describe("createClass", () => { + it("should create a new class", async () => { + const newClassData = { name: "Archer", description: "Bow user" }; + const createdClass = { id: 3n, ...newClassData }; + + mockReturning.mockResolvedValue([createdClass]); + + const result = await classService.createClass(newClassData as any); + + expect(result).toEqual(createdClass as any); + expect(mockInsert).toHaveBeenCalledWith(classes); + expect(mockValues).toHaveBeenCalledWith(newClassData); + }); + }); + + describe("deleteClass", () => { + it("should delete a class", async () => { + mockDelete.mockReturnValue({ where: mockWhere }); + // The chain is delete(table).where(...) for delete + + // Wait, in user.service.test.ts: + // mockDelete called without chain setup in the file provided? + // "mockDelete = mock()" + // And in mock: "delete: mockDelete" + // And in usage: "await txFn.delete(users).where(...)" + // So mockDelete must return an object with where. + + mockDelete.mockReturnValue({ where: mockWhere }); + + await classService.deleteClass(1n); + + expect(mockDelete).toHaveBeenCalledWith(classes); + // We can't easily check 'where' arguments specifically without complex matcher if we don't return specific mock + expect(mockWhere).toHaveBeenCalled(); + }); + }); +});