generated from nhcarrigan/template
830a9d2a56
Adds fetchDiscordUserById (bot token) to the Discord service and calls it in parallel with the DB queries on game load. When the returned hash differs from the stored value the Player record is updated and the hash is immediately synced into the returned game state, so the resource bar always shows the player's current Discord avatar. Also adds onError fallback: if the avatar URL is stale before the next load, the resource bar component now derives the URL fresh from state on every render rather than caching it.
157 lines
7.0 KiB
TypeScript
157 lines
7.0 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("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 () => {
|
|
process.env["DISCORD_CLIENT_ID"] = "client123";
|
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/callback";
|
|
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
|
const url = buildOAuthUrl();
|
|
expect(url).toContain("client_id=client123");
|
|
expect(url).toContain("response_type=code");
|
|
expect(url).toContain("scope=identify");
|
|
});
|
|
});
|
|
|
|
describe("exchangeCode", () => {
|
|
it("throws when env vars are missing", async () => {
|
|
delete process.env["DISCORD_CLIENT_ID"];
|
|
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_ID"] = "cid";
|
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
|
|
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_ID"] = "cid";
|
|
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" };
|
|
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_ID"] = "cid";
|
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
|
|
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" });
|
|
});
|
|
});
|
|
});
|