generated from nhcarrigan/template
635c630e49
The force-unlock debug route now scans completed quests for adventurer rewards and ensures those tiers are marked as unlocked in game state. The UI and API response type both surface the new `adventurersUnlocked` count alongside the existing zone/quest/boss/exploration counts. Closes #88 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #93 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
606 lines
27 KiB
TypeScript
606 lines
27 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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|