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; 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(); }); });