fix(dash): resolve test regressions, await promises, and improve TypeScript strictness

This commit is contained in:
syntaxbullet
2026-01-08 21:12:41 +01:00
parent 8253de9f73
commit 3f3a6c88e8
4 changed files with 78 additions and 31 deletions

View File

@@ -8,10 +8,10 @@ const mockSelect = mock(() => ({
const mockQuery = { const mockQuery = {
transactions: { transactions: {
findMany: mock(() => Promise.resolve([])), findMany: mock((): Promise<any[]> => Promise.resolve([])),
}, },
moderationCases: { moderationCases: {
findMany: mock(() => Promise.resolve([])), findMany: mock((): Promise<any[]> => Promise.resolve([])),
}, },
}; };
@@ -133,7 +133,7 @@ describe("dashboardService", () => {
createdAt: now, createdAt: now,
user: { username: "user1" }, user: { username: "user1" },
}, },
] as any); ] as unknown as any[]);
mockQuery.moderationCases.findMany.mockResolvedValueOnce([ mockQuery.moderationCases.findMany.mockResolvedValueOnce([
{ {
@@ -142,7 +142,7 @@ describe("dashboardService", () => {
reason: "Test", reason: "Test",
createdAt: earlier, createdAt: earlier,
}, },
] as any); ] as unknown as any[]);
const events = await dashboardService.getRecentEvents(10); const events = await dashboardService.getRecentEvents(10);
@@ -156,7 +156,7 @@ describe("dashboardService", () => {
describe("recordEvent", () => { describe("recordEvent", () => {
test("should emit NEW_EVENT to systemEvents", async () => { test("should emit NEW_EVENT to systemEvents", async () => {
const mockEmit = mock((_event: string, _data: any) => { }); const mockEmit = mock((_event: string, _data: unknown) => { });
mock.module("@shared/lib/events", () => ({ mock.module("@shared/lib/events", () => ({
systemEvents: { systemEvents: {

View File

@@ -63,7 +63,7 @@ export const economyService = {
// Record dashboard event // Record dashboard event
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
dashboardService.recordEvent({ await dashboardService.recordEvent({
type: 'info', type: 'info',
message: `${sender.username} transferred ${amount.toLocaleString()} AU to User ID ${toUserId}`, message: `${sender.username} transferred ${amount.toLocaleString()} AU to User ID ${toUserId}`,
icon: '💸' icon: '💸'
@@ -159,7 +159,7 @@ export const economyService = {
// Record dashboard event // Record dashboard event
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
dashboardService.recordEvent({ await dashboardService.recordEvent({
type: 'success', type: 'success',
message: `${user.username} claimed daily reward: ${totalReward.toLocaleString()} AU`, message: `${user.username} claimed daily reward: ${totalReward.toLocaleString()} AU`,
icon: '☀️' icon: '☀️'

View File

@@ -1,17 +1,46 @@
import { describe, test, expect, afterAll, mock } from "bun:test"; import { describe, test, expect, afterAll, mock } from "bun:test";
import type { WebServerInstance } from "./server";
import { createWebServer } from "./server";
// Mock the services directly to stay focused on server limits interface MockBotStats {
mock.module("@shared/modules/dashboard/dashboard.service", () => ({ bot: { name: string; avatarUrl: string | null };
dashboardService: { guilds: number;
getActiveUserCount: mock(() => Promise.resolve(5)), ping: number;
getTotalUserCount: mock(() => Promise.resolve(10)), cachedUsers: number;
getEconomyStats: mock(() => Promise.resolve({ totalWealth: 1000n, avgLevel: 5, topStreak: 2 })), commandsRegistered: number;
getRecentEvents: mock(() => Promise.resolve([])), uptime: number;
lastCommandTimestamp: number | null;
} }
}));
// 1. Mock DrizzleClient (dependency of dashboardService)
mock.module("@shared/db/DrizzleClient", () => {
const mockBuilder = {
where: mock(() => Promise.resolve([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }])),
then: (onfulfilled: any) => onfulfilled([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }]),
};
const mockFrom = {
from: mock(() => mockBuilder),
};
return {
DrizzleClient: {
select: mock(() => mockFrom),
query: {
transactions: { findMany: mock(() => Promise.resolve([])) },
moderationCases: { findMany: mock(() => Promise.resolve([])) },
users: {
findFirst: mock(() => Promise.resolve({ username: "test" })),
findMany: mock(() => Promise.resolve([])),
},
}
},
};
});
// 2. Mock Bot Stats Provider
mock.module("../../bot/lib/clientStats", () => ({ mock.module("../../bot/lib/clientStats", () => ({
getClientStats: mock(() => ({ getClientStats: mock((): MockBotStats => ({
bot: { name: "TestBot", avatarUrl: null }, bot: { name: "TestBot", avatarUrl: null },
guilds: 5, guilds: 5,
ping: 42, ping: 42,
@@ -22,17 +51,22 @@ mock.module("../../bot/lib/clientStats", () => ({
})), })),
})); }));
// Ensure @shared/lib/events is mocked if needed // 3. Mock System Events
mock.module("@shared/lib/events", () => ({ mock.module("@shared/lib/events", () => ({
systemEvents: { on: mock(() => { }) }, systemEvents: {
EVENTS: { DASHBOARD: { NEW_EVENT: "dashboard:new_event" } } on: mock(() => { }),
emit: mock(() => { }),
},
EVENTS: {
DASHBOARD: {
NEW_EVENT: "dashboard:new_event",
}
}
})); }));
import { createWebServer } from "./server";
describe("WebServer Security & Limits", () => { describe("WebServer Security & Limits", () => {
const port = 3001; const port = 3001;
let serverInstance: any; let serverInstance: WebServerInstance | null = null;
afterAll(async () => { afterAll(async () => {
if (serverInstance) { if (serverInstance) {
@@ -50,16 +84,20 @@ describe("WebServer Security & Limits", () => {
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
sockets.push(ws); sockets.push(ws);
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 5));
} }
// Give connections time to settle // Give connections time to settle
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 800));
// Should be exactly 10 or less const pendingCount = serverInstance.server.pendingWebSockets;
expect(serverInstance.server.pendingWebSockets).toBeLessThanOrEqual(10); expect(pendingCount).toBeLessThanOrEqual(10);
} finally { } finally {
sockets.forEach(s => s.close()); sockets.forEach(s => {
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING) {
s.close();
}
});
} }
}); });
@@ -69,7 +107,7 @@ describe("WebServer Security & Limits", () => {
} }
const response = await fetch(`http://localhost:${port}/api/health`); const response = await fetch(`http://localhost:${port}/api/health`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as { status: string };
expect(data.status).toBe("ok"); expect(data.status).toBe("ok");
}); });
}); });

View File

@@ -151,8 +151,17 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
}, },
async message(ws, message) { async message(ws, message) {
try { try {
const messageStr = message.toString();
// Defense-in-depth: redundant length check before parsing
if (messageStr.length > MAX_PAYLOAD_BYTES) {
console.error("❌ [WS] Payload exceeded maximum limit");
return;
}
const rawData = JSON.parse(messageStr);
const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types"); const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types");
const parsed = WsMessageSchema.safeParse(JSON.parse(message.toString())); const parsed = WsMessageSchema.safeParse(rawData);
if (!parsed.success) { if (!parsed.success) {
console.error("❌ [WS] Invalid message format:", parsed.error.issues); console.error("❌ [WS] Invalid message format:", parsed.error.issues);
@@ -163,7 +172,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
ws.send(JSON.stringify({ type: "PONG" })); ws.send(JSON.stringify({ type: "PONG" }));
} }
} catch (e) { } catch (e) {
console.error("❌ [WS] Failed to parse message:", e); console.error("❌ [WS] Failed to handle message:", e instanceof Error ? e.message : "Malformed JSON");
} }
}, },
close(ws) { close(ws) {