deadhead-space-freight-tycoon/packages/engine/test/ported/deal.test.ts

114 lines
4.1 KiB
TypeScript

// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine.
import { content } from "@deadhead/content";
import { errorResponseSchema, playerStateSchema } from "@deadhead/schema";
import { describe, expect, it } from "vitest";
import { useTestApp, signUp, call, getState, edit, readSave, type Guest } from "../support";
const testApp = useTestApp();
/** Fast-forwards a new guest to "four runs done, loaner back at Earth". */
async function readyForDeal(_guest: Guest) {
await edit(testApp().app, (save) => {
save.player.tutorialRunsCompleted = content.config.tutorialRunsBeforeDeal;
save.player.loanerLocationId = "earth";
});
}
const accept = (guest: Guest) => call(testApp().app, guest, "POST", "/api/deal/accept");
/** Everything the deal touches, read straight from the save. */
async function snapshot(_guest: Guest) {
const save = await readSave(testApp().app);
return {
player: save.player,
company: save.company,
ships: save.ships,
bays: save.bays,
pilots: save.pilots,
};
}
describe("accepting the deal", () => {
it("hands over a crewed Courier and Clipper at Earth, an Earth bay and the loan, and ends the tutorial", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
expect((await getState(testApp().app, guest)).tutorial?.dealAvailable).toBe(true);
const response = await accept(guest);
expect(response.statusCode).toBe(200);
const state = playerStateSchema.parse(response.json());
expect(state.tutorial).toBeNull();
// CR-013: the Courier for the avatar and the Clipper for the veteran.
expect(state.ships).toHaveLength(2);
const [ship, clipper] = state.ships;
expect(ship).toMatchObject({
modelId: "courier",
status: "idle",
locationId: "earth",
pilotId: guest.avatarId,
});
const veteran = state.pilots.find((p) => p.isVeteran)!;
expect(clipper).toMatchObject({
modelId: "clipper",
status: "idle",
locationId: "earth",
pilotId: veteran.id,
});
expect(veteran).toMatchObject({ assignedShipId: clipper!.id, locationId: "earth" });
expect(state.bays.map((b) => b.locationId)).toEqual(["earth"]);
expect(state.pilots[0]).toMatchObject({
isAvatar: true,
assignedShipId: ship!.id,
locationId: "earth",
});
expect(state.company.loanBalance).toBe(content.dealLoanAmount);
const after = await snapshot(guest);
expect(after.player).toMatchObject({
tutorialActive: false,
dealAccepted: true,
loanerLocationId: null,
});
});
it("fails with no changes when the deal is not available", async () => {
const guest = await signUp(testApp().app);
const before = await snapshot(guest);
const response = await accept(guest);
expect(response.statusCode).toBe(409);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("deal-not-available");
expect(await snapshot(guest)).toEqual(before);
});
it("fails while the loaner is at the Moon", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
await edit(testApp().app, (save) => {
save.player.loanerLocationId = "moon";
});
expect((await accept(guest)).statusCode).toBe(409);
});
it("fails the second time", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
expect((await accept(guest)).statusCode).toBe(200);
const before = await snapshot(guest);
const again = await accept(guest);
expect(errorResponseSchema.parse(again.json()).error.code).toBe("deal-not-available");
expect(await snapshot(guest)).toEqual(before);
});
it("produces exactly one deal from two simultaneous accepts", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
const responses = await Promise.all([accept(guest), accept(guest)]);
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
const after = await snapshot(guest);
expect(after.ships).toHaveLength(2);
expect(after.bays).toHaveLength(1);
expect(after.company!.loanBalance).toBe(content.dealLoanAmount);
});
});