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

149 lines
5.0 KiB
TypeScript

// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine.
import { content } from "@deadhead/content";
import { errorResponseSchema, shipResponseSchema } from "@deadhead/schema";
import { describe, expect, it } from "vitest";
import { shipName } from "../../src/game/names";
import {
TEST_NOW,
useTestApp,
signUp,
call,
getState,
edit,
setCredits,
signUpOwner,
} from "../support";
const randomUUID = () => globalThis.crypto.randomUUID();
const testApp = useTestApp();
const errorCode = (response: { json(): unknown }) =>
errorResponseSchema.parse(response.json()).error.code;
describe("buy a ship", () => {
it("deducts the price and adds an idle, uncrewed ship at the Earth bay", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
const price = content.shipModel("courier")!.price;
await setCredits(testApp().app, price + 500);
const response = await call(app, owner, "POST", "/api/ships", {
modelId: "courier",
bayId: owner.earthBayId,
});
expect(response.statusCode).toBe(200);
const { ship } = shipResponseSchema.parse(response.json());
expect(ship).toMatchObject({
modelId: "courier",
status: "idle",
locationId: "earth",
pilotId: null,
dispatchId: null,
});
expect(ship.name.length).toBeGreaterThan(0);
const state = await getState(app, owner);
expect(state.company.credits).toBe(500);
expect(state.ships.map((s) => s.id)).toEqual([owner.shipId, ship.id]);
});
it("returns insufficient-funds and changes nothing without enough credits", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
const price = content.shipModel("freighter")!.price;
await setCredits(testApp().app, price - 1);
const response = await call(app, owner, "POST", "/api/ships", {
modelId: "freighter",
bayId: owner.earthBayId,
});
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("insufficient-funds");
const state = await getState(app, owner);
expect(state.company.credits).toBe(price - 1);
expect(state.ships).toHaveLength(1);
});
it("returns tutorial-active during the tutorial and changes nothing", async () => {
const { app } = testApp();
const guest = await signUp(app);
await setCredits(testApp().app, 1_000_000);
const response = await call(app, guest, "POST", "/api/ships", {
modelId: "courier",
bayId: "any",
});
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("tutorial-active");
const state = await getState(app, guest);
expect(state.company.credits).toBe(1_000_000);
expect(state.ships).toHaveLength(0);
});
it("refuses to sell the company loaner", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(testApp().app, 1_000_000);
const response = await call(app, owner, "POST", "/api/ships", {
modelId: "company-courier",
bayId: owner.earthBayId,
});
expect(response.statusCode).toBe(404);
});
it("rejects an unknown model", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
const response = await call(app, owner, "POST", "/api/ships", {
modelId: "x-wing",
bayId: owner.earthBayId,
});
expect(response.statusCode).toBe(404);
expect(errorCode(response)).toBe("not-found");
});
});
describe("ship delivery to a bay (CR-002)", () => {
it("delivers a Freighter bought for the Moon bay to the Moon", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
const moonBayId = randomUUID();
await edit(app, (save) => {
save.bays.push({ id: moonBayId, locationId: "moon", createdAt: TEST_NOW });
});
await setCredits(testApp().app, content.shipModel("freighter")!.price);
const response = await call(app, owner, "POST", "/api/ships", {
modelId: "freighter",
bayId: moonBayId,
});
expect(response.statusCode).toBe(200);
expect(shipResponseSchema.parse(response.json()).ship).toMatchObject({
locationId: "moon",
status: "idle",
pilotId: null,
});
});
it("rejects a purchase with no bay id as a validation error", async () => {
const { app } = testApp();
const owner = await signUpOwner(app);
await setCredits(testApp().app, 1_000_000);
const response = await call(app, owner, "POST", "/api/ships", { modelId: "courier" });
expect(response.statusCode).toBe(400);
expect(errorCode(response)).toBe("validation-error");
expect((await getState(app, owner)).company.credits).toBe(1_000_000);
});
// "Refuses another player's bay" no longer applies: one company per save (CR-014).
});
describe("ship names", () => {
it("are stable for a world seed and differ between ships", () => {
expect(shipName(42, 0)).toBe(shipName(42, 0));
const names = new Set(Array.from({ length: 10 }, (_, i) => shipName(42, i)));
expect(names.size).toBeGreaterThan(1);
});
});