diff --git a/src/modules/economy/economy.service.test.ts b/src/modules/economy/economy.service.test.ts index 5c1bcc5..8f21bde 100644 --- a/src/modules/economy/economy.service.test.ts +++ b/src/modules/economy/economy.service.test.ts @@ -173,10 +173,22 @@ describe("economyService", () => { it("should throw if cooldown is active", async () => { const future = new Date("2023-01-02T12:00:00Z"); // +24h mockFindFirst.mockResolvedValue({ expiresAt: future }); - expect(economyService.claimDaily("1")).rejects.toThrow("Daily already claimed"); }); + it("should set cooldown to next UTC midnight", async () => { + // 2023-01-01T12:00:00Z -> Should be 2023-01-02T00:00:00Z + + mockFindFirst + .mockResolvedValueOnce(undefined) // No cooldown + .mockResolvedValueOnce({ id: 1n, dailyStreak: 5, balance: 1000n }); + + const result = await economyService.claimDaily("1"); + + const expectedReset = new Date("2023-01-02T00:00:00Z"); + expect(result.nextReadyAt.toISOString()).toBe(expectedReset.toISOString()); + }); + it("should reset streak if missed a day (long time gap)", async () => { // Expired 3 days ago const past = new Date("2023-01-01T00:00:00Z"); // now is 12:00 @@ -193,8 +205,31 @@ describe("economyService", () => { const result = await economyService.claimDaily("1"); // timeSinceReady = 48h. - // streak = (5+1) - floor(48h / 24h) = 6 - 2 = 4. - expect(result.streak).toBe(4); + // streak should reset to 1 + expect(result.streak).toBe(1); + }); + + it("should prevent weekly bonus exploit by resetting streak", async () => { + // Mock user at streak 7. + // Mock time as 24h + 1m after expiry. + + const expiredAt = new Date("2023-01-01T11:59:00Z"); // now is 12:00 next day, plus 1 min gap? + // no, 'now' is 2023-01-01T12:00:00Z set in beforeEach + + // We want gap > 24h. + // If expiry was yesterday 11:59:59. Gap is 24h + 1s. + + const expiredAtExploit = new Date("2022-12-31T11:59:00Z"); // Over 24h ago + + mockFindFirst + .mockResolvedValueOnce({ expiresAt: expiredAtExploit }) + .mockResolvedValueOnce({ id: 1n, dailyStreak: 7 }); + + const result = await economyService.claimDaily("1"); + + // Should reset to 1 + expect(result.streak).toBe(1); + expect(result.isWeekly).toBe(false); }); }); diff --git a/src/modules/economy/economy.service.ts b/src/modules/economy/economy.service.ts index 8749b8e..c68d4f4 100644 --- a/src/modules/economy/economy.service.ts +++ b/src/modules/economy/economy.service.ts @@ -98,7 +98,7 @@ export const economyService = { if (cooldown) { const timeSinceReady = now.getTime() - cooldown.expiresAt.getTime(); if (timeSinceReady > 24 * 60 * 60 * 1000) { - streak = Math.max(1, streak - Math.floor(timeSinceReady / (24 * 60 * 60 * 1000))); + streak = 1; } } else { streak = 1; @@ -119,8 +119,10 @@ export const economyService = { }) .where(eq(users.id, BigInt(userId))); - // Set new cooldown (now + 24h) - const nextReadyAt = new Date(now.getTime() + config.economy.daily.cooldownMs); + // Set new cooldown (Next UTC Midnight) + const nextReadyAt = new Date(now); + nextReadyAt.setUTCDate(nextReadyAt.getUTCDate() + 1); + nextReadyAt.setUTCHours(0, 0, 0, 0); await txFn.insert(userTimers) .values({