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)
127 lines
5.6 KiB
TypeScript
127 lines
5.6 KiB
TypeScript
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
|
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Tests build minimal state objects */
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { Hono } from "hono";
|
|
|
|
vi.mock("../../src/db/client.js", () => ({
|
|
prisma: {
|
|
player: {
|
|
findUnique: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
gameState: {
|
|
create: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../src/services/discord.js", () => ({
|
|
buildOAuthUrl: vi.fn(),
|
|
exchangeCode: vi.fn(),
|
|
fetchDiscordUser: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../src/services/jwt.js", () => ({
|
|
signToken: vi.fn().mockReturnValue("test_jwt"),
|
|
}));
|
|
|
|
describe("auth route", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
process.env["CORS_ORIGIN"] = "http://localhost:5173";
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env["CORS_ORIGIN"];
|
|
});
|
|
|
|
const makeApp = async () => {
|
|
const { authRouter } = await import("../../src/routes/auth.js");
|
|
const { buildOAuthUrl, exchangeCode, fetchDiscordUser } = await import("../../src/services/discord.js");
|
|
const { prisma } = await import("../../src/db/client.js");
|
|
const app = new Hono();
|
|
app.route("/auth", authRouter);
|
|
return { app, buildOAuthUrl: vi.mocked(buildOAuthUrl), exchangeCode: vi.mocked(exchangeCode), fetchDiscordUser: vi.mocked(fetchDiscordUser), prisma };
|
|
};
|
|
|
|
describe("GET /url", () => {
|
|
it("returns the OAuth URL when buildOAuthUrl succeeds", async () => {
|
|
const { app, buildOAuthUrl } = await makeApp();
|
|
buildOAuthUrl.mockReturnValueOnce("https://discord.com/oauth2/authorize?...");
|
|
const res = await app.fetch(new Request("http://localhost/auth/url"));
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json() as { url: string };
|
|
expect(body.url).toContain("discord.com");
|
|
});
|
|
|
|
it("returns 500 when buildOAuthUrl throws", async () => {
|
|
const { app, buildOAuthUrl } = await makeApp();
|
|
buildOAuthUrl.mockImplementationOnce(() => { throw new Error("Missing env"); });
|
|
const res = await app.fetch(new Request("http://localhost/auth/url"));
|
|
expect(res.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe("GET /callback", () => {
|
|
it("returns 400 when code parameter is missing", async () => {
|
|
const { app } = await makeApp();
|
|
const res = await app.fetch(new Request("http://localhost/auth/callback"));
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("redirects with isNew=true for a new user", async () => {
|
|
const { app, exchangeCode, fetchDiscordUser, prisma } = await makeApp();
|
|
exchangeCode.mockResolvedValueOnce({ access_token: "token" });
|
|
fetchDiscordUser.mockResolvedValueOnce({ id: "new_user", username: "Newbie", discriminator: "0", avatar: null });
|
|
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null);
|
|
const createdPlayer = {
|
|
discordId: "new_user", username: "Newbie", discriminator: "0", avatar: null,
|
|
characterName: "Newbie", createdAt: 0, lastSavedAt: 0,
|
|
totalGoldEarned: 0, totalClicks: 0, lifetimeGoldEarned: 0, lifetimeClicks: 0,
|
|
lifetimeBossesDefeated: 0, lifetimeQuestsCompleted: 0,
|
|
lifetimeAdventurersRecruited: 0, lifetimeAchievementsUnlocked: 0,
|
|
};
|
|
vi.mocked(prisma.player.create).mockResolvedValueOnce(createdPlayer as never);
|
|
vi.mocked(prisma.gameState.create).mockResolvedValueOnce({} as never);
|
|
const res = await app.fetch(new Request("http://localhost/auth/callback?code=auth_code"));
|
|
expect(res.status).toBe(302);
|
|
const location = res.headers.get("Location") ?? "";
|
|
expect(location).toContain("isNew=true");
|
|
expect(location).toContain("token=test_jwt");
|
|
});
|
|
|
|
it("redirects with isNew=false for an existing user", async () => {
|
|
const { app, exchangeCode, fetchDiscordUser, prisma } = await makeApp();
|
|
exchangeCode.mockResolvedValueOnce({ access_token: "token" });
|
|
fetchDiscordUser.mockResolvedValueOnce({ id: "existing_user", username: "OldTimer", discriminator: "0", avatar: null });
|
|
const existingPlayer = { discordId: "existing_user", username: "OldTimer", discriminator: "0", avatar: null, characterName: "OldTimer" };
|
|
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(existingPlayer as never);
|
|
const updatedPlayer = { ...existingPlayer, discordId: "existing_user" };
|
|
vi.mocked(prisma.player.update).mockResolvedValueOnce(updatedPlayer as never);
|
|
const res = await app.fetch(new Request("http://localhost/auth/callback?code=auth_code"));
|
|
expect(res.status).toBe(302);
|
|
const location = res.headers.get("Location") ?? "";
|
|
expect(location).toContain("isNew=false");
|
|
});
|
|
|
|
it("redirects with error when callback throws", async () => {
|
|
const { app, exchangeCode } = await makeApp();
|
|
exchangeCode.mockRejectedValueOnce(new Error("OAuth failed"));
|
|
const res = await app.fetch(new Request("http://localhost/auth/callback?code=bad_code"));
|
|
expect(res.status).toBe(302);
|
|
const location = res.headers.get("Location") ?? "";
|
|
expect(location).toContain("error=auth_failed");
|
|
});
|
|
|
|
it("redirects with error when callback throws a non-Error value", async () => {
|
|
const { app, exchangeCode } = await makeApp();
|
|
exchangeCode.mockRejectedValueOnce("raw string error");
|
|
const res = await app.fetch(new Request("http://localhost/auth/callback?code=bad_code"));
|
|
expect(res.status).toBe(302);
|
|
const location = res.headers.get("Location") ?? "";
|
|
expect(location).toContain("error=auth_failed");
|
|
});
|
|
});
|
|
});
|