forked from syntaxbullet/AuroraBot-discord
refactor: initial moves
This commit is contained in:
259
shared/modules/user/user.service.test.ts
Normal file
259
shared/modules/user/user.service.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
72
shared/modules/user/user.service.ts
Normal file
72
shared/modules/user/user.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { users } from "@db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction } from "@shared/lib/types";
|
||||
|
||||
export const userService = {
|
||||
getUserById: async (id: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
with: { class: true }
|
||||
});
|
||||
return user;
|
||||
},
|
||||
getUserByUsername: async (username: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) });
|
||||
return user;
|
||||
},
|
||||
getOrCreateUser: async (id: string, username: string, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
let user = await txFn.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
with: { class: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await txFn.insert(users).values({
|
||||
id: BigInt(id),
|
||||
username,
|
||||
}).returning();
|
||||
|
||||
// Re-query to get the user with class relation
|
||||
user = await txFn.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
with: { class: true }
|
||||
});
|
||||
}
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
getUserClass: async (id: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
with: { class: true }
|
||||
});
|
||||
return user?.class;
|
||||
},
|
||||
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [user] = await txFn.insert(users).values({
|
||||
id: BigInt(id),
|
||||
username,
|
||||
classId,
|
||||
}).returning();
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [user] = await txFn.update(users)
|
||||
.set(data)
|
||||
.where(eq(users.id, BigInt(id)))
|
||||
.returning();
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
deleteUser: async (id: string, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
await txFn.delete(users).where(eq(users.id, BigInt(id)));
|
||||
}, tx);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user