forked from syntaxbullet/AuroraBot-discord
refactor: initial moves
This commit is contained in:
151
shared/modules/quest/quest.service.test.ts
Normal file
151
shared/modules/quest/quest.service.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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 },
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
88
shared/modules/quest/quest.service.ts
Normal file
88
shared/modules/quest/quest.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { userQuests } from "@db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { UserError } from "@/lib/errors";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { economyService } from "@shared/modules/economy/economy.service";
|
||||
import { levelingService } from "@shared/modules/leveling/leveling.service";
|
||||
import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction } from "@shared/lib/types";
|
||||
import { TransactionType } from "@shared/lib/constants";
|
||||
|
||||
export const questService = {
|
||||
assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
return await txFn.insert(userQuests)
|
||||
.values({
|
||||
userId: BigInt(userId),
|
||||
questId: questId,
|
||||
progress: 0,
|
||||
})
|
||||
.onConflictDoNothing() // Ignore if already assigned
|
||||
.returning();
|
||||
}, tx);
|
||||
},
|
||||
|
||||
updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
return await txFn.update(userQuests)
|
||||
.set({ progress: progress })
|
||||
.where(and(
|
||||
eq(userQuests.userId, BigInt(userId)),
|
||||
eq(userQuests.questId, questId)
|
||||
))
|
||||
.returning();
|
||||
}, tx);
|
||||
},
|
||||
|
||||
completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const userQuest = await txFn.query.userQuests.findFirst({
|
||||
where: and(
|
||||
eq(userQuests.userId, BigInt(userId)),
|
||||
eq(userQuests.questId, questId)
|
||||
),
|
||||
with: {
|
||||
quest: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!userQuest) throw new UserError("Quest not assigned");
|
||||
if (userQuest.completedAt) throw new UserError("Quest already completed");
|
||||
|
||||
// Mark completed
|
||||
await txFn.update(userQuests)
|
||||
.set({ completedAt: new Date() })
|
||||
.where(and(
|
||||
eq(userQuests.userId, BigInt(userId)),
|
||||
eq(userQuests.questId, questId)
|
||||
));
|
||||
|
||||
// Distribute Rewards
|
||||
const rewards = userQuest.quest.rewards as { xp?: number, balance?: number };
|
||||
const results = { xp: 0n, balance: 0n };
|
||||
|
||||
if (rewards?.balance) {
|
||||
const bal = BigInt(rewards.balance);
|
||||
await economyService.modifyUserBalance(userId, bal, TransactionType.QUEST_REWARD, `Reward for quest ${questId}`, null, txFn);
|
||||
results.balance = bal;
|
||||
}
|
||||
|
||||
if (rewards?.xp) {
|
||||
const xp = BigInt(rewards.xp);
|
||||
await levelingService.addXp(userId, xp, txFn);
|
||||
results.xp = xp;
|
||||
}
|
||||
|
||||
return { success: true, rewards: results };
|
||||
}, tx);
|
||||
},
|
||||
|
||||
getUserQuests: async (userId: string) => {
|
||||
return await DrizzleClient.query.userQuests.findMany({
|
||||
where: eq(userQuests.userId, BigInt(userId)),
|
||||
with: {
|
||||
quest: true,
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user