/* 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 { beforeEach, describe, expect, it, vi } from "vitest"; import { Hono } from "hono"; vi.mock("../../src/db/client.js", () => ({ prisma: { player: { findMany: vi.fn() }, gameState: { findMany: vi.fn() }, }, })); const makePlayer = (overrides: Record = {}) => ({ discordId: "player_1", characterName: "Hero", username: "hero", avatar: null, profileSettings: null, activeTitle: null, lifetimeGoldEarned: 0, lifetimeBossesDefeated: 0, lifetimeQuestsCompleted: 0, lifetimeAchievementsUnlocked: 0, ...overrides, }); describe("leaderboards route", () => { let app: Hono; let prisma: { player: { findMany: ReturnType }; gameState: { findMany: ReturnType } }; beforeEach(async () => { vi.clearAllMocks(); const { leaderboardRouter } = await import("../../src/routes/leaderboards.js"); const { prisma: p } = await import("../../src/db/client.js"); prisma = p as typeof prisma; app = new Hono(); app.route("/leaderboards", leaderboardRouter); }); const get = (query = "") => app.fetch(new Request(`http://localhost/leaderboards${query ? `?${query}` : ""}`)); it("returns 400 for an invalid category", async () => { const res = await get("category=invalid"); expect(res.status).toBe(400); }); it("returns totalGold leaderboard by default", async () => { const players = [ makePlayer({ discordId: "p1", lifetimeGoldEarned: 1000 }), makePlayer({ discordId: "p2", lifetimeGoldEarned: 500 }), ]; vi.mocked(prisma.player.findMany).mockResolvedValueOnce(players as never); const res = await get(); expect(res.status).toBe(200); const body = await res.json() as { category: string; entries: Array<{ rank: number; value: number }> }; expect(body.category).toBe("totalGold"); expect(body.entries[0]?.value).toBe(1000); expect(body.entries[0]?.rank).toBe(1); }); it("returns bossesDefeated leaderboard", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ lifetimeBossesDefeated: 42 })] as never); const res = await get("category=bossesDefeated"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(42); }); it("returns questsCompleted leaderboard", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ lifetimeQuestsCompleted: 7 })] as never); const res = await get("category=questsCompleted"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(7); }); it("returns achievementsUnlocked leaderboard", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ lifetimeAchievementsUnlocked: 3 })] as never); const res = await get("category=achievementsUnlocked"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(3); }); it("returns prestigeCount from game state", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([{ discordId: "p1", state: { prestige: { count: 5 }, transcendence: null, apotheosis: null }, }] as never); const res = await get("category=prestigeCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(5); }); it("returns transcendenceCount from game state", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([{ discordId: "p1", state: { prestige: { count: 0 }, transcendence: { count: 2 }, apotheosis: null }, }] as never); const res = await get("category=transcendenceCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(2); }); it("returns apotheosisCount from game state", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([{ discordId: "p1", state: { prestige: { count: 0 }, transcendence: null, apotheosis: { count: 1 } }, }] as never); const res = await get("category=apotheosisCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(1); }); it("filters out players with showOnLeaderboards=false", async () => { const players = [ makePlayer({ discordId: "visible", lifetimeGoldEarned: 100 }), makePlayer({ discordId: "hidden", lifetimeGoldEarned: 200, profileSettings: { showOnLeaderboards: false } }), ]; vi.mocked(prisma.player.findMany).mockResolvedValueOnce(players as never); const res = await get(); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ discordId: string }> }; expect(body.entries).toHaveLength(1); expect(body.entries[0]?.discordId).toBe("visible"); }); it("respects the limit parameter", async () => { const players = Array.from({ length: 5 }, (_, i) => makePlayer({ discordId: `p${String(i)}`, lifetimeGoldEarned: i })); vi.mocked(prisma.player.findMany).mockResolvedValueOnce(players as never); const res = await get("limit=2"); expect(res.status).toBe(200); const body = await res.json() as { entries: unknown[] }; expect(body.entries).toHaveLength(2); }); it("uses active title name in entries", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([ makePlayer({ discordId: "p1", activeTitle: "the_first" }), ] as never); const res = await get(); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ activeTitle: string }> }; // title may or may not be found — just verify the field exists expect(typeof body.entries[0]?.activeTitle).toBe("string"); }); it("returns 500 when the database throws", async () => { vi.mocked(prisma.player.findMany).mockRejectedValueOnce(new Error("DB error")); const res = await get(); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value", async () => { vi.mocked(prisma.player.findMany).mockRejectedValueOnce("raw string error"); const res = await get(); expect(res.status).toBe(500); }); it("defaults to 0 for game-state categories when state is missing", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([] as never); const res = await get("category=prestigeCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(0); }); it("resolves title name when active title ID is found in TITLES", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([ makePlayer({ discordId: "p1", activeTitle: "the_adventurous" }), ] as never); const res = await get(); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ activeTitle: string }> }; // "the_adventurous" has name "The Adventurous" in TITLES — should differ from raw ID expect(body.entries[0]?.activeTitle).toBe("The Adventurous"); }); it("defaults to 0 for transcendenceCount when transcendence is null in state", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([{ discordId: "p1", state: { prestige: { count: 0 }, transcendence: null, apotheosis: null }, }] as never); const res = await get("category=transcendenceCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(0); }); it("defaults to 0 for apotheosisCount when apotheosis is null in state", async () => { vi.mocked(prisma.player.findMany).mockResolvedValueOnce([makePlayer({ discordId: "p1" })] as never); vi.mocked(prisma.gameState.findMany).mockResolvedValueOnce([{ discordId: "p1", state: { prestige: { count: 0 }, transcendence: null, apotheosis: null }, }] as never); const res = await get("category=apotheosisCount"); expect(res.status).toBe(200); const body = await res.json() as { entries: Array<{ value: number }> }; expect(body.entries[0]?.value).toBe(0); }); });