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"); }); });