feat(dashboard): implement bot settings page with partial updates and serialization fixes
This commit is contained in:
160
web/src/server.settings.test.ts
Normal file
160
web/src/server.settings.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, it, mock, beforeEach, afterEach, jest } from "bun:test";
|
||||
import { type WebServerInstance } from "./server";
|
||||
|
||||
// Mock the dependencies
|
||||
const mockConfig = {
|
||||
leveling: {
|
||||
base: 100,
|
||||
exponent: 1.5,
|
||||
chat: { minXp: 10, maxXp: 20, cooldownMs: 60000 }
|
||||
},
|
||||
economy: {
|
||||
daily: { amount: 100n, streakBonus: 10n, weeklyBonus: 50n, cooldownMs: 86400000 },
|
||||
transfers: { allowSelfTransfer: false, minAmount: 50n },
|
||||
exam: { multMin: 1.5, multMax: 2.5 }
|
||||
},
|
||||
inventory: { maxStackSize: 99n, maxSlots: 20 },
|
||||
lootdrop: {
|
||||
spawnChance: 0.1,
|
||||
cooldownMs: 3600000,
|
||||
minMessages: 10,
|
||||
reward: { min: 100, max: 500, currency: "gold" }
|
||||
},
|
||||
commands: { "help": true },
|
||||
system: {},
|
||||
moderation: {
|
||||
prune: { maxAmount: 100, confirmThreshold: 50, batchSize: 100, batchDelayMs: 1000 },
|
||||
cases: { dmOnWarn: true }
|
||||
}
|
||||
};
|
||||
|
||||
const mockSaveConfig = jest.fn();
|
||||
|
||||
// Mock @shared/lib/config using mock.module
|
||||
mock.module("@shared/lib/config", () => ({
|
||||
config: mockConfig,
|
||||
saveConfig: mockSaveConfig,
|
||||
GameConfigType: {}
|
||||
}));
|
||||
|
||||
// Mock BotClient
|
||||
const mockGuild = {
|
||||
roles: {
|
||||
cache: [
|
||||
{ id: "role1", name: "Admin", hexColor: "#ffffff", position: 1 },
|
||||
{ id: "role2", name: "User", hexColor: "#000000", position: 0 }
|
||||
]
|
||||
},
|
||||
channels: {
|
||||
cache: [
|
||||
{ id: "chan1", name: "general", type: 0 }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mock.module("../../bot/lib/BotClient", () => ({
|
||||
AuroraClient: {
|
||||
guilds: {
|
||||
cache: {
|
||||
get: () => mockGuild
|
||||
}
|
||||
},
|
||||
commands: [
|
||||
{ data: { name: "ping" } }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
mock.module("@shared/lib/env", () => ({
|
||||
env: {
|
||||
DISCORD_GUILD_ID: "123456789"
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock spawn
|
||||
mock.module("bun", () => {
|
||||
return {
|
||||
spawn: jest.fn(() => ({
|
||||
unref: () => { }
|
||||
})),
|
||||
serve: Bun.serve
|
||||
};
|
||||
});
|
||||
|
||||
// Import createWebServer after mocks
|
||||
import { createWebServer } from "./server";
|
||||
|
||||
describe("Settings API", () => {
|
||||
let serverInstance: WebServerInstance;
|
||||
const PORT = 3009;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
serverInstance = await createWebServer({ port: PORT });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (serverInstance) {
|
||||
await serverInstance.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("GET /api/settings should return current configuration", async () => {
|
||||
const res = await fetch(`${BASE_URL}/api/settings`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const data = await res.json();
|
||||
// Check if BigInts are converted to strings
|
||||
expect(data.economy.daily.amount).toBe("100");
|
||||
expect(data.leveling.base).toBe(100);
|
||||
});
|
||||
|
||||
it("POST /api/settings should save valid configuration via merge", async () => {
|
||||
// We only send a partial update, expecting the server to merge it
|
||||
// Note: For now the server implementation might still default to overwrite if we haven't updated it yet.
|
||||
// But the user requested "partial vs full" fix.
|
||||
// Let's assume we implement the merge logic.
|
||||
const partialConfig = { studentRole: "new-role-partial" };
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api/settings`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(partialConfig)
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// Expect saveConfig to be called with the MERGED result
|
||||
expect(mockSaveConfig).toHaveBeenCalledWith(expect.objectContaining({
|
||||
studentRole: "new-role-partial",
|
||||
leveling: mockConfig.leveling // Should keep existing values
|
||||
}));
|
||||
});
|
||||
|
||||
it("POST /api/settings should return 400 when save fails", async () => {
|
||||
mockSaveConfig.mockImplementationOnce(() => {
|
||||
throw new Error("Validation failed");
|
||||
});
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api/settings`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}) // Empty might be valid partial, but mocks throw
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
const data = await res.json();
|
||||
expect(data.details).toBe("Validation failed");
|
||||
});
|
||||
|
||||
it("GET /api/settings/meta should return simplified metadata", async () => {
|
||||
const res = await fetch(`${BASE_URL}/api/settings/meta`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const data = await res.json();
|
||||
expect(data.roles).toHaveLength(2);
|
||||
expect(data.roles[0]).toEqual({ id: "role1", name: "Admin", color: "#ffffff" });
|
||||
expect(data.channels[0]).toEqual({ id: "chan1", name: "general", type: 0 });
|
||||
expect(data.commands).toContain("ping");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user