deadhead-space-freight-tycoon/apps/server/test/arrival-job.test.ts

121 lines
4.5 KiB
TypeScript

import { eq } from "drizzle-orm";
import { describe, expect, it } from "vitest";
import { buildApp } from "../src/app";
import { companies, dispatches, ships } from "../src/db/schema";
import { RESOLVE_ARRIVALS_JOB, resolveArrivalsJob } from "../src/jobs/definitions";
import { createRecordingQueue } from "../src/jobs/queue";
import { makeOwner, setCredits, useTestDatabase } from "./db";
import { call, getState, putBoard, signUp, testContract, useTestApp } from "./game";
import { testConfig } from "./helpers";
const database = useTestDatabase();
const testApp = useTestApp(database);
/** An owner who has just dispatched a 2,000-credit Earth→Moon contract. */
async function dispatched() {
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 response = await call(app, guest, "POST", "/api/dispatches", {
kind: "contract",
contractId: contract.id,
shipId,
pilotId: guest.avatarId,
});
const dispatch = response.json().dispatch as { id: string; arrivalAt: number; fuelCost: number };
return { guest, shipId, dispatch };
}
const credits = async (playerId: string) =>
(await database().db.select().from(companies).where(eq(companies.playerId, playerId)))[0]!
.credits;
describe("arrival resolution job", () => {
it("is scheduled for the dispatch's arrival time", async () => {
const { jobs } = testApp();
const { guest, dispatch } = await dispatched();
expect(jobs.sent).toEqual([
{
name: RESOLVE_ARRIVALS_JOB,
data: { playerId: guest.playerId, dispatchId: dispatch.id },
startAfter: dispatch.arrivalAt,
},
]);
});
it("resolves the dispatch at arrival without any player state fetch", async () => {
const { clock, jobs } = testApp();
const { db } = database();
const { guest, shipId, dispatch } = await dispatched();
const jobCtx = { db, clock: clock.read };
// Too early: nothing happens.
clock.now = dispatch.arrivalAt - 1;
await resolveArrivalsJob(jobCtx, jobs.sent[0]!.data);
expect((await db.select().from(dispatches))[0]!.status).toBe("in_flight");
clock.now = dispatch.arrivalAt;
await resolveArrivalsJob(jobCtx, jobs.sent[0]!.data);
const [row] = await db.select().from(dispatches).where(eq(dispatches.id, dispatch.id));
expect(row!.status).toBe("resolved");
expect(await credits(guest.playerId)).toBe(10_000 - dispatch.fuelCost + 2000);
const [ship] = await db.select().from(ships).where(eq(ships.id, shipId));
expect(ship).toMatchObject({ status: "idle", locationId: "moon" });
});
it("does nothing if lazy resolution already happened", async () => {
const { app, clock, jobs } = testApp();
const { db } = database();
const { guest, dispatch } = await dispatched();
clock.now = dispatch.arrivalAt + 1000;
const state = await getState(app, guest);
await resolveArrivalsJob({ db, clock: clock.read }, jobs.sent[0]!.data);
expect(await credits(guest.playerId)).toBe(state.company.credits);
expect((await getState(app, guest)).summaries).toHaveLength(1);
});
});
describe("test-only clock offset endpoint", () => {
it("shifts the server clock outside production", async () => {
const { app, clock } = testApp();
const response = await app.inject({
method: "POST",
url: "/api/test/clock-offset",
payload: { offsetMs: 3_600_000 },
});
expect(response.statusCode).toBe(200);
const time = (await app.inject({ method: "GET", url: "/api/time" })).json();
expect(time.realTime).toBe(clock.now + 3_600_000);
});
it("schedules arrival jobs on the real clock while an offset is set", async () => {
const { app, jobs } = testApp();
await app.inject({
method: "POST",
url: "/api/test/clock-offset",
payload: { offsetMs: 60_000 },
});
const { dispatch } = await dispatched();
expect(jobs.sent[0]!.startAfter).toBe(dispatch.arrivalAt - 60_000);
});
it("does not exist in production", async () => {
const app = buildApp(testConfig({ NODE_ENV: "production" }), {
database: database(),
jobs: createRecordingQueue(),
});
const response = await app.inject({
method: "POST",
url: "/api/test/clock-offset",
payload: { offsetMs: 1 },
});
expect(response.statusCode).toBe(404);
await app.close();
});
});