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

121 lines
4.6 KiB
TypeScript

import { content } from "@deadhead/content";
import { errorResponseSchema, playerStateSchema } from "@deadhead/schema";
import { eq } from "drizzle-orm";
import { describe, expect, it } from "vitest";
import { bays, companies, pilots, players, ships } from "../src/db/schema";
import { useTestDatabase } from "./db";
import { call, getState, signUp, useTestApp, type Guest } from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
/** Fast-forwards a new guest to "four runs done, loaner back at Earth". */
async function readyForDeal(guest: Guest) {
await database()
.db.update(players)
.set({
tutorialRunsCompleted: content.config.tutorialRunsBeforeDeal,
loanerLocationId: "earth",
})
.where(eq(players.id, guest.playerId));
}
const accept = (guest: Guest) => call(testApp().app, guest, "POST", "/api/deal/accept");
/** Everything the deal touches, read straight from the database. */
async function snapshot(guest: Guest) {
const { db } = database();
return {
player: (await db.select().from(players).where(eq(players.id, guest.playerId)))[0],
company: (await db.select().from(companies).where(eq(companies.playerId, guest.playerId)))[0],
ships: await db.select().from(ships).where(eq(ships.playerId, guest.playerId)),
bays: await db.select().from(bays).where(eq(bays.playerId, guest.playerId)),
pilots: await db.select().from(pilots).where(eq(pilots.playerId, guest.playerId)),
};
}
describe("accepting the deal", () => {
it("hands over a crewed Courier and Clipper at Earth, an Earth bay and the loan, and ends the tutorial", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
expect((await getState(testApp().app, guest)).tutorial?.dealAvailable).toBe(true);
const response = await accept(guest);
expect(response.statusCode).toBe(200);
const state = playerStateSchema.parse(response.json());
expect(state.tutorial).toBeNull();
// CR-013: the Courier for the avatar and the Clipper for the veteran.
expect(state.ships).toHaveLength(2);
const [ship, clipper] = state.ships;
expect(ship).toMatchObject({
modelId: "courier",
status: "idle",
locationId: "earth",
pilotId: guest.avatarId,
});
const veteran = state.pilots.find((p) => p.isVeteran)!;
expect(clipper).toMatchObject({
modelId: "clipper",
status: "idle",
locationId: "earth",
pilotId: veteran.id,
});
expect(veteran).toMatchObject({ assignedShipId: clipper!.id, locationId: "earth" });
expect(state.bays.map((b) => b.locationId)).toEqual(["earth"]);
expect(state.pilots[0]).toMatchObject({
isAvatar: true,
assignedShipId: ship!.id,
locationId: "earth",
});
expect(state.company.loanBalance).toBe(content.dealLoanAmount);
const after = await snapshot(guest);
expect(after.player).toMatchObject({
tutorialActive: false,
dealAccepted: true,
loanerLocationId: null,
});
});
it("fails with no changes when the deal is not available", async () => {
const guest = await signUp(testApp().app);
const before = await snapshot(guest);
const response = await accept(guest);
expect(response.statusCode).toBe(409);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("deal-not-available");
expect(await snapshot(guest)).toEqual(before);
});
it("fails while the loaner is at the Moon", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
await database()
.db.update(players)
.set({ loanerLocationId: "moon" })
.where(eq(players.id, guest.playerId));
expect((await accept(guest)).statusCode).toBe(409);
});
it("fails the second time", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
expect((await accept(guest)).statusCode).toBe(200);
const before = await snapshot(guest);
const again = await accept(guest);
expect(errorResponseSchema.parse(again.json()).error.code).toBe("deal-not-available");
expect(await snapshot(guest)).toEqual(before);
});
it("produces exactly one deal from two simultaneous accepts", async () => {
const guest = await signUp(testApp().app);
await readyForDeal(guest);
const responses = await Promise.all([accept(guest), accept(guest)]);
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
const after = await snapshot(guest);
expect(after.ships).toHaveLength(2);
expect(after.bays).toHaveLength(1);
expect(after.company!.loanBalance).toBe(content.dealLoanAmount);
});
});