293 lines
11 KiB
TypeScript
293 lines
11 KiB
TypeScript
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<any[]> => Promise.resolve([])),
|
|
},
|
|
moderationCases: {
|
|
findMany: mock((): Promise<any[]> => 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);
|
|
});
|
|
});
|
|
});
|