deadhead-space-freight-tycoon/apps/server/test/concurrency.test.ts

79 lines
3.5 KiB
TypeScript

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import { makeOwner, setCredits, useTestDatabase } from "./db";
import { call, getState, putBoard, signUp, signUpOwner, testContract, useTestApp } from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
describe("concurrency and money safety", () => {
it("lets only one of two simultaneous purchases through when only one is affordable", async () => {
const { app } = testApp();
const guest = await signUpOwner(app, database().db);
const price = content.shipModel("courier")!.price;
await setCredits(database().db, guest.playerId, price + price / 2);
const responses = await Promise.all([
call(app, guest, "POST", "/api/ships", { modelId: "courier", bayId: guest.earthBayId }),
call(app, guest, "POST", "/api/ships", { modelId: "courier", bayId: guest.earthBayId }),
]);
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
const state = await getState(app, guest);
// The owner already had one ship.
expect(state.ships).toHaveLength(2);
expect(state.company.credits).toBe(price / 2);
});
it("never goes negative under many simultaneous purchases", async () => {
const { app } = testApp();
const guest = await signUpOwner(app, database().db);
const price = content.shipModel("courier")!.price;
await setCredits(database().db, guest.playerId, price * 3);
const responses = await Promise.all(
Array.from({ length: 8 }, () =>
call(app, guest, "POST", "/api/ships", { modelId: "courier", bayId: guest.earthBayId }),
),
);
expect(responses.filter((r) => r.statusCode === 200)).toHaveLength(3);
const state = await getState(app, guest);
expect(state.ships).toHaveLength(1 + 3);
expect(state.company.credits).toBe(0);
});
it("accepts a contract only once when dispatched twice at the same moment", async () => {
const { app } = testApp();
const { db } = database();
const guest = await signUp(app);
const { shipId } = await makeOwner(db, guest.playerId, guest.avatarId);
await setCredits(db, guest.playerId, 10_000);
const contract = testContract();
await putBoard(db, guest.playerId, [contract]);
const body = { kind: "contract", contractId: contract.id, shipId, pilotId: guest.avatarId };
const responses = await Promise.all([
call(app, guest, "POST", "/api/dispatches", body),
call(app, guest, "POST", "/api/dispatches", body),
]);
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
const state = await getState(app, guest);
expect(state.activeDispatches).toHaveLength(1);
expect(state.company.credits).toBe(10_000 - state.activeDispatches[0]!.fuelCost);
});
it("leases one bay when the same lease is sent twice at once", async () => {
const { app } = testApp();
const guest = await signUpOwner(app, database().db);
await setCredits(database().db, guest.playerId, 100_000);
const responses = await Promise.all([
call(app, guest, "POST", "/api/bays", { locationId: "moon" }),
call(app, guest, "POST", "/api/bays", { locationId: "moon" }),
]);
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
const state = await getState(app, guest);
expect(state.bays.map((b) => b.locationId).sort()).toEqual(["earth", "moon"]);
expect(state.company.credits).toBe(100_000 - content.location("moon")!.bayLeasePrice);
});
});