import { describe, test, expect, mock, beforeEach } from "bun:test"; import { dashboardService } from "./dashboard.service"; // Mock DrizzleClient const mockSelect = mock(() => ({ from: mock(() => Promise.resolve([{ count: "5" }])), })); const mockQuery = { transactions: { findMany: mock((): Promise => Promise.resolve([])), }, moderationCases: { findMany: mock((): Promise => Promise.resolve([])), }, }; mock.module("@shared/db/DrizzleClient", () => ({ DrizzleClient: { select: mockSelect, query: mockQuery, }, })); describe("dashboardService", () => { beforeEach(() => { mockSelect.mockClear(); mockQuery.transactions.findMany.mockClear(); mockQuery.moderationCases.findMany.mockClear(); // Reset default mock implementation mockSelect.mockImplementation(() => ({ from: mock(() => Promise.resolve([{ count: "5" }])), })); }); describe("getActiveUserCount", () => { test("should return active user count from database", async () => { mockSelect.mockImplementationOnce(() => ({ // @ts-ignore ts(2322) from: mock(() => ({ where: mock(() => Promise.resolve([{ count: "5" }])), })), })); const count = await dashboardService.getActiveUserCount(); expect(count).toBe(5); expect(mockSelect).toHaveBeenCalled(); }); test("should return 0 when no users found", async () => { mockSelect.mockImplementationOnce(() => ({ // @ts-ignore ts(2322) from: mock(() => ({ where: mock(() => Promise.resolve([{ count: "0" }])), })), })); const count = await dashboardService.getActiveUserCount(); expect(count).toBe(0); }); }); describe("getTotalUserCount", () => { test("should return total user count", async () => { const count = await dashboardService.getTotalUserCount(); expect(count).toBe(5); }); }); describe("getRecentTransactions", () => { test("should return formatted transaction events", async () => { const mockTx = [ { type: "DAILY_REWARD", description: "Daily reward", createdAt: new Date(), user: { username: "testuser" }, }, ] as any; mockQuery.transactions.findMany.mockResolvedValueOnce(mockTx); const events = await dashboardService.getRecentTransactions(10); expect(events).toHaveLength(1); expect(events[0]?.type).toBe("info"); expect(events[0]?.message).toContain("testuser"); expect(events[0]?.icon).toBe("☀️"); }); test("should handle empty transactions", async () => { mockQuery.transactions.findMany.mockResolvedValueOnce([]); const events = await dashboardService.getRecentTransactions(10); expect(events).toHaveLength(0); }); }); describe("getRecentModerationCases", () => { test("should return formatted moderation events", async () => { const mockCases = [ { type: "warn", username: "baduser", reason: "Spam", createdAt: new Date(), }, ] as any; mockQuery.moderationCases.findMany.mockResolvedValueOnce(mockCases); const events = await dashboardService.getRecentModerationCases(10); expect(events).toHaveLength(1); expect(events[0]?.type).toBe("error"); expect(events[0]?.message).toContain("WARN"); expect(events[0]?.message).toContain("baduser"); expect(events[0]?.icon).toBe("⚠️"); }); }); describe("getRecentEvents", () => { test("should combine and sort transactions and moderation events", async () => { const now = new Date(); const earlier = new Date(now.getTime() - 1000); mockQuery.transactions.findMany.mockResolvedValueOnce([ { type: "DAILY_REWARD", description: "Daily", createdAt: now, user: { username: "user1" }, }, ] as unknown as any[]); mockQuery.moderationCases.findMany.mockResolvedValueOnce([ { type: "warn", username: "user2", reason: "Test", createdAt: earlier, }, ] as unknown as any[]); const events = await dashboardService.getRecentEvents(10); expect(events).toHaveLength(2); // Should be sorted by timestamp (newest first) const t0 = events[0]?.timestamp instanceof Date ? events[0].timestamp.getTime() : new Date(events[0]?.timestamp ?? 0).getTime(); const t1 = events[1]?.timestamp instanceof Date ? events[1].timestamp.getTime() : new Date(events[1]?.timestamp ?? 0).getTime(); expect(t0).toBeGreaterThanOrEqual(t1); }); }); describe("recordEvent", () => { test("should emit NEW_EVENT to systemEvents", async () => { const mockEmit = mock((_event: string, _data: unknown) => { }); mock.module("@shared/lib/events", () => ({ systemEvents: { emit: mockEmit, }, EVENTS: { DASHBOARD: { NEW_EVENT: "dashboard:new_event", } } })); await dashboardService.recordEvent({ type: 'info', message: 'Test Event', icon: '🚀' }); expect(mockEmit).toHaveBeenCalled(); const calls = mockEmit.mock.calls; if (calls.length > 0 && calls[0]) { expect(calls[0][0]).toBe("dashboard:new_event"); const data = calls[0][1] as { message: string, timestamp: string }; expect(data.message).toBe("Test Event"); expect(data.timestamp).toBeDefined(); // Verify it's an ISO string expect(() => new Date(data.timestamp).toISOString()).not.toThrow(); } else { throw new Error("mockEmit was not called with expected arguments"); } }); }); describe("getActivityAggregation", () => { test("should return exactly 24 data points representing the last 24 hours", async () => { const now = new Date(); now.setHours(now.getHours(), 0, 0, 0); mockSelect.mockImplementationOnce(() => ({ // @ts-ignore from: mock(() => ({ where: mock(() => ({ groupBy: mock(() => ({ orderBy: mock(() => Promise.resolve([ { hour: now.toISOString(), transactions: "10", commands: "5" } ])) })) })) })) })); const activity = await dashboardService.getActivityAggregation(); expect(activity).toHaveLength(24); // Check if the current hour matches our mock const currentHourData = activity.find(a => new Date(a.hour).getTime() === now.getTime()); expect(currentHourData).toBeDefined(); expect(currentHourData?.transactions).toBe(10); expect(currentHourData?.commands).toBe(5); // Check if missing hours are filled with 0 const otherHour = activity.find(a => new Date(a.hour).getTime() !== now.getTime()); expect(otherHour?.transactions).toBe(0); expect(otherHour?.commands).toBe(0); }); test("should return 24 hours of zeros if database is empty", async () => { mockSelect.mockImplementationOnce(() => ({ // @ts-ignore from: mock(() => ({ where: mock(() => ({ groupBy: mock(() => ({ orderBy: mock(() => Promise.resolve([])) })) })) })) })); const activity = await dashboardService.getActivityAggregation(); expect(activity).toHaveLength(24); expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true); }); test("should return 24 hours of zeros if database returns rows with null hours", async () => { mockSelect.mockImplementationOnce(() => ({ // @ts-ignore from: mock(() => ({ where: mock(() => ({ groupBy: mock(() => ({ orderBy: mock(() => Promise.resolve([{ hour: null, transactions: "10", commands: "5" }])) })) })) })) })); const activity = await dashboardService.getActivityAggregation(); expect(activity).toHaveLength(24); expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true); }); test("should correctly map hours regardless of input sort order", async () => { const now = new Date(); now.setHours(now.getHours(), 0, 0, 0); const hourAgo = new Date(now.getTime() - 60 * 60 * 1000); mockSelect.mockImplementationOnce(() => ({ // @ts-ignore from: mock(() => ({ where: mock(() => ({ groupBy: mock(() => ({ orderBy: mock(() => Promise.resolve([ { hour: now.toISOString(), transactions: "10", commands: "5" }, { hour: hourAgo.toISOString(), transactions: "20", commands: "10" } ])) })) })) })) })); const activity = await dashboardService.getActivityAggregation(); const current = activity.find(a => a.hour === now.toISOString()); const past = activity.find(a => a.hour === hourAgo.toISOString()); expect(current?.transactions).toBe(10); expect(past?.transactions).toBe(20); }); }); });