generated from nhcarrigan/template
6bf1ac5e7d
## Summary - Grants the Elysian Discord role to players on login/registration and persists an `inGuild` flag on the Player record - Connects to the Discord Gateway via WebSocket to keep `inGuild` in sync as players join or leave the server - Shows a dismissible "Join our community" modal to players who are not yet in the guild - Hardens `inGuild` exposure through the load endpoint and game context - Moves all non-secret Discord IDs (guild, role, client, redirect URI) out of env vars and into hardcoded constants; removes them from `prod.env` ## Test plan - [ ] Lint, build, and test pipeline passes (100% coverage maintained) - [ ] New player auth grants Elysian role and sets `inGuild: true` - [ ] Existing player auth re-attempts role grant and updates `inGuild` - [ ] Join community modal appears for players not in the guild - [ ] Modal does not reappear within the same browser session after dismissal - [ ] Gateway correctly sets `inGuild: true/false` on member add/remove events ✨ This issue was created with help from Hikari~ 🌸 Reviewed-on: #134 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
135 lines
5.9 KiB
TypeScript
135 lines
5.9 KiB
TypeScript
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
describe("discord 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("buildOAuthUrl", () => {
|
|
it("returns a URL with correct query params", async () => {
|
|
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
|
const url = buildOAuthUrl();
|
|
expect(url).toContain("client_id=1479551654264049908");
|
|
expect(url).toContain("response_type=code");
|
|
expect(url).toContain("scope=identify");
|
|
});
|
|
});
|
|
|
|
describe("exchangeCode", () => {
|
|
it("throws when DISCORD_CLIENT_SECRET is missing", async () => {
|
|
delete process.env["DISCORD_CLIENT_SECRET"];
|
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
|
await expect(exchangeCode("mycode")).rejects.toThrow("Discord OAuth environment variables are required");
|
|
});
|
|
|
|
it("throws when response is not ok", async () => {
|
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized" });
|
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
|
await expect(exchangeCode("bad_code")).rejects.toThrow("Discord token exchange failed");
|
|
});
|
|
|
|
it("returns parsed body on success", async () => {
|
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
|
const tokenData = { access_token: "tok", token_type: "Bearer", expires_in: 3600, refresh_token: "ref", scope: "identify" };
|
|
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(tokenData) });
|
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
|
const result = await exchangeCode("good_code");
|
|
expect(result.access_token).toBe("tok");
|
|
});
|
|
});
|
|
|
|
describe("fetchDiscordUser", () => {
|
|
it("throws when response is not ok", async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Forbidden" });
|
|
const { fetchDiscordUser } = await import("../../src/services/discord.js");
|
|
await expect(fetchDiscordUser("bad_token")).rejects.toThrow("Discord user fetch failed");
|
|
});
|
|
|
|
it("returns parsed user on success", async () => {
|
|
const user = { id: "123", username: "testuser", discriminator: "0", avatar: null };
|
|
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(user) });
|
|
const { fetchDiscordUser } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUser("valid_token");
|
|
expect(result.id).toBe("123");
|
|
expect(result.username).toBe("testuser");
|
|
});
|
|
|
|
it("re-throws when fetch rejects with a non-Error value", async () => {
|
|
mockFetch.mockRejectedValueOnce("raw string error");
|
|
const { fetchDiscordUser } = await import("../../src/services/discord.js");
|
|
await expect(fetchDiscordUser("some_token")).rejects.toBe("raw string error");
|
|
});
|
|
});
|
|
|
|
describe("exchangeCode non-Error throw", () => {
|
|
it("re-throws when fetch rejects with a non-Error value", async () => {
|
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
|
mockFetch.mockRejectedValueOnce("raw string error");
|
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
|
await expect(exchangeCode("some_code")).rejects.toBe("raw string error");
|
|
});
|
|
});
|
|
|
|
describe("fetchDiscordUserById", () => {
|
|
it("returns null when DISCORD_BOT_TOKEN is missing", async () => {
|
|
delete process.env["DISCORD_BOT_TOKEN"];
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null when DISCORD_BOT_TOKEN is empty", async () => {
|
|
process.env["DISCORD_BOT_TOKEN"] = "";
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null when response is not ok", async () => {
|
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Not Found" });
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null when fetch throws", async () => {
|
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
|
mockFetch.mockRejectedValueOnce(new Error("network error"));
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null when fetch throws a non-Error value", async () => {
|
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
|
mockFetch.mockRejectedValueOnce("raw string error");
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns the user on success", async () => {
|
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
|
const user = { id: "123456", username: "testuser", discriminator: "0", avatar: "abc123" };
|
|
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(user) });
|
|
const { fetchDiscordUserById } = await import("../../src/services/discord.js");
|
|
const result = await fetchDiscordUserById("123456");
|
|
expect(result).toMatchObject({ id: "123456", avatar: "abc123" });
|
|
});
|
|
});
|
|
});
|