import { randomUUID } from "node:crypto"; import { content } from "@deadhead/content"; import { errorResponseSchema, shipResponseSchema } from "@deadhead/schema"; import { describe, expect, it } from "vitest"; import { bays } from "../src/db/schema"; import { shipName } from "../src/game/names"; import { setCredits, TEST_NOW, useTestDatabase } from "./db"; import { call, getState, signUp, signUpOwner, useTestApp } from "./game"; const database = useTestDatabase(); const testApp = useTestApp(database); 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, database().db); const price = content.shipModel("courier")!.price; await setCredits(database().db, owner.playerId, 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, database().db); const price = content.shipModel("freighter")!.price; await setCredits(database().db, owner.playerId, 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(database().db, guest.playerId, 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, database().db); await setCredits(database().db, owner.playerId, 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, database().db); 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, database().db); const moonBayId = randomUUID(); await database() .db.insert(bays) .values({ id: moonBayId, playerId: owner.playerId, locationId: "moon", createdAt: TEST_NOW }); await setCredits(database().db, owner.playerId, 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, database().db); await setCredits(database().db, owner.playerId, 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); }); it("refuses another player's bay and changes nothing", async () => { const { app } = testApp(); const owner = await signUpOwner(app, database().db); const other = await signUpOwner(app, database().db); await setCredits(database().db, owner.playerId, 1_000_000); const response = await call(app, owner, "POST", "/api/ships", { modelId: "courier", bayId: other.earthBayId, }); expect(response.statusCode).toBe(404); expect(errorCode(response)).toBe("not-found"); const state = await getState(app, owner); expect(state.company.credits).toBe(1_000_000); expect(state.ships).toHaveLength(1); expect((await getState(app, other)).ships).toHaveLength(1); }); }); 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); }); });