Files
elysium/apps/api/test/services/webhook.spec.ts
T
hikari 8fa36f2757
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m7s
CI / Lint, Build & Test (pull_request) Successful in 1m9s
fix(webhook): send milestone notifications with SUPPRESS_NOTIFICATIONS flag
Adds flags: 4096 (MessageFlags.SUPPRESS_NOTIFICATIONS) to the Discord
webhook payload so milestone announcements appear in the channel without
triggering desktop or mobile push notifications. Closes #41.
2026-03-09 20:00:06 -07:00

141 lines
6.5 KiB
TypeScript

/* 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();
});
});
});