159 lines
5.8 KiB
TypeScript
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);
|
|
});
|
|
});
|