/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */ /* eslint-disable max-lines -- 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"; import type { GameState } from "@elysium/types"; vi.mock("../../src/db/client.js", () => ({ prisma: { player: { findUnique: vi.fn(), update: vi.fn() }, gameState: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn(), upsert: vi.fn() }, }, })); vi.mock("../../src/middleware/auth.js", () => ({ authMiddleware: vi.fn(async (c: { set: (key: string, value: string) => void }, next: () => Promise) => { c.set("discordId", "test_discord_id"); await next(); }), })); const DISCORD_ID = "test_discord_id"; const CURRENT_SCHEMA_VERSION = 1; const makeState = (overrides: Partial = {}): GameState => ({ player: { discordId: DISCORD_ID, username: "u", discriminator: "0", avatar: null, totalGoldEarned: 0, totalClicks: 0, characterName: "T" }, resources: { gold: 0, essence: 0, crystals: 0, runestones: 0 }, adventurers: [], upgrades: [], quests: [], bosses: [], equipment: [], achievements: [], zones: [], exploration: { areas: [], materials: [], craftedRecipeIds: [], craftedGoldMultiplier: 1, craftedEssenceMultiplier: 1, craftedClickMultiplier: 1, craftedCombatMultiplier: 1 }, companions: { unlockedCompanionIds: [], activeCompanionId: null }, prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] }, baseClickPower: 1, lastTickAt: Date.now() - 60_000, // 60 seconds ago schemaVersion: CURRENT_SCHEMA_VERSION, ...overrides, } as GameState); const makePlayer = (overrides: Record = {}) => ({ discordId: DISCORD_ID, characterName: "T", username: "u", discriminator: "0", avatar: null, createdAt: Date.now(), lastSavedAt: 0, totalGoldEarned: 0, totalClicks: 0, lifetimeGoldEarned: 0, lifetimeClicks: 0, lifetimeBossesDefeated: 0, lifetimeQuestsCompleted: 0, lifetimeAdventurersRecruited: 0, lifetimeAchievementsUnlocked: 0, loginStreak: 1, lastLoginDate: null, unlockedTitles: null, guildName: null, ...overrides, }); describe("game route", () => { let app: Hono; let prisma: { player: { findUnique: ReturnType; update: ReturnType }; gameState: { findUnique: ReturnType; create: ReturnType; update: ReturnType; upsert: ReturnType }; }; beforeEach(async () => { vi.clearAllMocks(); delete process.env["ANTI_CHEAT_SECRET"]; const { gameRouter } = await import("../../src/routes/game.js"); const { prisma: p } = await import("../../src/db/client.js"); prisma = p as typeof prisma; app = new Hono(); app.route("/game", gameRouter); }); afterEach(() => { delete process.env["ANTI_CHEAT_SECRET"]; }); describe("GET /load", () => { it("returns 404 when neither game state nor player exists", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(404); }); it("creates fresh state when game state is missing but player exists", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.create).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { offlineGold: number; schemaOutdated: boolean }; expect(body.offlineGold).toBe(0); expect(body.schemaOutdated).toBe(false); }); it("returns state with offline earnings when game state exists", async () => { const state = makeState({ lastTickAt: Date.now() - 10_000 }); // 10 seconds ago vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never).mockRejectedValueOnce(Object.assign(new Error("conflict"), { code: "P2034" })); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { state: GameState; offlineSeconds: number; currentSchemaVersion: number }; expect(body.currentSchemaVersion).toBe(CURRENT_SCHEMA_VERSION); expect(typeof body.offlineSeconds).toBe("number"); }); it("awards login bonus when player logs in on a new day", async () => { const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10); const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer({ lastLoginDate: yesterday, loginStreak: 3 }) as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { loginBonus: { streak: number; goldEarned: number } | null }; expect(body.loginBonus).not.toBeNull(); expect(body.loginBonus?.streak).toBe(4); }); it("resets streak when login gap is more than one day", async () => { const longAgo = "2020-01-01"; const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer({ lastLoginDate: longAgo, loginStreak: 10 }) as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { loginBonus: { streak: number } | null }; expect(body.loginBonus?.streak).toBe(1); }); it("does not award login bonus when already logged in today", async () => { const todayUTC = new Date().toISOString().slice(0, 10); const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer({ lastLoginDate: todayUTC }) as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { loginBonus: null }; expect(body.loginBonus).toBeNull(); }); it("includes HMAC signature when ANTI_CHEAT_SECRET is set", async () => { process.env["ANTI_CHEAT_SECRET"] = "my_secret"; const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { signature: string | undefined }; expect(typeof body.signature).toBe("string"); }); it("marks schema as outdated when save has older schema version", async () => { const state = makeState({ schemaVersion: 0 }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { schemaOutdated: boolean }; expect(body.schemaOutdated).toBe(true); }); it("returns non-zero offline earnings when adventurers have production stats", async () => { const todayUTC = new Date().toISOString().slice(0, 10); const state = makeState({ adventurers: [{ id: "worker", count: 1, unlocked: true, level: 1, goldPerSecond: 1, essencePerSecond: 1, combatPower: 0, }] as GameState["adventurers"], }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce( makePlayer({ lastLoginDate: todayUTC }) as never, ); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(200); const body = await res.json() as { offlineGold: number; offlineEssence: number }; expect(body.offlineGold).toBeGreaterThan(0); expect(body.offlineEssence).toBeGreaterThan(0); }); }); describe("POST /save", () => { const save = (body: Record) => app.fetch(new Request("http://localhost/game/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })); it("returns 400 when state is missing from body", async () => { const res = await save({}); expect(res.status).toBe(400); }); it("returns 409 when save schema version is outdated", async () => { const state = makeState({ schemaVersion: 0 }); const res = await save({ state }); expect(res.status).toBe(409); }); it("saves state when no previous record exists", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const state = makeState(); const res = await save({ state }); expect(res.status).toBe(200); const body = await res.json() as { savedAt: number }; expect(body.savedAt).toBeGreaterThan(0); }); it("falls back to state characterName when playerRecord is null", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const state = makeState(); const res = await save({ state }); expect(res.status).toBe(200); }); it("validates and sanitizes state when previous record exists", async () => { const prevState = makeState({ resources: { gold: 0, essence: 0, crystals: 0, runestones: 0 } }); const incomingState = makeState({ resources: { gold: 1e400, essence: 0, crystals: 0, runestones: 9999 } }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await save({ state: incomingState }); expect(res.status).toBe(200); }); it("rejects save with wrong HMAC signature when secret is configured", async () => { process.env["ANTI_CHEAT_SECRET"] = "my_secret"; const prevState = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); const res = await save({ state: makeState(), signature: "wrong_signature" }); expect(res.status).toBe(400); }); it("accepts save with correct HMAC signature", async () => { process.env["ANTI_CHEAT_SECRET"] = "my_secret"; const { createHmac } = await import("node:crypto"); const prevState = makeState(); const correctSig = createHmac("sha256", "my_secret").update(JSON.stringify(prevState)).digest("hex"); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await save({ state: makeState(), signature: correctSig }); expect(res.status).toBe(200); }); it("unlocks new titles and persists them", async () => { const prevState = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer({ guildName: "My Guild" }) as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await save({ state: makeState() }); expect(res.status).toBe(200); // Just verifies the route completes without error when title checking runs }); it("exercises all validateAndSanitize branches with rich state", async () => { const now = Date.now(); const prevState = makeState({ resources: { gold: 1000, essence: 50, crystals: 5, runestones: 5 }, adventurers: [ { id: "militia", count: 5, unlocked: true, level: 2, goldPerSecond: 0.5, essencePerSecond: 0, combatPower: 3 }, ] as GameState["adventurers"], upgrades: [ { id: "global_1", purchased: true, unlocked: true, target: "global", multiplier: 2 }, { id: "click_1", purchased: true, unlocked: true, target: "click", multiplier: 1.5 }, ] as GameState["upgrades"], quests: [ // main path: active → completed (startedAt far in past → expired) { id: "first_steps", status: "active", startedAt: 1000 }, // defensive: prevQuest.status === "completed" → skip in computeQuestRewards { id: "goblin_camp", status: "completed", startedAt: 1000 }, // defensive: prevQuest.status !== "active" → skip { id: "haunted_mine", status: "locked", startedAt: null }, // defensive: startedAt == null → skip { id: "ancient_ruins", status: "active", startedAt: null }, // defensive: !questData → skip (not in DEFAULT_QUESTS) { id: "not_a_real_quest", status: "active", startedAt: 1000 }, // anti-rollback: completed in prev, active in incoming → quests.map restores completed { id: "rollback_quest", status: "completed", startedAt: 1000 }, ] as GameState["quests"], bosses: [ // main path in computeBossRewards: available → defeated { id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, damagePerSecond: 5, goldReward: 10000, essenceReward: 25, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, // defensive: prevBoss.status === "defeated" → skip { id: "lich_queen", status: "defeated", currentHp: 0, maxHp: 10000, damagePerSecond: 20, goldReward: 100000, essenceReward: 200, crystalReward: 10, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, // defensive: prevBoss.status === "locked" → skip { id: "forest_giant", status: "locked", currentHp: 35000, maxHp: 35000, damagePerSecond: 40, goldReward: 350000, essenceReward: 400, crystalReward: 20, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, // defensive: !bossData → skip (not in DEFAULT_BOSSES) { id: "not_a_real_boss", status: "available", currentHp: 100, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, // anti-rollback: defeated in prev, available in incoming → bosses.map restores defeated { id: "anti_rollback_boss", status: "defeated", currentHp: 0, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, ] as GameState["bosses"], achievements: [ { id: "ach1", unlockedAt: 1000 }, // prev has unlockedAt → anti-rollback when incoming=null { id: "ach2", unlockedAt: null }, // prev null → future timestamp check → caught { id: "ach3", unlockedAt: null }, // prev null → legitimate past unlock → return a ] as GameState["achievements"], exploration: { areas: [], materials: [{ materialId: "verdant_sap", quantity: 10 }], craftedRecipeIds: ["haunted_mine_recipe"], craftedGoldMultiplier: 2, craftedEssenceMultiplier: 1, craftedClickMultiplier: 1, craftedCombatMultiplier: 1, }, transcendence: { count: 1, echoes: 10, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 }, apotheosis: { count: 2 }, story: { unlockedChapterIds: ["ch1"], completedChapters: [{ chapterId: "ch1", completedAt: 1000 }] }, }); const incomingState = makeState({ resources: { gold: 1e18, essence: 1e18, crystals: 5, runestones: 0 }, adventurers: [ { id: "militia", count: 5, unlocked: true, level: 2, goldPerSecond: 0.5, essencePerSecond: 0, combatPower: 3 }, ] as GameState["adventurers"], upgrades: [ { id: "global_1", purchased: true, unlocked: true, target: "global", multiplier: 2 }, { id: "click_1", purchased: true, unlocked: true, target: "click", multiplier: 1.5 }, ] as GameState["upgrades"], quests: [ { id: "first_steps", status: "completed", startedAt: 1000 }, // was active → now completed { id: "goblin_camp", status: "completed", startedAt: 1000 }, // both completed → skip { id: "haunted_mine", status: "completed", startedAt: null }, // prevStatus=locked → skip { id: "ancient_ruins", status: "completed", startedAt: null }, // startedAt=null → skip { id: "not_a_real_quest", status: "completed", startedAt: 1000 }, // !questData → skip { id: "rollback_quest", status: "active", startedAt: 1000 }, // anti-rollback → restored { id: "orphan_quest", status: "completed", startedAt: 1000 }, // !prevQuest → skip { id: "still_active_quest", status: "active", startedAt: 1000 }, // status !== completed → skip ] as GameState["quests"], bosses: [ { id: "troll_king", status: "defeated", currentHp: 0, maxHp: 1000, damagePerSecond: 5, goldReward: 10000, essenceReward: 25, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "lich_queen", status: "defeated", currentHp: 0, maxHp: 10000, damagePerSecond: 20, goldReward: 100000, essenceReward: 200, crystalReward: 10, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "forest_giant", status: "defeated", currentHp: 0, maxHp: 35000, damagePerSecond: 40, goldReward: 350000, essenceReward: 400, crystalReward: 20, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "not_a_real_boss", status: "defeated", currentHp: 0, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "anti_rollback_boss", status: "available", currentHp: 100, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "orphan_boss", status: "defeated", currentHp: 0, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, { id: "still_available_boss", status: "available", currentHp: 100, maxHp: 100, damagePerSecond: 1, goldReward: 0, essenceReward: 0, crystalReward: 0, upgradeRewards: [], equipmentRewards: [], prestigeRequirement: 0 }, ] as GameState["bosses"], achievements: [ { id: "ach1", unlockedAt: null }, // prev had unlockedAt → anti-rollback restores it { id: "ach2", unlockedAt: now + 99999 }, // future timestamp → cheat caught { id: "ach3", unlockedAt: 1000 }, // past timestamp → legitimate unlock { id: "ach4", unlockedAt: null }, // not in prev → !prev → return a ] as GameState["achievements"], exploration: { areas: [], materials: [{ materialId: "verdant_sap", quantity: 1000 }], // inflated → capped at 10 craftedRecipeIds: ["haunted_mine_recipe", "fake_recipe"], // fake_recipe filtered out craftedGoldMultiplier: 1, craftedEssenceMultiplier: 1, craftedClickMultiplier: 1, craftedCombatMultiplier: 1, }, transcendence: { count: 1, echoes: 100, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 }, apotheosis: { count: 5 }, story: { unlockedChapterIds: ["ch1", "ch2"], completedChapters: [{ chapterId: "ch1", completedAt: 1000 }, { chapterId: "ch2", completedAt: now }], }, }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer({ createdAt: Date.now() }) as never); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await save({ state: incomingState }); expect(res.status).toBe(200); const body = await res.json() as { savedAt: number }; expect(body.savedAt).toBeGreaterThan(0); }); it("validates companion when active companion is legitimately unlocked", async () => { const prevState = makeState(); const stateWithCompanion = makeState({ companions: { unlockedCompanionIds: [], activeCompanionId: "lyra" }, }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never); vi.mocked(prisma.player.findUnique).mockResolvedValueOnce( makePlayer({ lifetimeBossesDefeated: 100 }) as never, ); vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await save({ state: stateWithCompanion }); expect(res.status).toBe(200); }); }); describe("GET /load error path", () => { it("returns 500 when the database throws during load", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); vi.mocked(prisma.player.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value during load", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error"); vi.mocked(prisma.player.findUnique).mockRejectedValueOnce("raw string error"); const res = await app.fetch(new Request("http://localhost/game/load")); expect(res.status).toBe(500); }); }); describe("POST /save error path", () => { const save = (body: Record) => app.fetch(new Request("http://localhost/game/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })); it("returns 500 when the database throws during save", async () => { const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await save({ state }); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value during save", async () => { const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error"); const res = await save({ state }); expect(res.status).toBe(500); }); }); describe("POST /reset", () => { const reset = () => app.fetch(new Request("http://localhost/game/reset", { method: "POST" })); it("returns 404 when player is not found", async () => { vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null); const res = await reset(); expect(res.status).toBe(404); }); it("creates fresh state and returns it on success", async () => { vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await reset(); expect(res.status).toBe(200); const body = await res.json() as { offlineGold: number; schemaOutdated: boolean; loginBonus: null }; expect(body.offlineGold).toBe(0); expect(body.schemaOutdated).toBe(false); expect(body.loginBonus).toBeNull(); }); it("includes HMAC signature in reset response when secret is configured", async () => { process.env["ANTI_CHEAT_SECRET"] = "reset_secret"; vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never); vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never); const res = await reset(); expect(res.status).toBe(200); const body = await res.json() as { signature: string | undefined }; expect(typeof body.signature).toBe("string"); }); it("returns 500 when the database throws during reset", async () => { vi.mocked(prisma.player.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await reset(); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value during reset", async () => { vi.mocked(prisma.player.findUnique).mockRejectedValueOnce("raw string error"); const res = await reset(); expect(res.status).toBe(500); }); }); });