/* 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: { gameState: { findUnique: vi.fn() }, }, })); vi.mock("../../src/services/logger.js", () => ({ logger: { error: vi.fn().mockResolvedValue(undefined), log: vi.fn().mockResolvedValue(undefined), }, })); const makeState = (overrides: Record = {}) => ({ quests: [], exploration: { areas: [] }, ...overrides, }); describe("timers route", () => { let app: Hono; let prisma: { gameState: { findUnique: ReturnType } }; beforeEach(async () => { vi.clearAllMocks(); const { timersRouter } = await import("../../src/routes/timers.js"); const { prisma: p } = await import("../../src/db/client.js"); prisma = p as typeof prisma; app = new Hono(); app.route("/timers", timersRouter); }); const get = (userId: string) => app.fetch(new Request(`http://localhost/timers/${userId}`)); it("returns 400 for a non-numeric user ID", async () => { const res = await get("not-a-number"); expect(res.status).toBe(400); const body = await res.json() as { error: string }; expect(body.error).toBe("Invalid user ID"); }); it("returns 404 when player is not found", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); const res = await get("123456789"); expect(res.status).toBe(404); const body = await res.json() as { error: string }; expect(body.error).toBe("Player not found"); }); it("returns empty arrays when no active quests or explorations", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: makeState(), }); const res = await get("123456789"); expect(res.status).toBe(200); const body = await res.json() as { quests: unknown[]; explorations: unknown[] }; expect(body.quests).toEqual([]); expect(body.explorations).toEqual([]); }); it("returns active quest timers with endsAt computed from startedAt + duration", async () => { const startedAt = Date.now() - 30_000; const state = makeState({ quests: [ { id: "q1", name: "Forest Patrol", status: "active", startedAt: startedAt, durationSeconds: 600, }, ], }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); expect(res.status).toBe(200); const body = await res.json() as { quests: Array<{ questId: string; name: string; endsAt: number; timeLeft: number }>; }; expect(body.quests).toHaveLength(1); expect(body.quests[0]?.questId).toBe("q1"); expect(body.quests[0]?.name).toBe("Forest Patrol"); expect(body.quests[0]?.endsAt).toBe(startedAt + 600_000); expect(body.quests[0]?.timeLeft).toBeGreaterThan(0); }); it("filters out quests that are not in_progress", async () => { const state = makeState({ quests: [ { id: "q1", name: "Done Quest", status: "completed", startedAt: 0, durationSeconds: 60 }, { id: "q2", name: "Idle Quest", status: "available", durationSeconds: 60 }, ], }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); const body = await res.json() as { quests: unknown[] }; expect(body.quests).toHaveLength(0); }); it("returns timeLeft of 0 for already-completed quests still marked in_progress", async () => { const startedAt = Date.now() - 700_000; const state = makeState({ quests: [ { id: "q1", name: "Old Quest", status: "active", startedAt: startedAt, durationSeconds: 600, }, ], }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); const body = await res.json() as { quests: Array<{ timeLeft: number }>; }; expect(body.quests[0]?.timeLeft).toBe(0); }); it("returns active exploration timers", async () => { const endsAt = Date.now() + 120_000; const state = makeState({ exploration: { areas: [ { id: "verdant_meadows", status: "in_progress", endsAt }, { id: "unknown_area_xyz", status: "in_progress", endsAt }, ], }, }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); const body = await res.json() as { explorations: Array<{ areaId: string; name: string; endsAt: number; timeLeft: number }>; }; expect(body.explorations).toHaveLength(2); expect(body.explorations[0]?.areaId).toBe("verdant_meadows"); expect(body.explorations[0]?.endsAt).toBe(endsAt); expect(body.explorations[0]?.timeLeft).toBeGreaterThan(0); // Unknown area falls back to ID as name expect(body.explorations[1]?.name).toBe("unknown_area_xyz"); }); it("filters out explorations not in_progress", async () => { const state = makeState({ exploration: { areas: [ { id: "area1", status: "available" }, { id: "area2", status: "completed" }, ], }, }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); const body = await res.json() as { explorations: unknown[] }; expect(body.explorations).toHaveLength(0); }); it("handles missing exploration state gracefully", async () => { const state = { quests: [] }; vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state }); const res = await get("123456789"); expect(res.status).toBe(200); const body = await res.json() as { explorations: unknown[] }; expect(body.explorations).toHaveLength(0); }); it("returns 500 on database error", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce( new Error("DB failure"), ); const res = await get("123456789"); expect(res.status).toBe(500); const body = await res.json() as { error: string }; expect(body.error).toBe("Internal server error"); }); it("returns 500 and logs non-Error throws", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("string error"); const res = await get("123456789"); expect(res.status).toBe(500); }); });