deadhead-space-freight-tycoon/packages/engine/test/ported/validation.test.ts

62 lines
2.3 KiB
TypeScript

// Ported from apps/server/test (CR-014 Step 14.5): request and response checks
// from the route catalog, now on engine commands. (Malformed JSON bodies and
// HTTP 404s for unknown URLs no longer exist.)
import { errorResponseSchema, type RouteResponse } from "@deadhead/schema";
import { describe, expect, it } from "vitest";
import { GameEngine, MemoryBackend, SaveStorage } from "../../src/index";
import { useTestApp, signUp, call } from "../support";
const testApp = useTestApp();
const expectValidationError = (response: { statusCode: number; json(): unknown }) => {
expect(response.statusCode).toBe(400);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("validation-error");
};
describe("request validation from the command catalog", () => {
it("rejects unexpected fields on a command that takes none", async () => {
expectValidationError(
await call(testApp().app, null, "POST", "/api/guest", { surprise: true }),
);
});
it("rejects unexpected parameters before anything else", async () => {
expectValidationError(await call(testApp().app, null, "GET", "/api/state?evil=1"));
});
it("still answers valid requests", async () => {
expect((await call(testApp().app, null, "GET", "/api/time")).statusCode).toBe(200);
});
it("refuses an unknown command with not-found", async () => {
await expect(testApp().app.call("nope" as never, {} as never)).rejects.toMatchObject({
code: "not-found",
});
});
});
describe("response validation", () => {
const engine = (validateResponses: boolean) =>
new GameEngine({
storage: new SaveStorage(new MemoryBackend()),
clock: { now: () => 0 },
validateResponses,
// A handler that breaks the state response schema.
overrides: {
getPlayerState: () => ({ playerId: 42 }) as unknown as RouteResponse<"getPlayerState">,
},
});
it("fails a response that violates its schema when checking is on", async () => {
const app = engine(true);
await signUp(app);
await expect(app.call("getPlayerState", {})).rejects.toThrow(/violates its schema/);
});
it("does not check responses when checking is off (production)", async () => {
const app = engine(false);
await signUp(app);
await expect(app.call("getPlayerState", {})).resolves.toEqual({ playerId: 42 });
});
});