deadhead-space-freight-tycoon/apps/server/test/goals.test.ts

108 lines
4.0 KiB
TypeScript

import { content } from "@deadhead/content";
import { companyResponseSchema, errorResponseSchema } from "@deadhead/schema";
import { eq } from "drizzle-orm";
import { describe, expect, it } from "vitest";
import { pilots, players } from "../src/db/schema";
import { setCredits, useTestDatabase } from "./db";
import {
call,
getState,
insertDispatch,
signUp,
signUpOwner,
useTestApp,
type Guest,
} from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
const claim = (guest: Guest, goalId: string) =>
call(testApp().app, guest, "POST", `/api/goals/${goalId}/claim`);
const errorCode = (response: { json(): unknown }) =>
errorResponseSchema.parse(response.json()).error.code;
/** An owner who has accepted the deal (so "pay off your loan" can apply). */
async function dealOwner() {
const owner = await signUpOwner(testApp().app, database().db);
await database()
.db.update(players)
.set({ dealAccepted: true })
.where(eq(players.id, owner.playerId));
return owner;
}
const status = async (guest: Guest, goalId: string) =>
(await getState(testApp().app, guest)).goals.find((g) => g.id === goalId)?.status;
describe("onboarding goals", () => {
it("are absent from state during the tutorial", async () => {
const guest = await signUp(testApp().app);
expect((await getState(testApp().app, guest)).goals).toEqual([]);
});
it("are listed in content order once the tutorial is over", async () => {
const owner = await dealOwner();
const goals = (await getState(testApp().app, owner)).goals;
expect(goals.map((g) => g.id)).toEqual(content.onboardingGoals.map((g) => g.id));
// Paid-off loan: the test owner has no balance.
expect(goals.find((g) => g.id === "loan-paid-off")?.status).toBe("ready");
expect(goals.find((g) => g.id === "veteran-contract")?.status).toBe("incomplete");
});
it("becomes ready when its condition is met: a delivery with the player's own ship", async () => {
const { clock } = testApp();
const owner = await dealOwner();
expect(await status(owner, "first-own-delivery")).toBe("incomplete");
const record = await insertDispatch(database().db, {
playerId: owner.playerId,
shipId: owner.shipId,
pilotId: owner.avatarId,
departAt: clock.now,
});
clock.now = record.arrivalAt;
expect(await status(owner, "first-own-delivery")).toBe("ready");
});
it("counts skill points spent", async () => {
const owner = await dealOwner();
await database()
.db.update(pilots)
.set({ skillPointsSpent: 1 })
.where(eq(pilots.id, owner.avatarId));
expect(await status(owner, "first-skill-point")).toBe("ready");
});
it("claiming a completed goal adds its reward once", async () => {
const owner = await dealOwner();
await setCredits(database().db, owner.playerId, 100);
const reward = content.onboardingGoal("loan-paid-off")!.reward;
const response = await claim(owner, "loan-paid-off");
expect(response.statusCode).toBe(200);
expect(companyResponseSchema.parse(response.json()).company.credits).toBe(100 + reward);
expect(await status(owner, "loan-paid-off")).toBe("claimed");
const again = await claim(owner, "loan-paid-off");
expect(again.statusCode).toBe(409);
expect(errorCode(again)).toBe("goal-already-claimed");
expect((await getState(testApp().app, owner)).company.credits).toBe(100 + reward);
});
it("refuses an incomplete goal and an unknown one", async () => {
const owner = await dealOwner();
await setCredits(database().db, owner.playerId, 100);
const response = await claim(owner, "veteran-contract");
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("goal-not-ready");
expect((await claim(owner, "win-the-game")).statusCode).toBe(404);
expect((await getState(testApp().app, owner)).company.credits).toBe(100);
});
it("refuses claims during the tutorial", async () => {
const guest = await signUp(testApp().app);
expect(errorCode(await claim(guest, "loan-paid-off"))).toBe("goal-not-ready");
});
});