180 lines
6.3 KiB
TypeScript
180 lines
6.3 KiB
TypeScript
// 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");
|
|
});
|
|
});
|