// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine. import { content } from "@deadhead/content"; import { errorResponseSchema, pilotResponseSchema } from "@deadhead/schema"; import { describe, expect, it } from "vitest"; import { TEST_NOW, useTestApp, signUp, call, getState, edit, setCredits, insertDispatch, makeOwner, addShip, type Guest, } from "../support"; const randomUUID = () => globalThis.crypto.randomUUID(); const testApp = useTestApp(); const errorCode = (response: { json(): unknown }) => errorResponseSchema.parse(response.json()).error.code; /** An owner with a Moon bay, plenty of credits, and an extra uncrewed Courier at the Moon. */ async function ownerWithMoonBay() { const { app } = testApp(); const guest = await signUp(app); const { shipId: earthShipId } = await makeOwner(testApp().app, guest.avatarId); await setCredits(testApp().app, 100_000); await edit(app, (save) => { save.bays.push({ id: randomUUID(), locationId: "moon", createdAt: TEST_NOW }); }); const moonShipId = randomUUID(); await addShip(app, { id: moonShipId, modelId: "courier", name: "Moon Hopper", locationId: "moon", }); return { guest, earthShipId, moonShipId }; } async function hire(guest: Guest, locationId: string) { return call(testApp().app, guest, "POST", "/api/pilots", { locationId }); } describe("hiring pilots", () => { it("hires a level-0 pilot at the Moon bay and deducts the fee", async () => { const { app } = testApp(); const { guest } = await ownerWithMoonBay(); const response = await hire(guest, "moon"); expect(response.statusCode).toBe(200); const { pilot } = pilotResponseSchema.parse(response.json()); expect(pilot).toMatchObject({ isAvatar: false, level: 0, xp: 0, locationId: "moon", assignedShipId: null, status: "idle", }); expect(pilot.name.length).toBeGreaterThan(0); expect((await getState(app, guest)).company.credits).toBe( 100_000 - content.config.pilotHireCost, ); }); it("refuses to hire where the player has no bay", async () => { const { guest } = await ownerWithMoonBay(); const response = await hire(guest, "mars"); expect(response.statusCode).toBe(409); expect(errorCode(response)).toBe("no-bay-at-location"); }); it("refuses to hire without enough credits", async () => { const { guest } = await ownerWithMoonBay(); await setCredits(testApp().app, content.config.pilotHireCost - 1); expect(errorCode(await hire(guest, "moon"))).toBe("insufficient-funds"); }); }); describe("assigning pilots", () => { it("assigns a pilot to an idle, uncrewed ship at the same location", async () => { const { app } = testApp(); const { guest, moonShipId } = await ownerWithMoonBay(); const { pilot } = pilotResponseSchema.parse((await hire(guest, "moon")).json()); const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, { shipId: moonShipId, }); expect(response.statusCode).toBe(200); expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({ assignedShipId: moonShipId, locationId: "moon", }); const state = await getState(app, guest); expect(state.ships.find((s) => s.id === moonShipId)!.pilotId).toBe(pilot.id); }); it("refuses a ship at another location", async () => { const { app } = testApp(); const { guest, moonShipId } = await ownerWithMoonBay(); const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json()); const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, { shipId: moonShipId, }); expect(errorCode(response)).toBe("ship-not-idle-here"); }); it("refuses a crewed ship", async () => { const { app } = testApp(); const { guest, earthShipId } = await ownerWithMoonBay(); const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json()); const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, { shipId: earthShipId, }); expect(errorCode(response)).toBe("ship-crewed"); }); it("refuses while the ship is in flight", async () => { const { app, clock } = testApp(); const { guest, earthShipId } = await ownerWithMoonBay(); const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json()); await insertDispatch(testApp().app, { shipId: earthShipId, pilotId: guest.avatarId, departAt: clock.now, }); const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, { shipId: earthShipId, }); expect(errorCode(response)).toBe("ship-not-idle"); }); it("refuses to assign the avatar during the tutorial", async () => { const { app } = testApp(); const guest = await signUp(app); // Ships cannot be bought during the tutorial, so put one at Earth directly. const shipId = randomUUID(); await addShip(app, { id: shipId, modelId: "courier", name: "Gift Courier", locationId: "earth", }); const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/assign`, { shipId, }); expect(errorCode(response)).toBe("avatar-in-tutorial"); }); }); describe("unassigning pilots", () => { it("unassigns at a bay, leaving the pilot at the ship's location", async () => { const { app } = testApp(); const { guest, earthShipId } = await ownerWithMoonBay(); const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/unassign`); expect(response.statusCode).toBe(200); expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({ assignedShipId: null, locationId: "earth", }); const state = await getState(app, guest); expect(state.ships.find((s) => s.id === earthShipId)!.pilotId).toBeNull(); }); it("refuses where the player has no bay", async () => { const { app } = testApp(); const { guest, earthShipId } = await ownerWithMoonBay(); // Park the crewed ship at Mars, where there is no bay. await edit(app, (save) => { save.ships.find((s) => s.id === earthShipId)!.locationId = "mars"; }); const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/unassign`); expect(response.statusCode).toBe(409); expect(errorCode(response)).toBe("no-bay-at-location"); }); });