refactor: rename web/ to api/ to better reflect its purpose
Some checks failed
Deploy to Production / test (push) Failing after 30s
Some checks failed
Deploy to Production / test (push) Failing after 30s
The web/ folder contains the REST API, WebSocket server, and OAuth routes — not a web frontend. Renaming to api/ clarifies this distinction since the actual web frontend lives in panel/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
153
api/src/server.test.ts
Normal file
153
api/src/server.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { describe, test, expect, afterAll, mock } from "bun:test";
|
||||
import type { WebServerInstance } from "./server";
|
||||
|
||||
interface MockBotStats {
|
||||
bot: { name: string; avatarUrl: string | null };
|
||||
guilds: number;
|
||||
ping: number;
|
||||
cachedUsers: number;
|
||||
commandsRegistered: number;
|
||||
uptime: number;
|
||||
lastCommandTimestamp: number | null;
|
||||
}
|
||||
|
||||
// 1. Mock DrizzleClient (dependency of dashboardService)
|
||||
// Must provide full chainable builder for select().from().leftJoin().groupBy().orderBy().limit()
|
||||
mock.module("@shared/db/DrizzleClient", () => {
|
||||
const mockBuilder: Record<string, any> = {};
|
||||
// Every chainable method returns mock builder; terminal calls return resolved promise
|
||||
mockBuilder.where = mock(() => Promise.resolve([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }]));
|
||||
mockBuilder.then = (onfulfilled: any) => onfulfilled([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }]);
|
||||
mockBuilder.orderBy = mock(() => mockBuilder);
|
||||
mockBuilder.limit = mock(() => Promise.resolve([]));
|
||||
mockBuilder.leftJoin = mock(() => mockBuilder);
|
||||
mockBuilder.groupBy = mock(() => mockBuilder);
|
||||
mockBuilder.from = mock(() => mockBuilder);
|
||||
|
||||
return {
|
||||
DrizzleClient: {
|
||||
select: mock(() => mockBuilder),
|
||||
query: {
|
||||
transactions: { findMany: mock(() => Promise.resolve([])) },
|
||||
moderationCases: { findMany: mock(() => Promise.resolve([])) },
|
||||
users: {
|
||||
findFirst: mock(() => Promise.resolve({ username: "test" })),
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
},
|
||||
lootdrops: { findMany: mock(() => Promise.resolve([])) },
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Mock Bot Stats Provider
|
||||
mock.module("../../bot/lib/clientStats", () => ({
|
||||
getClientStats: mock((): MockBotStats => ({
|
||||
bot: { name: "TestBot", avatarUrl: null },
|
||||
guilds: 5,
|
||||
ping: 42,
|
||||
cachedUsers: 100,
|
||||
commandsRegistered: 10,
|
||||
uptime: 3600,
|
||||
lastCommandTimestamp: Date.now(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// 3. Mock config (used by lootdrop.service.getLootdropState)
|
||||
mock.module("@shared/lib/config", () => ({
|
||||
config: {
|
||||
lootdrop: {
|
||||
activityWindowMs: 120000,
|
||||
minMessages: 1,
|
||||
spawnChance: 1,
|
||||
cooldownMs: 3000,
|
||||
reward: { min: 40, max: 150, currency: "Astral Units" }
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 4. Mock BotClient (used by stats helper for maintenanceMode)
|
||||
mock.module("../../bot/lib/BotClient", () => ({
|
||||
AuroraClient: {
|
||||
maintenanceMode: false,
|
||||
guilds: { cache: { get: () => null } },
|
||||
commands: [],
|
||||
knownCommands: new Map(),
|
||||
}
|
||||
}));
|
||||
|
||||
// Import after all mocks are set up
|
||||
import { createWebServer } from "./server";
|
||||
|
||||
describe("WebServer Security & Limits", () => {
|
||||
const port = 3001;
|
||||
const hostname = "127.0.0.1";
|
||||
let serverInstance: WebServerInstance | null = null;
|
||||
|
||||
afterAll(async () => {
|
||||
if (serverInstance) {
|
||||
await serverInstance.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("should reject more than 10 concurrent WebSocket connections", async () => {
|
||||
serverInstance = await createWebServer({ port, hostname });
|
||||
const wsUrl = `ws://${hostname}:${port}/ws`;
|
||||
const sockets: WebSocket[] = [];
|
||||
|
||||
try {
|
||||
// Attempt to open 12 connections (limit is 10)
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
sockets.push(ws);
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
}
|
||||
|
||||
// Give connections time to settle
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const pendingCount = serverInstance.server.pendingWebSockets;
|
||||
expect(pendingCount).toBeLessThanOrEqual(10);
|
||||
} finally {
|
||||
sockets.forEach(s => {
|
||||
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING) {
|
||||
s.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("should return 200 for health check", async () => {
|
||||
if (!serverInstance) {
|
||||
serverInstance = await createWebServer({ port, hostname });
|
||||
}
|
||||
const response = await fetch(`http://${hostname}:${port}/api/health`);
|
||||
expect(response.status).toBe(200);
|
||||
const data = (await response.json()) as { status: string };
|
||||
expect(data.status).toBe("ok");
|
||||
});
|
||||
|
||||
describe("Administrative Actions", () => {
|
||||
test("should allow administrative actions without token", async () => {
|
||||
const response = await fetch(`http://${hostname}:${port}/api/actions/reload-commands`, {
|
||||
method: "POST"
|
||||
});
|
||||
// Should be 200 (OK) or 500 (if underlying service fails, but NOT 401)
|
||||
expect(response.status).not.toBe(401);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test("should reject maintenance mode with invalid payload", async () => {
|
||||
const response = await fetch(`http://${hostname}:${port}/api/actions/maintenance-mode`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ not_enabled: true }) // Wrong field
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
const data = await response.json() as { error: string };
|
||||
expect(data.error).toBe("Invalid payload");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user