generated from nhcarrigan/template
d48b53eecd
- Add full test suite for frontend.ts (POST /log and POST /error) - Add error-path tests to all route handlers to cover catch blocks triggered by Prisma rejections - Add non-Error throw tests to cover the `new Error(String(error))` ternary false branch in middleware, services, and route catch handlers - Suppress unreachable outer catch in about.ts with v8 ignore (fetchReleases swallows all errors internally, making the outer catch genuinely dead code)
140 lines
6.4 KiB
TypeScript
140 lines
6.4 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 };
|
|
expect(body.content).toContain("<@user123>");
|
|
expect(body.content).toContain("prestiged");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|