121 lines
4.5 KiB
TypeScript
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();
|
|
});
|
|
});
|