121 lines
4.5 KiB
TypeScript
121 lines
4.5 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("is accepted with the loaner at the Moon, and the avatar ends up at Earth (CR-017)", async () => {
|
|
const guest = await signUp(testApp().app);
|
|
await readyForDeal(guest);
|
|
await edit(testApp().app, (save) => {
|
|
save.player.loanerLocationId = "moon";
|
|
});
|
|
const response = await accept(guest);
|
|
expect(response.statusCode).toBe(200);
|
|
const state = playerStateSchema.parse(response.json());
|
|
const avatar = state.pilots.find((p) => p.isAvatar)!;
|
|
const courier = state.ships.find((s) => s.id === avatar.assignedShipId)!;
|
|
expect(avatar.locationId).toBe("earth");
|
|
expect(courier).toMatchObject({ modelId: "courier", locationId: "earth" });
|
|
expect(state.tutorial).toBeNull();
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|