deadhead-space-freight-tycoon/apps/server/test/dev-tools.test.ts

157 lines
6.3 KiB
TypeScript

import { content, upgradeMaxLevel } from "@deadhead/content";
import { companyResponseSchema, errorResponseSchema, playerStateSchema } from "@deadhead/schema";
import { eq } from "drizzle-orm";
import { describe, expect, it } from "vitest";
import { buildApp } from "../src/app";
import { createRecordingQueue } from "../src/jobs/queue";
import { useTestDatabase } from "./db";
import {
call,
getState,
insertDispatch,
signUp,
signUpOwner,
testContract,
useTestApp,
} from "./game";
import { pilots, ships } from "../src/db/schema";
import { testConfig } from "./helpers";
const database = useTestDatabase();
const testApp = useTestApp(database);
describe("developer tools (Step 10K.11)", () => {
it("advances the server clock", async () => {
const guest = await signUp(testApp().app);
const before = (await call(testApp().app, guest, "GET", "/api/time")).json().realTime as number;
const response = await call(testApp().app, guest, "POST", "/api/test/advance-clock", {
advanceMs: 600_000,
});
expect(response.statusCode).toBe(200);
const after = (await call(testApp().app, guest, "GET", "/api/time")).json().realTime as number;
expect(after - before).toBeGreaterThanOrEqual(600_000);
});
it("adds credits", async () => {
const guest = await signUp(testApp().app);
const start = (await getState(testApp().app, guest)).company.credits;
const response = await call(testApp().app, guest, "POST", "/api/test/add-credits", {
amount: 10_000,
});
expect(companyResponseSchema.parse(response.json()).company.credits).toBe(start + 10_000);
});
it("lands every flight when advanced to the last arrival", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
const flight = await insertDispatch(database().db, {
playerId: owner.playerId,
shipId: owner.shipId,
pilotId: owner.avatarId,
departAt: clock.now,
contract: testContract(),
});
expect((await getState(app, owner)).activeDispatches).toHaveLength(1);
await call(app, owner, "POST", "/api/test/advance-clock", {
advanceMs: flight.arrivalAt - clock.now + 1000,
});
expect((await getState(app, owner)).activeDispatches).toHaveLength(0);
});
it("does not exist in production", async () => {
const app = buildApp(testConfig({ NODE_ENV: "production" }), {
database: database(),
jobs: createRecordingQueue(),
});
for (const [url, payload] of [
["/api/test/clock-offset", { offsetMs: 1 }],
["/api/test/advance-clock", { advanceMs: 1000 }],
["/api/test/add-credits", { amount: 10_000 }],
["/api/test/pilot", { pilotId: "p", action: "level-up" }],
["/api/test/ship", { shipId: "s", action: "max-upgrades" }],
["/api/test/discover-all", {}],
] as const) {
const response = await app.inject({ method: "POST", url, payload });
expect(response.statusCode, url).toBe(404);
}
await app.close();
});
it("levels a pilot up to the next level's XP, and clears duty and rest", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
await database()
.db.update(pilots)
.set({ xp: 40, dutyHours: 9, restEndsAt: clock.now + 3_600_000 })
.where(eq(pilots.id, owner.avatarId));
const pilotAfter = async (action: string) => {
const response = await call(app, owner, "POST", "/api/test/pilot", {
pilotId: owner.avatarId,
action,
});
expect(response.statusCode).toBe(200);
return playerStateSchema.parse(response.json()).pilots.find((p) => p.id === owner.avatarId)!;
};
const levelled = await pilotAfter("level-up");
expect(levelled.xp).toBe(content.config.pilotLevelXpThresholds[0]);
expect(levelled.level).toBe(1);
expect(levelled.skillPointsAvailable).toBe(content.config.skillPointsPerLevel);
expect((await pilotAfter("level-up")).level).toBe(2);
const rested = await pilotAfter("clear-rest");
expect(rested).toMatchObject({ dutyHours: 0, resting: false, restEndsAt: null });
});
it("maxes and resets a ship's upgrades, and finishes shop work", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
const ship = async (input: Record<string, unknown>) => {
const response = await call(app, owner, "POST", "/api/test/ship", {
shipId: owner.shipId,
...input,
});
expect(response.statusCode).toBe(200);
return playerStateSchema.parse(response.json()).ships.find((s) => s.id === owner.shipId)!;
};
const maxed = await ship({ action: "max-upgrades" });
for (const upgrade of content.upgrades) {
expect(maxed.upgrades[upgrade.id], upgrade.id).toBe(upgradeMaxLevel(upgrade, "courier"));
}
expect(maxed.stats.fuelRangeMillionKm).toBeGreaterThan(
content.shipModel("courier")!.fuelRangeMillionKm,
);
expect((await ship({ action: "reset-upgrades" })).upgrades).toEqual({});
await database()
.db.update(ships)
.set({ inShopUntil: clock.now + 600_000, shopUpgradeId: "engines", shopLevel: 1 })
.where(eq(ships.id, owner.shipId));
expect((await ship({ action: "finish-shop" })).inShopUntil).toBeNull();
});
it("moves an idle ship to a discovered place only", async () => {
const { app } = testApp();
const owner = await signUpOwner(app, database().db);
const move = (locationId: string) =>
call(app, owner, "POST", "/api/test/ship", {
shipId: owner.shipId,
action: "move",
locationId,
});
const moved = await move("moon");
expect(moved.statusCode).toBe(200);
expect(playerStateSchema.parse(moved.json()).ships[0]!.locationId).toBe("moon");
const refused = await move("mars");
expect(refused.statusCode).toBe(409);
expect(errorResponseSchema.parse(refused.json()).error.code).toBe("location-undiscovered");
});
it("discovers every location", async () => {
const { app } = testApp();
const owner = await signUpOwner(app, database().db);
const response = await call(app, owner, "POST", "/api/test/discover-all", {});
const { locations } = playerStateSchema.parse(response.json());
expect(locations.every((l) => l.discovered)).toBe(true);
expect(locations).toHaveLength(content.locations.length);
});
});