/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("webhook service", () => { const ORIGINAL_ENV = process.env; const mockFetch = vi.fn(); beforeEach(() => { process.env = { ...ORIGINAL_ENV }; vi.resetModules(); vi.stubGlobal("fetch", mockFetch); }); afterEach(() => { process.env = ORIGINAL_ENV; vi.unstubAllGlobals(); mockFetch.mockReset(); }); describe("grantApotheosisRole", () => { it("does nothing when bot token is missing", async () => { delete process.env["DISCORD_BOT_TOKEN"]; process.env["DISCORD_GUILD_ID"] = "guild123"; process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role123"; const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await grantApotheosisRole("user123"); expect(mockFetch).not.toHaveBeenCalled(); }); it("does nothing when guild id is missing", async () => { process.env["DISCORD_BOT_TOKEN"] = "token"; delete process.env["DISCORD_GUILD_ID"]; process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role123"; const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await grantApotheosisRole("user123"); expect(mockFetch).not.toHaveBeenCalled(); }); it("does nothing when role id is missing", async () => { process.env["DISCORD_BOT_TOKEN"] = "token"; process.env["DISCORD_GUILD_ID"] = "guild123"; delete process.env["DISCORD_APOTHEOSIS_ROLE_ID"]; const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await grantApotheosisRole("user123"); expect(mockFetch).not.toHaveBeenCalled(); }); it("calls Discord API with correct URL and auth when env vars are set", async () => { process.env["DISCORD_BOT_TOKEN"] = "bot_token"; process.env["DISCORD_GUILD_ID"] = "guild123"; process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role456"; mockFetch.mockResolvedValueOnce({ ok: true }); const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await grantApotheosisRole("user789"); expect(mockFetch).toHaveBeenCalledWith( "https://discord.com/api/v10/guilds/guild123/members/user789/roles/role456", expect.objectContaining({ method: "PUT", headers: expect.objectContaining({ Authorization: "Bot bot_token" }), }), ); }); it("swallows fetch errors gracefully", async () => { process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_GUILD_ID"] = "g"; process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "r"; mockFetch.mockRejectedValueOnce(new Error("Network error")); const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await expect(grantApotheosisRole("user")).resolves.toBeUndefined(); }); it("swallows non-Error fetch rejections gracefully", async () => { process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_GUILD_ID"] = "g"; process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "r"; mockFetch.mockRejectedValueOnce("raw string error"); const { grantApotheosisRole } = await import("../../src/services/webhook.js"); await expect(grantApotheosisRole("user")).resolves.toBeUndefined(); }); }); describe("postMilestoneWebhook", () => { const counts = { prestige: 1, transcendence: 0, apotheosis: 0 }; it("does nothing when webhook URL is missing", async () => { delete process.env["DISCORD_MILESTONE_WEBHOOK"]; const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await postMilestoneWebhook("user123", "prestige", counts); expect(mockFetch).not.toHaveBeenCalled(); }); it("posts prestige message with correct body", async () => { process.env["DISCORD_MILESTONE_WEBHOOK"] = "https://discord.com/webhook/abc"; mockFetch.mockResolvedValueOnce({ ok: true }); const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await postMilestoneWebhook("user123", "prestige", counts); const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://discord.com/webhook/abc"); const body = JSON.parse(options.body as string) as { content: string; flags: number }; expect(body.content).toContain("<@user123>"); expect(body.content).toContain("prestiged"); expect(body.flags).toBe(4096); }); it("posts transcendence message correctly", async () => { process.env["DISCORD_MILESTONE_WEBHOOK"] = "https://discord.com/webhook/abc"; mockFetch.mockResolvedValueOnce({ ok: true }); const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await postMilestoneWebhook("user123", "transcendence", { prestige: 0, transcendence: 1, apotheosis: 0 }); const [, options] = mockFetch.mock.calls[0] as [string, RequestInit]; const body = JSON.parse(options.body as string) as { content: string }; expect(body.content).toContain("transcended"); }); it("posts apotheosis message correctly", async () => { process.env["DISCORD_MILESTONE_WEBHOOK"] = "https://discord.com/webhook/abc"; mockFetch.mockResolvedValueOnce({ ok: true }); const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await postMilestoneWebhook("user123", "apotheosis", { prestige: 0, transcendence: 0, apotheosis: 1 }); const [, options] = mockFetch.mock.calls[0] as [string, RequestInit]; const body = JSON.parse(options.body as string) as { content: string }; expect(body.content).toContain("reached apotheosis"); }); it("swallows fetch errors gracefully", async () => { process.env["DISCORD_MILESTONE_WEBHOOK"] = "https://discord.com/webhook/abc"; mockFetch.mockRejectedValueOnce(new Error("Network timeout")); const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await expect(postMilestoneWebhook("user", "prestige", counts)).resolves.toBeUndefined(); }); it("swallows non-Error fetch rejections gracefully", async () => { process.env["DISCORD_MILESTONE_WEBHOOK"] = "https://discord.com/webhook/abc"; mockFetch.mockRejectedValueOnce("raw string error"); const { postMilestoneWebhook } = await import("../../src/services/webhook.js"); await expect(postMilestoneWebhook("user", "prestige", counts)).resolves.toBeUndefined(); }); }); });