import { describe, it, expect, mock, beforeEach } from "bun:test"; import { userService } from "@shared/modules/user/user.service"; // Define mock functions outside so we can control them in tests 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 }); // Mock DrizzleClient mock.module("@shared/db/DrizzleClient", () => { return { DrizzleClient: { query: { users: { findFirst: mockFindFirst, }, }, insert: mockInsert, update: mockUpdate, delete: mockDelete, transaction: async (cb: any) => { // Pass the mock client itself as the transaction object // This simplifies things as we use the same structure for tx and client return cb({ query: { users: { findFirst: mockFindFirst, }, }, insert: mockInsert, update: mockUpdate, delete: mockDelete, }); } }, }; }); // Mock withTransaction helper to use the same pattern as DrizzleClient.transaction mock.module("@/lib/db", () => { return { withTransaction: async (callback: any, tx?: any) => { if (tx) { return callback(tx); } // Simulate transaction by calling the callback with mock db return callback({ query: { users: { findFirst: mockFindFirst, }, }, insert: mockInsert, update: mockUpdate, delete: mockDelete, }); } }; }); describe("userService", () => { beforeEach(() => { mockFindFirst.mockReset(); mockInsert.mockClear(); mockValues.mockClear(); mockReturning.mockClear(); mockUpdate.mockClear(); mockSet.mockClear(); mockWhere.mockClear(); mockDelete.mockClear(); }); describe("getUserById", () => { it("should return a user when found", async () => { const mockUser = { id: 123n, username: "testuser", class: null }; mockFindFirst.mockResolvedValue(mockUser); const result = await userService.getUserById("123"); expect(result).toEqual(mockUser as any); expect(mockFindFirst).toHaveBeenCalledTimes(1); }); it("should return undefined when user not found", async () => { mockFindFirst.mockResolvedValue(undefined); const result = await userService.getUserById("999"); expect(result).toBeUndefined(); expect(mockFindFirst).toHaveBeenCalledTimes(1); }); }); describe("getUserByUsername", () => { it("should return user when username exists", async () => { const mockUser = { id: 456n, username: "alice", balance: 100n }; mockFindFirst.mockResolvedValue(mockUser); const result = await userService.getUserByUsername("alice"); expect(result).toEqual(mockUser as any); expect(mockFindFirst).toHaveBeenCalledTimes(1); }); it("should return undefined when username not found", async () => { mockFindFirst.mockResolvedValue(undefined); const result = await userService.getUserByUsername("nonexistent"); expect(result).toBeUndefined(); }); }); describe("getUserClass", () => { it("should return user class when user has a class", async () => { const mockClass = { id: 1n, name: "Warrior", emoji: "⚔️" }; const mockUser = { id: 123n, username: "testuser", class: mockClass }; mockFindFirst.mockResolvedValue(mockUser); const result = await userService.getUserClass("123"); expect(result).toEqual(mockClass as any); }); it("should return null when user has no class", async () => { const mockUser = { id: 123n, username: "testuser", class: null }; mockFindFirst.mockResolvedValue(mockUser); const result = await userService.getUserClass("123"); expect(result).toBeNull(); }); it("should return undefined when user not found", async () => { mockFindFirst.mockResolvedValue(undefined); const result = await userService.getUserClass("999"); expect(result).toBeUndefined(); }); }); describe("getOrCreateUser (withTransaction)", () => { it("should return existing user if found", async () => { const mockUser = { id: 123n, username: "existinguser", class: null }; mockFindFirst.mockResolvedValue(mockUser); const result = await userService.getOrCreateUser("123", "existinguser"); expect(result).toEqual(mockUser as any); expect(mockFindFirst).toHaveBeenCalledTimes(1); expect(mockInsert).not.toHaveBeenCalled(); }); it("should create new user if not found", async () => { const newUser = { id: 789n, username: "newuser", classId: null }; // First call returns undefined (user not found) // Second call returns the newly created user (after insert + re-query) mockFindFirst .mockResolvedValueOnce(undefined) .mockResolvedValueOnce({ id: 789n, username: "newuser", class: null }); mockReturning.mockResolvedValue([newUser]); const result = await userService.getOrCreateUser("789", "newuser"); expect(mockInsert).toHaveBeenCalledTimes(1); expect(mockValues).toHaveBeenCalledWith({ id: 789n, username: "newuser" }); // Should query twice: once to check, once after insert expect(mockFindFirst).toHaveBeenCalledTimes(2); }); }); describe("createUser (withTransaction)", () => { it("should create and return a new user", async () => { const newUser = { id: 456n, username: "newuser", classId: null }; mockReturning.mockResolvedValue([newUser]); const result = await userService.createUser("456", "newuser"); expect(result).toEqual(newUser as any); expect(mockInsert).toHaveBeenCalledTimes(1); expect(mockValues).toHaveBeenCalledWith({ id: 456n, username: "newuser", classId: undefined }); }); it("should create user with classId when provided", async () => { const newUser = { id: 999n, username: "warrior", classId: 5n }; mockReturning.mockResolvedValue([newUser]); const result = await userService.createUser("999", "warrior", 5n); expect(result).toEqual(newUser as any); expect(mockValues).toHaveBeenCalledWith({ id: 999n, username: "warrior", classId: 5n }); }); }); describe("updateUser (withTransaction)", () => { it("should update user data", async () => { const updatedUser = { id: 123n, username: "testuser", balance: 500n }; mockReturning.mockResolvedValue([updatedUser]); const result = await userService.updateUser("123", { balance: 500n }); expect(result).toEqual(updatedUser as any); expect(mockUpdate).toHaveBeenCalledTimes(1); expect(mockSet).toHaveBeenCalledWith({ balance: 500n }); }); it("should update multiple fields", async () => { const updatedUser = { id: 456n, username: "alice", xp: 100n, level: 5 }; mockReturning.mockResolvedValue([updatedUser]); const result = await userService.updateUser("456", { xp: 100n, level: 5 }); expect(result).toEqual(updatedUser as any); expect(mockSet).toHaveBeenCalledWith({ xp: 100n, level: 5 }); }); }); describe("deleteUser (withTransaction)", () => { it("should delete user from database", async () => { mockWhere.mockResolvedValue(undefined); await userService.deleteUser("123"); expect(mockDelete).toHaveBeenCalledTimes(1); expect(mockWhere).toHaveBeenCalledTimes(1); }); }); });