refactor: hardcode non-secret Discord IDs as constants

Move guild ID, apotheosis role ID, Discord client ID, and redirect URI
from env vars to hardcoded constants — none of these are secrets, and
hardcoding removes unnecessary runtime configuration surface. Update
tests to use the real IDs and drop now-irrelevant env var scenarios.
This commit is contained in:
2026-03-24 18:26:38 -07:00
committed by Naomi Carrigan
parent 8ccc3f4d0d
commit 1a99e83b86
8 changed files with 37 additions and 150 deletions
-4
View File
@@ -1,6 +1,4 @@
DISCORD_CLIENT_ID="op://Environment Variables - Naomi/Elysium/discord client id"
DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Elysium/discord client secret" DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Elysium/discord client secret"
DISCORD_REDIRECT_URI="op://Environment Variables - Naomi/Elysium/discord redirect uri"
JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret" JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret"
DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url" DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url"
ANTI_CHEAT_SECRET="op://Environment Variables - Naomi/Elysium/anti cheat secret" ANTI_CHEAT_SECRET="op://Environment Variables - Naomi/Elysium/anti cheat secret"
@@ -8,6 +6,4 @@ PORT="op://Environment Variables - Naomi/Elysium/port"
CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin" CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin"
DISCORD_MILESTONE_WEBHOOK="op://Environment Variables - Naomi/Elysium/discord milestone webhook" DISCORD_MILESTONE_WEBHOOK="op://Environment Variables - Naomi/Elysium/discord milestone webhook"
DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Elysium/discord bot token" DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Elysium/discord bot token"
DISCORD_GUILD_ID="op://Environment Variables - Naomi/Elysium/discord guild id"
DISCORD_APOTHEOSIS_ROLE_ID="op://Environment Variables - Naomi/Elysium/discord apotheosis role id"
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth" LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
+8 -21
View File
@@ -7,6 +7,9 @@
/* eslint-disable @typescript-eslint/naming-convention -- Discord API requires snake_case fields and HTTP headers require Pascal-Case */ /* eslint-disable @typescript-eslint/naming-convention -- Discord API requires snake_case fields and HTTP headers require Pascal-Case */
import { logger } from "./logger.js"; import { logger } from "./logger.js";
const discordClientId = "1465425348375089182";
const discordRedirectUri = "https://elysium.nhcarrigan.com/api/auth/callback";
interface DiscordTokenResponse { interface DiscordTokenResponse {
access_token: string; access_token: string;
token_type: string; token_type: string;
@@ -31,24 +34,18 @@ interface DiscordUser {
const exchangeCode = async( const exchangeCode = async(
code: string, code: string,
): Promise<DiscordTokenResponse> => { ): Promise<DiscordTokenResponse> => {
const clientId = process.env.DISCORD_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET; const clientSecret = process.env.DISCORD_CLIENT_SECRET;
const redirectUri = process.env.DISCORD_REDIRECT_URI;
if ( if (clientSecret === undefined || clientSecret === "") {
clientId === undefined || clientId === ""
|| clientSecret === undefined || clientSecret === ""
|| redirectUri === undefined || redirectUri === ""
) {
throw new Error("Discord OAuth environment variables are required"); throw new Error("Discord OAuth environment variables are required");
} }
const parameters = new URLSearchParams({ const parameters = new URLSearchParams({
client_id: clientId, client_id: discordClientId,
client_secret: clientSecret, client_secret: clientSecret,
code: code, code: code,
grant_type: "authorization_code", grant_type: "authorization_code",
redirect_uri: redirectUri, redirect_uri: discordRedirectUri,
}); });
try { try {
@@ -146,19 +143,9 @@ const fetchDiscordUserById = async(
* @throws {Error} If OAuth environment variables are missing. * @throws {Error} If OAuth environment variables are missing.
*/ */
const buildOAuthUrl = (): string => { const buildOAuthUrl = (): string => {
const clientId = process.env.DISCORD_CLIENT_ID;
const redirectUri = process.env.DISCORD_REDIRECT_URI;
if (
clientId === undefined || clientId === ""
|| redirectUri === undefined || redirectUri === ""
) {
throw new Error("Discord OAuth environment variables are required");
}
const parameters = new URLSearchParams({ const parameters = new URLSearchParams({
client_id: clientId, client_id: discordClientId,
redirect_uri: redirectUri, redirect_uri: discordRedirectUri,
response_type: "code", response_type: "code",
scope: "identify", scope: "identify",
}); });
+4 -4
View File
@@ -8,6 +8,8 @@
import { prisma } from "../db/client.js"; import { prisma } from "../db/client.js";
import { logger } from "./logger.js"; import { logger } from "./logger.js";
const discordGuildId = "1354624415861833870";
/** /**
* Discord Gateway opcodes used by this client. * Discord Gateway opcodes used by this client.
*/ */
@@ -36,8 +38,7 @@ const handleGuildMemberAdd = async(
discordId: string, discordId: string,
guildId: string, guildId: string,
): Promise<void> => { ): Promise<void> => {
const configuredGuildId = process.env.DISCORD_GUILD_ID; if (guildId !== discordGuildId) {
if (guildId !== configuredGuildId) {
return; return;
} }
try { try {
@@ -66,8 +67,7 @@ const handleGuildMemberRemove = async(
discordId: string, discordId: string,
guildId: string, guildId: string,
): Promise<void> => { ): Promise<void> => {
const configuredGuildId = process.env.DISCORD_GUILD_ID; if (guildId !== discordGuildId) {
if (guildId !== configuredGuildId) {
return; return;
} }
try { try {
+6 -14
View File
@@ -18,7 +18,9 @@ const suppressNotifications = 4096;
/** /**
* The Discord role ID for the Elysian role granted to all Elysium players. * The Discord role ID for the Elysian role granted to all Elysium players.
*/ */
const discordGuildId = "1354624415861833870";
const elysianRoleId = "1486144823684628490"; const elysianRoleId = "1486144823684628490";
const apotheosisRoleId = "1479966598210129991";
/** /**
* Grants the Elysian Discord role to the given player and returns whether they are in the guild. * Grants the Elysian Discord role to the given player and returns whether they are in the guild.
@@ -28,18 +30,14 @@ const elysianRoleId = "1486144823684628490";
*/ */
const grantElysianRole = async(discordId: string): Promise<boolean> => { const grantElysianRole = async(discordId: string): Promise<boolean> => {
const botToken = process.env.DISCORD_BOT_TOKEN; const botToken = process.env.DISCORD_BOT_TOKEN;
const guildId = process.env.DISCORD_GUILD_ID;
if ( if (botToken === undefined || botToken === "") {
botToken === undefined || botToken === ""
|| guildId === undefined || guildId === ""
) {
return false; return false;
} }
try { try {
const response = await fetch( const response = await fetch(
`${discordApi}/guilds/${guildId}/members/${discordId}/roles/${elysianRoleId}`, `${discordApi}/guilds/${discordGuildId}/members/${discordId}/roles/${elysianRoleId}`,
{ {
headers: { headers: {
"Authorization": `Bot ${botToken}`, "Authorization": `Bot ${botToken}`,
@@ -68,20 +66,14 @@ const grantElysianRole = async(discordId: string): Promise<boolean> => {
*/ */
const grantApotheosisRole = async(discordId: string): Promise<void> => { const grantApotheosisRole = async(discordId: string): Promise<void> => {
const botToken = process.env.DISCORD_BOT_TOKEN; const botToken = process.env.DISCORD_BOT_TOKEN;
const guildId = process.env.DISCORD_GUILD_ID;
const roleId = process.env.DISCORD_APOTHEOSIS_ROLE_ID;
if ( if (botToken === undefined || botToken === "") {
botToken === undefined || botToken === ""
|| guildId === undefined || guildId === ""
|| roleId === undefined || roleId === ""
) {
return; return;
} }
try { try {
await fetch( await fetch(
`${discordApi}/guilds/${guildId}/members/${discordId}/roles/${roleId}`, `${discordApi}/guilds/${discordGuildId}/members/${discordId}/roles/${apotheosisRoleId}`,
{ {
headers: { headers: {
"Authorization": `Bot ${botToken}`, "Authorization": `Bot ${botToken}`,
+3 -25
View File
@@ -18,51 +18,31 @@ describe("discord service", () => {
}); });
describe("buildOAuthUrl", () => { describe("buildOAuthUrl", () => {
it("throws when DISCORD_CLIENT_ID is missing", async () => {
delete process.env["DISCORD_CLIENT_ID"];
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/callback";
const { buildOAuthUrl } = await import("../../src/services/discord.js");
expect(() => buildOAuthUrl()).toThrow("Discord OAuth environment variables are required");
});
it("throws when DISCORD_REDIRECT_URI is missing", async () => {
process.env["DISCORD_CLIENT_ID"] = "client123";
delete process.env["DISCORD_REDIRECT_URI"];
const { buildOAuthUrl } = await import("../../src/services/discord.js");
expect(() => buildOAuthUrl()).toThrow("Discord OAuth environment variables are required");
});
it("returns a URL with correct query params", async () => { it("returns a URL with correct query params", async () => {
process.env["DISCORD_CLIENT_ID"] = "client123";
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/callback";
const { buildOAuthUrl } = await import("../../src/services/discord.js"); const { buildOAuthUrl } = await import("../../src/services/discord.js");
const url = buildOAuthUrl(); const url = buildOAuthUrl();
expect(url).toContain("client_id=client123"); expect(url).toContain("client_id=1465425348375089182");
expect(url).toContain("response_type=code"); expect(url).toContain("response_type=code");
expect(url).toContain("scope=identify"); expect(url).toContain("scope=identify");
}); });
}); });
describe("exchangeCode", () => { describe("exchangeCode", () => {
it("throws when env vars are missing", async () => { it("throws when DISCORD_CLIENT_SECRET is missing", async () => {
delete process.env["DISCORD_CLIENT_ID"]; delete process.env["DISCORD_CLIENT_SECRET"];
const { exchangeCode } = await import("../../src/services/discord.js"); const { exchangeCode } = await import("../../src/services/discord.js");
await expect(exchangeCode("mycode")).rejects.toThrow("Discord OAuth environment variables are required"); await expect(exchangeCode("mycode")).rejects.toThrow("Discord OAuth environment variables are required");
}); });
it("throws when response is not ok", async () => { it("throws when response is not ok", async () => {
process.env["DISCORD_CLIENT_ID"] = "cid";
process.env["DISCORD_CLIENT_SECRET"] = "secret"; process.env["DISCORD_CLIENT_SECRET"] = "secret";
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized" }); mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized" });
const { exchangeCode } = await import("../../src/services/discord.js"); const { exchangeCode } = await import("../../src/services/discord.js");
await expect(exchangeCode("bad_code")).rejects.toThrow("Discord token exchange failed"); await expect(exchangeCode("bad_code")).rejects.toThrow("Discord token exchange failed");
}); });
it("returns parsed body on success", async () => { it("returns parsed body on success", async () => {
process.env["DISCORD_CLIENT_ID"] = "cid";
process.env["DISCORD_CLIENT_SECRET"] = "secret"; process.env["DISCORD_CLIENT_SECRET"] = "secret";
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
const tokenData = { access_token: "tok", token_type: "Bearer", expires_in: 3600, refresh_token: "ref", scope: "identify" }; const tokenData = { access_token: "tok", token_type: "Bearer", expires_in: 3600, refresh_token: "ref", scope: "identify" };
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(tokenData) }); mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(tokenData) });
const { exchangeCode } = await import("../../src/services/discord.js"); const { exchangeCode } = await import("../../src/services/discord.js");
@@ -96,9 +76,7 @@ describe("discord service", () => {
describe("exchangeCode non-Error throw", () => { describe("exchangeCode non-Error throw", () => {
it("re-throws when fetch rejects with a non-Error value", async () => { it("re-throws when fetch rejects with a non-Error value", async () => {
process.env["DISCORD_CLIENT_ID"] = "cid";
process.env["DISCORD_CLIENT_SECRET"] = "secret"; process.env["DISCORD_CLIENT_SECRET"] = "secret";
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
mockFetch.mockRejectedValueOnce("raw string error"); mockFetch.mockRejectedValueOnce("raw string error");
const { exchangeCode } = await import("../../src/services/discord.js"); const { exchangeCode } = await import("../../src/services/discord.js");
await expect(exchangeCode("some_code")).rejects.toBe("raw string error"); await expect(exchangeCode("some_code")).rejects.toBe("raw string error");
+11 -34
View File
@@ -16,60 +16,48 @@ vi.mock("../../src/services/logger.js", () => ({
import { prisma } from "../../src/db/client.js"; import { prisma } from "../../src/db/client.js";
describe("gateway service", () => { const discordGuildId = "1354624415861833870";
const ORIGINAL_ENV = process.env;
describe("gateway service", () => {
beforeEach(() => { beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
vi.resetAllMocks(); vi.resetAllMocks();
}); });
afterEach(() => { afterEach(() => {
process.env = ORIGINAL_ENV; vi.clearAllMocks();
}); });
describe("handleGuildMemberAdd", () => { describe("handleGuildMemberAdd", () => {
it("sets inGuild to true for the matching guild", async () => { it("sets inGuild to true for the matching guild", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 }); vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js"); const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
await handleGuildMemberAdd("user123", "guild123"); await handleGuildMemberAdd("user123", discordGuildId);
expect(prisma.player.updateMany).toHaveBeenCalledWith({ expect(prisma.player.updateMany).toHaveBeenCalledWith({
data: { inGuild: true }, data: { inGuild: true },
where: { discordId: "user123" }, where: { discordId: "user123" },
}); });
}); });
it("no-ops when guild id does not match", async () => { it("no-ops when guild id does not match the configured guild", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js"); const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
await handleGuildMemberAdd("user123", "other_guild"); await handleGuildMemberAdd("user123", "other_guild");
expect(prisma.player.updateMany).not.toHaveBeenCalled(); expect(prisma.player.updateMany).not.toHaveBeenCalled();
}); });
it("no-ops when DISCORD_GUILD_ID env var is missing and guild does not match undefined", async () => {
delete process.env["DISCORD_GUILD_ID"];
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
await handleGuildMemberAdd("user123", "guild123");
expect(prisma.player.updateMany).not.toHaveBeenCalled();
});
it("logs error when prisma throws an Error", async () => { it("logs error when prisma throws an Error", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
const dbError = new Error("DB failure"); const dbError = new Error("DB failure");
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError); vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js"); const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
const { logger } = await import("../../src/services/logger.js"); const { logger } = await import("../../src/services/logger.js");
await handleGuildMemberAdd("user123", "guild123"); await handleGuildMemberAdd("user123", discordGuildId);
expect(logger.error).toHaveBeenCalledWith("gateway_member_add", dbError); expect(logger.error).toHaveBeenCalledWith("gateway_member_add", dbError);
}); });
it("logs error when prisma throws a non-Error", async () => { it("logs error when prisma throws a non-Error", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error"); vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js"); const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
const { logger } = await import("../../src/services/logger.js"); const { logger } = await import("../../src/services/logger.js");
await handleGuildMemberAdd("user123", "guild123"); await handleGuildMemberAdd("user123", discordGuildId);
expect(logger.error).toHaveBeenCalledWith( expect(logger.error).toHaveBeenCalledWith(
"gateway_member_add", "gateway_member_add",
new Error("raw error"), new Error("raw error"),
@@ -79,46 +67,35 @@ describe("gateway service", () => {
describe("handleGuildMemberRemove", () => { describe("handleGuildMemberRemove", () => {
it("sets inGuild to false for the matching guild", async () => { it("sets inGuild to false for the matching guild", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 }); vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js"); const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
await handleGuildMemberRemove("user123", "guild123"); await handleGuildMemberRemove("user123", discordGuildId);
expect(prisma.player.updateMany).toHaveBeenCalledWith({ expect(prisma.player.updateMany).toHaveBeenCalledWith({
data: { inGuild: false }, data: { inGuild: false },
where: { discordId: "user123" }, where: { discordId: "user123" },
}); });
}); });
it("no-ops when guild id does not match", async () => { it("no-ops when guild id does not match the configured guild", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js"); const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
await handleGuildMemberRemove("user123", "other_guild"); await handleGuildMemberRemove("user123", "other_guild");
expect(prisma.player.updateMany).not.toHaveBeenCalled(); expect(prisma.player.updateMany).not.toHaveBeenCalled();
}); });
it("no-ops when DISCORD_GUILD_ID env var is missing", async () => {
delete process.env["DISCORD_GUILD_ID"];
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
await handleGuildMemberRemove("user123", "guild123");
expect(prisma.player.updateMany).not.toHaveBeenCalled();
});
it("logs error when prisma throws an Error", async () => { it("logs error when prisma throws an Error", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
const dbError = new Error("DB failure"); const dbError = new Error("DB failure");
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError); vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js"); const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
const { logger } = await import("../../src/services/logger.js"); const { logger } = await import("../../src/services/logger.js");
await handleGuildMemberRemove("user123", "guild123"); await handleGuildMemberRemove("user123", discordGuildId);
expect(logger.error).toHaveBeenCalledWith("gateway_member_remove", dbError); expect(logger.error).toHaveBeenCalledWith("gateway_member_remove", dbError);
}); });
it("logs error when prisma throws a non-Error", async () => { it("logs error when prisma throws a non-Error", async () => {
process.env["DISCORD_GUILD_ID"] = "guild123";
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error"); vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js"); const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
const { logger } = await import("../../src/services/logger.js"); const { logger } = await import("../../src/services/logger.js");
await handleGuildMemberRemove("user123", "guild123"); await handleGuildMemberRemove("user123", discordGuildId);
expect(logger.error).toHaveBeenCalledWith( expect(logger.error).toHaveBeenCalledWith(
"gateway_member_remove", "gateway_member_remove",
new Error("raw error"), new Error("raw error"),
+4 -47
View File
@@ -20,42 +20,20 @@ describe("webhook service", () => {
describe("grantApotheosisRole", () => { describe("grantApotheosisRole", () => {
it("does nothing when bot token is missing", async () => { it("does nothing when bot token is missing", async () => {
delete process.env["DISCORD_BOT_TOKEN"]; 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"); const { grantApotheosisRole } = await import("../../src/services/webhook.js");
await grantApotheosisRole("user123"); await grantApotheosisRole("user123");
expect(mockFetch).not.toHaveBeenCalled(); expect(mockFetch).not.toHaveBeenCalled();
}); });
it("does nothing when guild id is missing", async () => { it("calls Discord API with correct URL and auth when bot token is set", 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_BOT_TOKEN"] = "bot_token";
process.env["DISCORD_GUILD_ID"] = "guild123";
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role456";
mockFetch.mockResolvedValueOnce({ ok: true }); mockFetch.mockResolvedValueOnce({ ok: true });
const { grantApotheosisRole } = await import("../../src/services/webhook.js"); const { grantApotheosisRole } = await import("../../src/services/webhook.js");
await grantApotheosisRole("user789"); await grantApotheosisRole("user789");
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
"https://discord.com/api/v10/guilds/guild123/members/user789/roles/role456", "https://discord.com/api/v10/guilds/1354624415861833870/members/user789/roles/1479966598210129991",
expect.objectContaining({ expect.objectContaining({
method: "PUT", method: "PUT",
headers: expect.objectContaining({ Authorization: "Bot bot_token" }), headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
}), }),
); );
@@ -63,8 +41,6 @@ describe("webhook service", () => {
it("swallows fetch errors gracefully", async () => { it("swallows fetch errors gracefully", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; 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")); mockFetch.mockRejectedValueOnce(new Error("Network error"));
const { grantApotheosisRole } = await import("../../src/services/webhook.js"); const { grantApotheosisRole } = await import("../../src/services/webhook.js");
await expect(grantApotheosisRole("user")).resolves.toBeUndefined(); await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
@@ -72,8 +48,6 @@ describe("webhook service", () => {
it("swallows non-Error fetch rejections gracefully", async () => { it("swallows non-Error fetch rejections gracefully", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_BOT_TOKEN"] = "tok";
process.env["DISCORD_GUILD_ID"] = "g";
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "r";
mockFetch.mockRejectedValueOnce("raw string error"); mockFetch.mockRejectedValueOnce("raw string error");
const { grantApotheosisRole } = await import("../../src/services/webhook.js"); const { grantApotheosisRole } = await import("../../src/services/webhook.js");
await expect(grantApotheosisRole("user")).resolves.toBeUndefined(); await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
@@ -83,18 +57,6 @@ describe("webhook service", () => {
describe("grantElysianRole", () => { describe("grantElysianRole", () => {
it("does nothing when bot token is missing", async () => { it("does nothing when bot token is missing", async () => {
delete process.env["DISCORD_BOT_TOKEN"]; delete process.env["DISCORD_BOT_TOKEN"];
process.env["DISCORD_GUILD_ID"] = "guild123";
process.env["DISCORD_ELYSIAN_ROLE_ID"] = "role123";
const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user123");
expect(mockFetch).not.toHaveBeenCalled();
expect(result).toBe(false);
});
it("does nothing when guild id is missing", async () => {
process.env["DISCORD_BOT_TOKEN"] = "token";
delete process.env["DISCORD_GUILD_ID"];
process.env["DISCORD_ELYSIAN_ROLE_ID"] = "role123";
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user123"); const result = await grantElysianRole("user123");
expect(mockFetch).not.toHaveBeenCalled(); expect(mockFetch).not.toHaveBeenCalled();
@@ -103,12 +65,11 @@ describe("webhook service", () => {
it("returns true when Discord API responds with ok", async () => { it("returns true when Discord API responds with ok", async () => {
process.env["DISCORD_BOT_TOKEN"] = "bot_token"; process.env["DISCORD_BOT_TOKEN"] = "bot_token";
process.env["DISCORD_GUILD_ID"] = "guild123";
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }); mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user789"); const result = await grantElysianRole("user789");
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
"https://discord.com/api/v10/guilds/guild123/members/user789/roles/1486144823684628490", "https://discord.com/api/v10/guilds/1354624415861833870/members/user789/roles/1486144823684628490",
expect.objectContaining({ expect.objectContaining({
method: "PUT", method: "PUT",
headers: expect.objectContaining({ Authorization: "Bot bot_token" }), headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
@@ -119,7 +80,6 @@ describe("webhook service", () => {
it("returns true when Discord API responds with 204", async () => { it("returns true when Discord API responds with 204", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_BOT_TOKEN"] = "tok";
process.env["DISCORD_GUILD_ID"] = "g";
mockFetch.mockResolvedValueOnce({ ok: false, status: 204 }); mockFetch.mockResolvedValueOnce({ ok: false, status: 204 });
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user"); const result = await grantElysianRole("user");
@@ -128,7 +88,6 @@ describe("webhook service", () => {
it("returns false when Discord API responds with an error status", async () => { it("returns false when Discord API responds with an error status", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_BOT_TOKEN"] = "tok";
process.env["DISCORD_GUILD_ID"] = "g";
mockFetch.mockResolvedValueOnce({ ok: false, status: 403 }); mockFetch.mockResolvedValueOnce({ ok: false, status: 403 });
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user"); const result = await grantElysianRole("user");
@@ -137,7 +96,6 @@ describe("webhook service", () => {
it("returns false and swallows fetch errors gracefully", async () => { it("returns false and swallows fetch errors gracefully", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_BOT_TOKEN"] = "tok";
process.env["DISCORD_GUILD_ID"] = "g";
mockFetch.mockRejectedValueOnce(new Error("Network error")); mockFetch.mockRejectedValueOnce(new Error("Network error"));
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user"); const result = await grantElysianRole("user");
@@ -146,7 +104,6 @@ describe("webhook service", () => {
it("returns false and swallows non-Error fetch rejections", async () => { it("returns false and swallows non-Error fetch rejections", async () => {
process.env["DISCORD_BOT_TOKEN"] = "tok"; process.env["DISCORD_BOT_TOKEN"] = "tok";
process.env["DISCORD_GUILD_ID"] = "g";
mockFetch.mockRejectedValueOnce("raw string error"); mockFetch.mockRejectedValueOnce("raw string error");
const { grantElysianRole } = await import("../../src/services/webhook.js"); const { grantElysianRole } = await import("../../src/services/webhook.js");
const result = await grantElysianRole("user"); const result = await grantElysianRole("user");
+1 -1
View File
@@ -737,7 +737,7 @@ export const GameProvider = ({
setSchemaOutdated(data.schemaOutdated); setSchemaOutdated(data.schemaOutdated);
setSaveSchemaVersion(data.state.schemaVersion ?? 0); setSaveSchemaVersion(data.state.schemaVersion ?? 0);
setCurrentSchemaVersion(data.currentSchemaVersion); setCurrentSchemaVersion(data.currentSchemaVersion);
setInGuild(data.inGuild === true); setInGuild(data.inGuild);
// Fetch number format preference from profile (fire-and-forget, non-blocking) // Fetch number format preference from profile (fire-and-forget, non-blocking)
void fetch(`/api/profile/${data.state.player.discordId}`). void fetch(`/api/profile/${data.state.player.discordId}`).