generated from nhcarrigan/template
d48b53eecd
- Add full test suite for frontend.ts (POST /log and POST /error) - Add error-path tests to all route handlers to cover catch blocks triggered by Prisma rejections - Add non-Error throw tests to cover the `new Error(String(error))` ternary false branch in middleware, services, and route catch handlers - Suppress unreachable outer catch in about.ts with v8 ignore (fetchReleases swallows all errors internally, making the outer catch genuinely dead code)
178 lines
7.8 KiB
TypeScript
178 lines
7.8 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: {
|
|
player: { update: vi.fn() },
|
|
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<void>) => {
|
|
c.set("discordId", "test_discord_id");
|
|
await next();
|
|
}),
|
|
}));
|
|
|
|
vi.mock("../../src/services/webhook.js", () => ({
|
|
postMilestoneWebhook: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
const DISCORD_ID = "test_discord_id";
|
|
|
|
const makeState = (overrides: Partial<GameState> = {}): 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: [{ id: "the_absolute_one", status: "defeated" }] as GameState["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);
|
|
|
|
describe("transcendence route", () => {
|
|
let app: Hono;
|
|
let prisma: {
|
|
player: { update: ReturnType<typeof vi.fn> };
|
|
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
const { transcendenceRouter } = await import("../../src/routes/transcendence.js");
|
|
const { prisma: p } = await import("../../src/db/client.js");
|
|
prisma = p as typeof prisma;
|
|
app = new Hono();
|
|
app.route("/transcendence", transcendenceRouter);
|
|
});
|
|
|
|
const post = (path: string, body?: Record<string, unknown>) =>
|
|
app.fetch(new Request(`http://localhost/transcendence${path}`, {
|
|
method: "POST",
|
|
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
}));
|
|
|
|
describe("POST /", () => {
|
|
it("returns 404 when no save is found", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
|
|
const res = await post("");
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("returns 400 when the absolute one is not defeated", async () => {
|
|
const state = makeState({ bosses: [] });
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
const res = await post("");
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns echoes and count on successful transcendence", async () => {
|
|
const state = makeState();
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
|
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
|
const res = await post("");
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json() as { echoes: number; newTranscendenceCount: number };
|
|
expect(body.newTranscendenceCount).toBe(1);
|
|
expect(body.echoes).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it("returns 500 when the database throws during transcendence", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
|
const res = await post("");
|
|
expect(res.status).toBe(500);
|
|
});
|
|
|
|
it("returns 500 when the database throws a non-Error value during transcendence", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error");
|
|
const res = await post("");
|
|
expect(res.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe("POST /buy-upgrade", () => {
|
|
it("returns 400 when upgradeId is missing", async () => {
|
|
const res = await post("/buy-upgrade", {});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns 404 for unknown upgrade", async () => {
|
|
const res = await post("/buy-upgrade", { upgradeId: "nonexistent_echo" });
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("returns 404 when no save is found", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("returns 400 when transcendence data is missing from state", async () => {
|
|
const state = makeState({ transcendence: undefined });
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns 400 when upgrade is already purchased", async () => {
|
|
const state = makeState({
|
|
transcendence: { count: 1, echoes: 100, purchasedUpgradeIds: ["echo_income_1"], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
|
|
});
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns 400 when not enough echoes", async () => {
|
|
const state = makeState({
|
|
transcendence: { count: 1, echoes: 0, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
|
|
});
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
// echo_income_1 costs 5 echoes but state has 0
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns updated data on successful echo upgrade purchase", async () => {
|
|
const state = makeState({
|
|
transcendence: { count: 1, echoes: 100, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
|
|
});
|
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
|
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json() as { echoesRemaining: number; purchasedUpgradeIds: string[] };
|
|
expect(body.echoesRemaining).toBe(95); // 100 - 5
|
|
expect(body.purchasedUpgradeIds).toContain("echo_income_1");
|
|
});
|
|
|
|
it("returns 500 when the database throws during buy-upgrade", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(500);
|
|
});
|
|
|
|
it("returns 500 when the database throws a non-Error value during buy-upgrade", async () => {
|
|
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw string error");
|
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
|
expect(res.status).toBe(500);
|
|
});
|
|
});
|
|
});
|