Files
elysium/apps/api/test/routes/debug.spec.ts
T
hikari 6e573bea14
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m5s
CI / Lint, Build & Test (push) Successful in 1m9s
chore: more feedback fixes (#129)
## Summary

- Fix `NaN` displayed in Sync New Content / Force Unlock notifications by guarding against undefined counts
- Poll server for exploration claimability before showing Collect button to prevent client/server desync
- Return authoritative materials list from craft API to prevent client desync causing false affordability
- Add test coverage for `sync-new-content` and `explore/claimable` endpoints

Closes #125
Closes #127
Closes #128

## Test plan

- [ ] Trigger a sync with new content and verify the notification shows a real count instead of `NaN`
- [ ] Start an exploration, wait for it to complete, and verify the Collect button only appears after the server confirms claimable
- [ ] Attempt to craft a recipe and verify the material counts in the UI update to match the server's authoritative values

 This issue was created with help from Hikari~ 🌸

Reviewed-on: #129
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-24 13:20:37 -07:00

792 lines
37 KiB
TypeScript

/* 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, describe, expect, it, vi } from "vitest";
import { Hono } from "hono";
import type { GameState } from "@elysium/types";
vi.mock("../../src/db/client.js", () => ({
prisma: {
gameState: { findUnique: vi.fn(), update: vi.fn(), upsert: vi.fn() },
player: { findUnique: vi.fn() },
},
}));
vi.mock("../../src/middleware/auth.js", () => ({
authMiddleware: vi.fn(async (c: { set: (key: string, value: string) => void }, next: () => Promise<void>) => {
c.set("discordId", "test_discord_id");
await next();
}),
}));
vi.mock("../../src/services/logger.js", () => ({
logger: {
error: vi.fn().mockResolvedValue(undefined),
log: vi.fn().mockResolvedValue(undefined),
},
}));
const DISCORD_ID = "test_discord_id";
const makeExploration = (areas: GameState["exploration"]["areas"] = []): GameState["exploration"] => ({
areas: areas,
craftedCombatMultiplier: 1,
craftedClickMultiplier: 1,
craftedEssenceMultiplier: 1,
craftedGoldMultiplier: 1,
craftedRecipeIds: [],
materials: [],
});
const makeState = (overrides: Partial<GameState> = {}): GameState => ({
achievements: [],
adventurers: [],
baseClickPower: 1,
bosses: [],
companions: { activeCompanionId: null, unlockedCompanionIds: [] },
equipment: [],
exploration: makeExploration(),
lastTickAt: 0,
player: { avatar: null, characterName: "T", discordId: DISCORD_ID, discriminator: "0", totalClicks: 0, totalGoldEarned: 0, username: "u" },
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
quests: [],
resources: { crystals: 0, essence: 0, gold: 0, runestones: 0 },
schemaVersion: 1,
upgrades: [],
zones: [],
...overrides,
} as GameState);
const makePlayer = (overrides: Record<string, unknown> = {}) => ({
avatar: null,
characterName: "TestChar",
createdAt: 0,
discordId: DISCORD_ID,
discriminator: "0",
lifetimeAchievementsUnlocked: 0,
lifetimeAdventurersRecruited: 0,
lifetimeBossesDefeated: 0,
lifetimeClicks: 0,
lifetimeGoldEarned: 0,
lifetimeQuestsCompleted: 0,
loginStreak: 1,
username: "test_user",
...overrides,
});
describe("debug route", () => {
let app: Hono;
let prisma: {
gameState: {
findUnique: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
upsert: ReturnType<typeof vi.fn>;
};
player: { findUnique: ReturnType<typeof vi.fn> };
};
beforeEach(async () => {
vi.clearAllMocks();
const { debugRouter } = await import("../../src/routes/debug.js");
const { prisma: p } = await import("../../src/db/client.js");
prisma = p as typeof prisma;
app = new Hono();
app.route("/debug", debugRouter);
});
const forceUnlocks = () =>
app.fetch(new Request("http://localhost/debug/force-unlocks", { method: "POST" }));
const hardReset = () =>
app.fetch(new Request("http://localhost/debug/hard-reset", { method: "POST" }));
describe("POST /force-unlocks", () => {
it("returns 404 when no game state found", async () => {
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
const res = await forceUnlocks();
expect(res.status).toBe(404);
});
it("returns 200 with all zeros when no stale locks exist", async () => {
const state = makeState({
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
expect(res.status).toBe(200);
const body = await res.json() as {
bossesUnlocked: number;
explorationUnlocked: number;
questsUnlocked: number;
zonesUnlocked: number;
};
expect(body.zonesUnlocked).toBe(0);
expect(body.explorationUnlocked).toBe(0);
});
it("unlocks verdant_vale when it is locked and has no requirements", async () => {
const state = makeState({
zones: [{ id: "verdant_vale", status: "locked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
expect(res.status).toBe(200);
const body = await res.json() as { zonesUnlocked: number };
expect(body.zonesUnlocked).toBe(1);
});
it("does not unlock zone when boss condition is not met", async () => {
const state = makeState({
bosses: [{ id: "forest_giant", status: "available" }] as GameState["bosses"],
quests: [{ id: "ancient_ruins", status: "completed" }] as GameState["quests"],
zones: [{ id: "shattered_ruins", status: "locked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { zonesUnlocked: number };
expect(body.zonesUnlocked).toBe(0);
});
it("does not unlock zone when quest condition is not met", async () => {
const state = makeState({
bosses: [{ id: "forest_giant", status: "defeated" }] as GameState["bosses"],
quests: [{ id: "ancient_ruins", status: "active" }] as GameState["quests"],
zones: [{ id: "shattered_ruins", status: "locked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { zonesUnlocked: number };
expect(body.zonesUnlocked).toBe(0);
});
it("unlocks zone when both boss and quest conditions are met", async () => {
const state = makeState({
bosses: [{ id: "forest_giant", status: "defeated" }] as GameState["bosses"],
quests: [{ id: "ancient_ruins", status: "completed" }] as GameState["quests"],
zones: [{ id: "shattered_ruins", status: "locked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { zonesUnlocked: number };
expect(body.zonesUnlocked).toBe(1);
});
it("unlocks a quest when zone is unlocked and prerequisites are met", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "locked" }] as GameState["quests"],
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { questsUnlocked: number };
expect(body.questsUnlocked).toBe(1);
});
it("does not unlock quest when zone is locked", async () => {
/*
* Use shattered_ruins (requires forest_giant defeated) so applyZoneUnlocks
* cannot auto-unlock it, keeping it locked when applyQuestUnlocks runs.
*/
const state = makeState({
bosses: [{ id: "forest_giant", status: "available" }] as GameState["bosses"],
quests: [{ id: "necromancer_tower", status: "locked" }] as GameState["quests"],
zones: [{ id: "shattered_ruins", status: "locked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { questsUnlocked: number };
expect(body.questsUnlocked).toBe(0);
});
it("does not unlock quest when zone is not in state", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "locked" }] as GameState["quests"],
zones: [] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { questsUnlocked: number };
expect(body.questsUnlocked).toBe(0);
});
it("does not unlock quest when it is already available", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "available" }] as GameState["quests"],
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { questsUnlocked: number };
expect(body.questsUnlocked).toBe(0);
});
it("does not unlock quest when prerequisites are not completed", async () => {
const state = makeState({
quests: [{ id: "goblin_camp", status: "locked" }] as GameState["quests"],
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { questsUnlocked: number };
expect(body.questsUnlocked).toBe(0);
});
it("unlocks the first boss in a zone when the zone is unlocked", async () => {
const state = makeState({
bosses: [{ id: "troll_king", status: "locked" }] as GameState["bosses"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { bossesUnlocked: number };
expect(body.bossesUnlocked).toBe(1);
});
it("does not unlock boss when prestige requirement is not met", async () => {
const state = makeState({
bosses: [{ id: "the_first_light", status: "locked" }] as GameState["bosses"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
zones: [{ id: "celestial_reaches", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { bossesUnlocked: number };
expect(body.bossesUnlocked).toBe(0);
});
it("does not unlock boss when previous boss is not defeated", async () => {
const state = makeState({
bosses: [
{ id: "troll_king", status: "available" },
{ id: "lich_queen", status: "locked" },
] as GameState["bosses"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { bossesUnlocked: number };
expect(body.bossesUnlocked).toBe(0);
});
it("does not unlock boss when previous boss is not in state", async () => {
const state = makeState({
bosses: [{ id: "lich_queen", status: "locked" }] as GameState["bosses"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { bossesUnlocked: number };
expect(body.bossesUnlocked).toBe(0);
});
it("unlocks next boss when previous boss is defeated", async () => {
const state = makeState({
bosses: [
{ id: "troll_king", status: "defeated" },
{ id: "lich_queen", status: "locked" },
] as GameState["bosses"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { bossesUnlocked: number };
expect(body.bossesUnlocked).toBe(1);
});
it("returns explorationUnlocked=0 when exploration is undefined", async () => {
const state = makeState({
exploration: undefined as unknown as GameState["exploration"],
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { explorationUnlocked: number };
expect(body.explorationUnlocked).toBe(0);
});
it("unlocks exploration area when its zone is unlocked", async () => {
const state = makeState({
exploration: makeExploration([
{ id: "verdant_meadow", status: "locked" } as GameState["exploration"]["areas"][0],
]),
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { explorationUnlocked: number };
expect(body.explorationUnlocked).toBe(1);
});
it("does not unlock exploration area when zone is not unlocked", async () => {
const state = makeState({
exploration: makeExploration([
{ id: "vm_e1", status: "locked" } as GameState["exploration"]["areas"][0],
]),
zones: [] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { explorationUnlocked: number };
expect(body.explorationUnlocked).toBe(0);
});
it("does not unlock exploration area when it is already available", async () => {
const state = makeState({
exploration: makeExploration([
{ id: "verdant_meadow", status: "available" } as GameState["exploration"]["areas"][0],
]),
zones: [{ id: "verdant_vale", status: "unlocked" }] as GameState["zones"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { explorationUnlocked: number };
expect(body.explorationUnlocked).toBe(0);
});
it("unlocks adventurer tier when its quest has been completed", async () => {
const state = makeState({
adventurers: [ { id: "scout", unlocked: false } ] as GameState["adventurers"],
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { adventurersUnlocked: number };
expect(body.adventurersUnlocked).toBe(1);
});
it("does not unlock adventurer tier when it is already unlocked", async () => {
const state = makeState({
adventurers: [ { id: "scout", unlocked: true } ] as GameState["adventurers"],
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { adventurersUnlocked: number };
expect(body.adventurersUnlocked).toBe(0);
});
it("unlocks upgrade when its boss has been defeated", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { upgradesUnlocked: number };
expect(body.upgradesUnlocked).toBe(1);
});
it("does not unlock upgrade when boss is not defeated", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { upgradesUnlocked: number };
expect(body.upgradesUnlocked).toBe(0);
});
it("does not unlock upgrade when it is already unlocked", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
upgrades: [ { id: "click_2", unlocked: true } ] as GameState["upgrades"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { upgradesUnlocked: number };
expect(body.upgradesUnlocked).toBe(0);
});
it("unlocks upgrade granted as a quest reward", async () => {
const state = makeState({
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
upgrades: [ { id: "global_1", unlocked: false } ] as GameState["upgrades"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { upgradesUnlocked: number };
expect(body.upgradesUnlocked).toBe(1);
});
it("marks equipment as owned when its boss has been defeated", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { equipmentUnlocked: number };
expect(body.equipmentUnlocked).toBe(1);
});
it("does not mark equipment as owned when boss is not defeated", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { equipmentUnlocked: number };
expect(body.equipmentUnlocked).toBe(0);
});
it("does not mark equipment as owned when it is already owned", async () => {
const state = makeState({
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
equipment: [ { id: "iron_sword", owned: true } ] as GameState["equipment"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { equipmentUnlocked: number };
expect(body.equipmentUnlocked).toBe(0);
});
it("returns storyUnlocked=0 when story is undefined", async () => {
const state = makeState({
story: undefined as unknown as GameState["story"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { storyUnlocked: number };
expect(body.storyUnlocked).toBe(0);
});
it("unlocks story chapter when its boss has been defeated", async () => {
const state = makeState({
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
story: { completedChapters: [], unlockedChapterIds: [] },
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { storyUnlocked: number };
expect(body.storyUnlocked).toBe(1);
});
it("does not unlock story chapter when boss is not defeated", async () => {
const state = makeState({
bosses: [ { id: "forest_giant", status: "available" } ] as GameState["bosses"],
story: { completedChapters: [], unlockedChapterIds: [] },
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { storyUnlocked: number };
expect(body.storyUnlocked).toBe(0);
});
it("does not unlock story chapter when it is already unlocked", async () => {
const state = makeState({
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
story: { completedChapters: [], unlockedChapterIds: [ "story_ch_01" ] },
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
const body = await res.json() as { storyUnlocked: number };
expect(body.storyUnlocked).toBe(0);
});
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("omits signature when ANTI_CHEAT_SECRET is not set", async () => {
delete process.env.ANTI_CHEAT_SECRET;
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await forceUnlocks();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeUndefined();
});
it("returns 500 when DB throws an Error", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await forceUnlocks();
expect(res.status).toBe(500);
});
it("returns 500 when DB throws a non-Error value", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw error");
const res = await forceUnlocks();
expect(res.status).toBe(500);
});
});
const syncNewContent = () =>
app.fetch(new Request("http://localhost/debug/sync-new-content", { method: "POST" }));
describe("POST /sync-new-content", () => {
it("returns 404 when no game state found", async () => {
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
const res = await syncNewContent();
expect(res.status).toBe(404);
});
it("returns 200 with zero counts when state already has all content", async () => {
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
});
it("injects missing entries when arrays are empty", async () => {
const state = makeState({ adventurers: [], bosses: [], quests: [], upgrades: [], achievements: [], equipment: [], zones: [] });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { adventurersAdded: number; bossesAdded: number };
expect(body.adventurersAdded).toBeGreaterThan(0);
expect(body.bossesAdded).toBeGreaterThan(0);
});
it("injects missing exploration areas when exploration has no areas", async () => {
const state = makeState({ exploration: makeExploration([]) });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { explorationAreasAdded: number };
expect(body.explorationAreasAdded).toBeGreaterThan(0);
});
it("skips existing exploration areas when building the id set", async () => {
const state = makeState({ exploration: makeExploration([
{ id: "verdant_meadow", status: "available", completedOnce: false } as GameState["exploration"]["areas"][0],
]) });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { explorationAreasAdded: number };
// One area already existed so total injected is one less than full count
expect(body.explorationAreasAdded).toBeGreaterThan(0);
});
it("returns explorationAreasAdded=0 when exploration state is undefined", async () => {
const state = makeState({ exploration: undefined as unknown as GameState["exploration"] });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { explorationAreasAdded: number };
expect(body.explorationAreasAdded).toBe(0);
});
it("patches quest rewards when saved quest has fewer rewards than default", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "available", rewards: [] }] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
const quest = body.state.quests.find((q) => q.id === "first_steps");
expect(quest?.rewards.length).toBeGreaterThan(0);
});
it("skips quest reward patching for quests not in defaults", async () => {
const state = makeState({
quests: [{ id: "nonexistent_quest", status: "available", rewards: [] }] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
const quest = body.state.quests.find((q) => q.id === "nonexistent_quest");
expect(quest?.rewards).toStrictEqual([]);
});
it("does not re-add rewards that are already present in the saved quest", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "available", rewards: [{ type: "adventurer", targetId: "militia" }] }] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
const quest = body.state.quests.find((q) => q.id === "first_steps");
// Reward already present so count stays the same
expect(quest?.rewards.filter((r) => r.targetId === "militia").length).toBe(1);
});
it("patches boss upgrade rewards when saved boss has fewer rewards than default", async () => {
const state = makeState({
bosses: [{ id: "troll_king", status: "available", upgradeRewards: [] }] as GameState["bosses"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
const boss = body.state.bosses.find((b) => b.id === "troll_king");
expect(boss?.upgradeRewards.length).toBeGreaterThan(0);
});
it("skips boss reward patching for bosses not in defaults", async () => {
const state = makeState({
bosses: [{ id: "nonexistent_boss", status: "available", upgradeRewards: [] }] as GameState["bosses"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
const boss = body.state.bosses.find((b) => b.id === "nonexistent_boss");
expect(boss?.upgradeRewards).toStrictEqual([]);
});
it("sorts multiple legacy items to the end when their ids are not in the defaults", async () => {
const state = makeState({
achievements: [
{ id: "legacy_achievement_a", status: "locked" },
{ id: "legacy_achievement_b", status: "locked" },
] as GameState["achievements"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
});
it("uses amount field when building the reward key for quests with amount-based rewards", async () => {
const state = makeState({
quests: [{ id: "dragon_lair", status: "available", rewards: [{ type: "gold", amount: 500 }] }] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
});
it("falls back to empty string when reward has neither targetId nor amount", async () => {
const state = makeState({
quests: [{ id: "first_steps", status: "available", rewards: [{ type: "unknown" }] }] as GameState["quests"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
});
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("returns 500 when DB throws an Error", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await syncNewContent();
expect(res.status).toBe(500);
});
it("returns 500 when DB throws a non-Error value", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw error");
const res = await syncNewContent();
expect(res.status).toBe(500);
});
});
describe("POST /hard-reset", () => {
it("returns 404 when no player found", async () => {
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null);
const res = await hardReset();
expect(res.status).toBe(404);
});
it("returns 200 with a fresh state on success", async () => {
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
const res = await hardReset();
expect(res.status).toBe(200);
const body = await res.json() as {
loginBonus: null;
loginStreak: number;
schemaOutdated: boolean;
};
expect(body.loginBonus).toBeNull();
expect(body.schemaOutdated).toBe(false);
expect(body.loginStreak).toBe(1);
});
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
const res = await hardReset();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("returns 500 when DB throws an Error", async () => {
vi.mocked(prisma.player.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await hardReset();
expect(res.status).toBe(500);
});
it("returns 500 when DB throws a non-Error value", async () => {
vi.mocked(prisma.player.findUnique).mockRejectedValueOnce("raw error");
const res = await hardReset();
expect(res.status).toBe(500);
});
});
});