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

85 lines
3.0 KiB
TypeScript

import { errorResponseSchema, routes } from "@deadhead/schema";
import Fastify from "fastify";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { buildApp } from "../src/app";
import { registerErrorHandler } from "../src/http/errors";
import { registerRoute } from "../src/http/route";
import { useTestDatabase } from "./db";
import { testConfig } from "./helpers";
const database = useTestDatabase();
describe("request validation from the route catalog", () => {
let app: ReturnType<typeof buildApp>;
beforeEach(() => {
app = buildApp(testConfig(), { database: database() });
});
afterEach(() => app.close());
const expectValidationError = (response: { statusCode: number; json(): unknown }) => {
expect(response.statusCode).toBe(400);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("validation-error");
};
it("rejects unexpected fields on a route that takes none", async () => {
expectValidationError(
await app.inject({ method: "POST", url: "/api/guest", payload: { surprise: true } }),
);
});
it("rejects a body that is not valid JSON", async () => {
expectValidationError(
await app.inject({
method: "POST",
url: "/api/guest",
headers: { "content-type": "application/json" },
payload: "{not json",
}),
);
});
it("rejects unexpected query parameters on an authenticated route before checking the session", async () => {
expectValidationError(await app.inject({ method: "GET", url: "/api/state?evil=1" }));
});
it("still answers valid requests", async () => {
expect((await app.inject({ method: "GET", url: "/api/time" })).statusCode).toBe(200);
});
it("returns not-found in the error format for unknown endpoints", async () => {
const response = await app.inject({ method: "GET", url: "/api/nope" });
expect(response.statusCode).toBe(404);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("not-found");
});
});
describe("response validation", () => {
const build = (validateResponses: boolean) => {
const app = Fastify({ logger: false });
registerErrorHandler(app);
// A handler that breaks the server-time response schema.
registerRoute(
app,
routes.getServerTime,
{ requirePlayer: async () => "unused", validateResponses },
async () => ({ realTime: "not a number" }) as never,
);
return app;
};
it("fails a response that violates its schema in test mode", async () => {
const app = build(true);
const response = await app.inject({ method: "GET", url: "/api/time" });
expect(response.statusCode).toBe(500);
expect(errorResponseSchema.parse(response.json()).error.code).toBe("internal-error");
await app.close();
});
it("does not check responses when validation is off (production)", async () => {
const app = build(false);
const response = await app.inject({ method: "GET", url: "/api/time" });
expect(response.statusCode).toBe(200);
await app.close();
});
});