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)
211 lines
9.0 KiB
TypeScript
211 lines
9.0 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 { 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<string, unknown> = {}) => ({
|
|
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<typeof vi.fn> }; gameState: { findMany: ReturnType<typeof vi.fn> } };
|
|
|
|
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);
|
|
});
|
|
});
|