/* 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"); }); }); });