/* 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 { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import { Hono } from "hono"; import type { GameState, GoddessExplorationArea } from "@elysium/types"; vi.mock("../../src/db/client.js", () => ({ prisma: { gameState: { findUnique: vi.fn(), update: 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(); }), })); // Custom test areas exercising event types not present in the real data const PRAYERS_LOSS_AREA: GoddessExplorationArea = { description: "Test area for prayers_loss events", durationSeconds: 1, events: [ { effect: { amount: 100, type: "prayers_loss" }, id: "test_prayers_loss", text: "You lost some prayers." }, ], id: "test_prayers_loss_area", name: "Test Prayers Loss Area", possibleMaterials: [], zoneId: "goddess_celestial_garden", }; const DIVINITY_GAIN_AREA: GoddessExplorationArea = { description: "Test area for divinity_gain events", durationSeconds: 1, events: [ { effect: { amount: 10, type: "divinity_gain" }, id: "test_divinity_gain", text: "You gained divinity." }, ], id: "test_divinity_gain_area", name: "Test Divinity Gain Area", possibleMaterials: [], zoneId: "goddess_celestial_garden", }; vi.mock("../../src/data/goddessExplorations.js", async (importOriginal) => { const original = await importOriginal(); return { defaultGoddessExplorationAreas: [ ...original.defaultGoddessExplorationAreas, PRAYERS_LOSS_AREA, DIVINITY_GAIN_AREA, ], }; }); const DISCORD_ID = "test_discord_id"; // garden_glade: zoneId=goddess_celestial_garden, durationSeconds=30 // events[0]: prayers_gain 50; events[1]: disciple_loss 0.05 // possibleMaterials: divine_petal(weight 5), prayer_crystal(weight 3) — total 8 const TEST_AREA_ID = "garden_glade"; const TEST_ZONE_ID = "goddess_celestial_garden"; // celestial_meadow: durationSeconds=60 // events[0]: prayers_gain 200; events[1]: sacred_material_gain celestial_dust qty 2 const MATERIAL_AREA_ID = "celestial_meadow"; const makeGoddessState = (areaId: string, zoneStatus: "unlocked" | "locked" = "unlocked"): NonNullable => ({ zones: [ { id: TEST_ZONE_ID, name: "Celestial Garden", description: "", emoji: "🌸", status: zoneStatus, unlockBossId: null, unlockQuestId: null, }, ], bosses: [], quests: [], disciples: [ { id: "novice", name: "Novice", count: 100, costPrayers: 10, prayersPerSecond: 1, description: "", zoneId: TEST_ZONE_ID }, ], equipment: [], upgrades: [], achievements: [], consecration: { count: 0, divinity: 50, productionMultiplier: 1, purchasedUpgradeIds: [], }, enlightenment: { count: 0, stardust: 0, purchasedUpgradeIds: [], stardustPrayersMultiplier: 1, stardustCombatMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustMetaMultiplier: 1, }, exploration: { areas: [ { id: areaId, status: "available" }, ], materials: [], craftedRecipeIds: [], craftedPrayersMultiplier: 1, craftedDivinityMultiplier: 1, craftedCombatMultiplier: 1, }, totalPrayersEarned: 0, lifetimePrayersEarned: 0, lifetimeBossesDefeated: 0, lifetimeQuestsCompleted: 0, baseClickPower: 1, lastTickAt: 0, }); 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: 0, schemaVersion: 1, ...overrides, } as GameState); /** Builds a state with the area in_progress and startedAt in the past so it's complete. */ const makeCompletedAreaState = ( areaId: string, extraMaterials: Array<{ materialId: string; quantity: number }> = [], extraPrayers = 0, ): GameState => { const goddess = makeGoddessState(areaId); goddess.exploration.areas = [{ id: areaId, status: "in_progress", startedAt: 0 }]; goddess.exploration.materials = extraMaterials; return makeState({ goddess, resources: { gold: 0, essence: 0, crystals: 0, runestones: 0, prayers: extraPrayers } }); }; describe("goddessExplore route", () => { let app: Hono; let prisma: { gameState: { findUnique: ReturnType; update: ReturnType } }; beforeEach(async () => { vi.clearAllMocks(); const { goddessExploreRouter } = await import("../../src/routes/goddessExplore.js"); const { prisma: p } = await import("../../src/db/client.js"); prisma = p as typeof prisma; app = new Hono(); app.route("/goddess-explore", goddessExploreRouter); }); afterEach(() => { vi.restoreAllMocks(); }); const getClaimable = (areaId?: string) => { const url = areaId === undefined ? "http://localhost/goddess-explore/claimable" : `http://localhost/goddess-explore/claimable?areaId=${areaId}`; return app.fetch(new Request(url)); }; const postStart = (body: Record) => app.fetch(new Request("http://localhost/goddess-explore/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })); const postCollect = (body: Record) => app.fetch(new Request("http://localhost/goddess-explore/collect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })); describe("GET /claimable", () => { it("returns 400 when areaId is missing", async () => { const res = await getClaimable(); expect(res.status).toBe(400); }); it("returns 404 for unknown area", async () => { const res = await getClaimable("nonexistent_area"); expect(res.status).toBe(404); }); it("returns 404 when no save is found", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(404); }); it("returns claimable=false when goddess is undefined", async () => { const state = makeState(); // no goddess vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(200); const body = await res.json() as { claimable: boolean }; expect(body.claimable).toBe(false); }); it("returns claimable=false when area is not in_progress", async () => { const goddess = makeGoddessState(TEST_AREA_ID); // area status is "available" (not in_progress) const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(200); const body = await res.json() as { claimable: boolean }; expect(body.claimable).toBe(false); }); it("returns claimable=false when area not found in state", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = []; // area missing entirely const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(200); const body = await res.json() as { claimable: boolean }; expect(body.claimable).toBe(false); }); it("returns claimable=false when exploration is not yet complete", async () => { const goddess = makeGoddessState(TEST_AREA_ID); // startedAt = now → not complete yet (duration is 30s) goddess.exploration.areas = [{ id: TEST_AREA_ID, status: "in_progress", startedAt: Date.now() }]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(200); const body = await res.json() as { claimable: boolean }; expect(body.claimable).toBe(false); }); it("returns claimable=true when exploration is complete", async () => { const goddess = makeGoddessState(TEST_AREA_ID); // startedAt = 0 → expired long ago goddess.exploration.areas = [{ id: TEST_AREA_ID, status: "in_progress", startedAt: 0 }]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(200); const body = await res.json() as { claimable: boolean }; expect(body.claimable).toBe(true); }); it("returns 500 when the database throws an Error", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error"); const res = await getClaimable(TEST_AREA_ID); expect(res.status).toBe(500); }); }); describe("POST /start", () => { it("returns 400 when areaId is missing", async () => { const res = await postStart({}); expect(res.status).toBe(400); }); it("returns 404 for unknown area", async () => { const res = await postStart({ areaId: "nonexistent_area" }); expect(res.status).toBe(404); }); it("returns 404 when no save is found", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(404); }); it("returns 400 when goddess is undefined", async () => { const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 400 when zone is not unlocked", async () => { const goddess = makeGoddessState(TEST_AREA_ID, "locked"); const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 400 when zone is not found in goddess state", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.zones = []; // zone missing const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 404 when area is not found in state", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = []; // area missing const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(404); }); it("returns 400 when another exploration is already in progress", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = [ { id: TEST_AREA_ID, status: "available" }, { id: "other_area", status: "in_progress" }, ]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 400 when area is locked", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = [{ id: TEST_AREA_ID, status: "locked" }]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 200 with areaId and endsAt on success", async () => { const goddess = makeGoddessState(TEST_AREA_ID); const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { areaId: string; endsAt: number }; expect(body.areaId).toBe(TEST_AREA_ID); expect(body.endsAt).toBeGreaterThan(Date.now()); }); it("returns 500 when the database throws an Error", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error"); const res = await postStart({ areaId: TEST_AREA_ID }); expect(res.status).toBe(500); }); }); describe("POST /collect", () => { it("returns 400 when areaId is missing", async () => { const res = await postCollect({}); expect(res.status).toBe(400); }); it("returns 404 for unknown area", async () => { const res = await postCollect({ areaId: "nonexistent_area" }); expect(res.status).toBe(404); }); it("returns 404 when no save is found", async () => { vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(404); }); it("returns 400 when goddess is undefined", async () => { const state = makeState(); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 404 when area is not found in state", async () => { const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = []; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(404); }); it("returns 400 when area is not in_progress", async () => { const goddess = makeGoddessState(TEST_AREA_ID); // area is "available", not "in_progress" const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns 400 when exploration is not yet complete", async () => { const goddess = makeGoddessState(TEST_AREA_ID); // startedAt = now → still in progress for 30s goddess.exploration.areas = [{ id: TEST_AREA_ID, status: "in_progress", startedAt: Date.now() }]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(400); }); it("returns foundNothing=true when Math.random is below 0.2 (nothing path)", async () => { vi.spyOn(Math, "random").mockReturnValue(0.1); // < 0.2 → nothing const state = makeCompletedAreaState(TEST_AREA_ID); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { foundNothing: boolean; nothingMessage: string; materialsFound: unknown[] }; expect(body.foundNothing).toBe(true); expect(typeof body.nothingMessage).toBe("string"); }); it("applies prayers_gain event and returns prayersChange > 0", async () => { const mockRandom = vi.spyOn(Math, "random"); // garden_glade has 2 events: [prayers_gain(0), disciple_loss(1)] // Call 1: nothing check 0.5 ≥ 0.2 → proceed // Call 2: event index Math.floor(0.1 * 2) = 0 → prayers_gain // Call 3: possibleMaterials roll (total weight 8): 0 * 8 = 0, 0 - 5 = -5 ≤ 0 → divine_petal // Call 4: quantity Math.floor(0 * (3-1+1)) + 1 = 0 + 1 = 1 mockRandom .mockReturnValueOnce(0.5) .mockReturnValueOnce(0.1) .mockReturnValueOnce(0) .mockReturnValueOnce(0); const state = makeCompletedAreaState(TEST_AREA_ID); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { foundNothing: boolean; event: { prayersChange: number }; materialsFound: Array<{ materialId: string }> }; expect(body.foundNothing).toBe(false); expect(body.event.prayersChange).toBeGreaterThan(0); }); it("applies sacred_material_gain event and pushes new material", async () => { const mockRandom = vi.spyOn(Math, "random"); // celestial_meadow: events[1] = sacred_material_gain celestial_dust qty 2 // index 1: Math.floor(0.6 * 2) = 1 // possibleMaterials: [divine_petal(4), celestial_dust(3)] total 7 // Call 3: 0 * 7 = 0, 0 - 4 = -4 ≤ 0 → divine_petal (new material) // Call 4: Math.floor(0 * (4-2+1)) + 2 = 0 + 2 = 2 mockRandom .mockReturnValueOnce(0.5) // nothing check: proceed .mockReturnValueOnce(0.6) // event index: Math.floor(0.6 * 2) = 1 → sacred_material_gain .mockReturnValueOnce(0) // possibleMaterials roll: 0 * 7 = 0, 0 - 4 = -4 ≤ 0 → divine_petal .mockReturnValueOnce(0); // quantity: Math.floor(0 * 3) + 2 = 2 const state = makeCompletedAreaState(MATERIAL_AREA_ID); // no materials in state vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: MATERIAL_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { event: { materialGained: { materialId: string; quantity: number } }; materialsFound: Array<{ materialId: string }> }; expect(body.event.materialGained?.materialId).toBe("celestial_dust"); }); it("increments existing material quantity on sacred_material_gain event", async () => { const mockRandom = vi.spyOn(Math, "random"); // Same as above — celestial_meadow events[1] = sacred_material_gain celestial_dust mockRandom .mockReturnValueOnce(0.5) // nothing check: proceed .mockReturnValueOnce(0.6) // event: sacred_material_gain .mockReturnValueOnce(0) // possibleMaterials roll → divine_petal .mockReturnValueOnce(0); // quantity → 2 const state = makeCompletedAreaState(MATERIAL_AREA_ID, [ { materialId: "celestial_dust", quantity: 5 }, // pre-existing ]); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: MATERIAL_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { event: { materialGained: { materialId: string; quantity: number } } }; expect(body.event.materialGained?.materialId).toBe("celestial_dust"); expect(body.event.materialGained?.quantity).toBeGreaterThan(0); }); it("returns materialsFound with new material from possibleMaterials when none pre-existing", async () => { const mockRandom = vi.spyOn(Math, "random"); // prayers_gain event, then roll for divine_petal (new) mockRandom .mockReturnValueOnce(0.5) // nothing check: proceed .mockReturnValueOnce(0.1) // event: prayers_gain (index 0) .mockReturnValueOnce(0) // possibleMaterials roll → divine_petal (first, weight 5) .mockReturnValueOnce(0); // quantity → min (1) const state = makeCompletedAreaState(TEST_AREA_ID); // no materials vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { materialsFound: Array<{ materialId: string; quantity: number }> }; expect(body.materialsFound.length).toBeGreaterThan(0); expect(body.materialsFound[0]?.materialId).toBe("divine_petal"); }); it("increments existing possibleMaterial quantity when material is already in state", async () => { const mockRandom = vi.spyOn(Math, "random"); mockRandom .mockReturnValueOnce(0.5) // nothing check: proceed .mockReturnValueOnce(0.1) // event: prayers_gain (index 0) .mockReturnValueOnce(0) // possibleMaterials roll → divine_petal .mockReturnValueOnce(0); // quantity → 1 const state = makeCompletedAreaState(TEST_AREA_ID, [ { materialId: "divine_petal", quantity: 10 }, // pre-existing ]); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); const body = await res.json() as { materialsFound: Array<{ materialId: string }> }; expect(body.materialsFound.some((m) => { return m.materialId === "divine_petal"; })).toBe(true); }); it("applies disciple_loss event and reduces disciple counts", async () => { const mockRandom = vi.spyOn(Math, "random"); // garden_glade events[1] = disciple_loss fraction 0.05 // Call 1: nothing check 0.5 ≥ 0.2 → proceed // Call 2: event index Math.floor(0.6 * 2) = 1 → disciple_loss // possibleMaterials: total weight 8; call 3: 0.9 * 8 = 7.2; 7.2 - 5 = 2.2 > 0; 2.2 - 3 = -0.8 ≤ 0 → prayer_crystal // Call 4: quantity for prayer_crystal: Math.floor(0 * (2-1+1)) + 1 = 1 mockRandom .mockReturnValueOnce(0.5) // nothing check: proceed .mockReturnValueOnce(0.6) // event: Math.floor(0.6 * 2) = 1 → disciple_loss .mockReturnValueOnce(0.9) // possibleMaterials roll → prayer_crystal .mockReturnValueOnce(0); // quantity → 1 const goddess = makeGoddessState(TEST_AREA_ID); goddess.exploration.areas = [{ id: TEST_AREA_ID, status: "in_progress", startedAt: 0 }]; // Need disciples with non-zero count so lost > 0 triggers goddess.disciples = [ { id: "novice", name: "Novice", count: 100, costPrayers: 10, prayersPerSecond: 1, description: "", zoneId: TEST_ZONE_ID }, ]; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(200); }); it("applies prayers_loss event and returns negative prayersChange", async () => { const mockRandom = vi.spyOn(Math, "random"); // test_prayers_loss_area has 1 event: prayers_loss 100 // Call 1: nothing check 0.5 ≥ 0.2 → proceed // Call 2: event index Math.floor(0.1 * 1) = 0 → prayers_loss // No possibleMaterials, so no further calls needed mockRandom .mockReturnValueOnce(0.5) .mockReturnValueOnce(0.1); const goddess = makeGoddessState(PRAYERS_LOSS_AREA.id); goddess.exploration.areas = [{ id: PRAYERS_LOSS_AREA.id, status: "in_progress", startedAt: 0 }]; const state = makeState({ goddess, resources: { gold: 0, essence: 0, crystals: 0, runestones: 0, prayers: 200 } }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: PRAYERS_LOSS_AREA.id }); expect(res.status).toBe(200); const body = await res.json() as { event: { prayersChange: number }; foundNothing: boolean }; expect(body.foundNothing).toBe(false); expect(body.event.prayersChange).toBeLessThanOrEqual(0); }); it("applies divinity_gain event and increases divinity", async () => { const mockRandom = vi.spyOn(Math, "random"); // test_divinity_gain_area has 1 event: divinity_gain 10 // Call 1: nothing check 0.5 ≥ 0.2 → proceed // Call 2: event index Math.floor(0.1 * 1) = 0 → divinity_gain // No possibleMaterials mockRandom .mockReturnValueOnce(0.5) .mockReturnValueOnce(0.1); const goddess = makeGoddessState(DIVINITY_GAIN_AREA.id); goddess.exploration.areas = [{ id: DIVINITY_GAIN_AREA.id, status: "in_progress", startedAt: 0 }]; const initialDivinity = goddess.consecration.divinity; const state = makeState({ goddess }); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); const res = await postCollect({ areaId: DIVINITY_GAIN_AREA.id }); expect(res.status).toBe(200); // Verify state was saved with updated divinity const updateArg = vi.mocked(prisma.gameState.update).mock.calls[0]![0] as { data: { state: { goddess: { consecration: { divinity: number } } } }; }; expect(updateArg.data.state.goddess.consecration.divinity).toBe(initialDivinity + 10); }); it("returns 500 when the database throws an Error", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(500); }); it("returns 500 when the database throws a non-Error value", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error"); const res = await postCollect({ areaId: TEST_AREA_ID }); expect(res.status).toBe(500); }); }); });