deadhead-space-freight-tycoon/apps/server/test/buy-ship.test.ts

159 lines
5.8 KiB
TypeScript

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);
});
});