UNPKG

fastify-openapi-glue

Version:

generate a fastify configuration from an openapi specification

528 lines (454 loc) 12.2 kB
import { createRequire } from "node:module"; import { test } from "node:test"; import Fastify from "fastify"; import fastifyOpenapiGlue from "../index.js"; const importJSON = createRequire(import.meta.url); const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; const testSpec = await importJSON("./test-openapi.v3.json"); const petStoreSpec = await importJSON("./petstore-openapi.v3.json"); const testSpecYAML = localFile("./test-openapi.v3.yaml"); const genericPathItemsSpec = await importJSON( "./test-openapi-v3-generic-path-items.json", ); import { Service } from "./service.js"; const serviceHandlers = new Service(); const noStrict = { ajv: { customOptions: { strict: false, }, }, }; const opts = { specification: testSpec, serviceHandlers, }; const prefixOpts = { specification: testSpec, serviceHandlers, prefix: "prefix", }; const yamlOpts = { specification: testSpecYAML, serviceHandlers, }; const invalidSwaggerOpts = { specification: { valid: false }, serviceHandlers, }; const invalidServiceOpts = { specification: testSpecYAML, serviceHandlers: "wrong", }; const petStoreOpts = { specification: petStoreSpec, serviceHandlers, }; const genericPathItemsOpts = { specification: genericPathItemsSpec, serviceHandlers, }; const serviceAndOperationResolver = { specification: testSpec, serviceHandlers: localFile("./not-a-valid-service.js"), operationResolver() { return; }, }; const noServiceNoResolver = { specification: testSpec, }; const withOperationResolver = { specification: testSpec, operationResolver() { return async (_req, reply) => { reply.send("ok"); }; }, }; const withOperationResolverUsingMethodPath = { specification: testSpec, operationResolver(_operationId, method) { const result = method === "GET" ? "ok" : "notOk"; return async (_req, reply) => { reply.send(result); }; }, }; process.on("warning", (warning) => { if (warning.name === "FastifyWarning") { throw new Error(`Fastify generated a warning: ${warning}`); } }); test("path parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/pathParam/2", }); t.assert.equal(res.statusCode, 200); }); test("query parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/queryParam?int1=1&int2=2", }); t.assert.equal(res.statusCode, 200); }); test("query parameters with object schema work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/queryParamObject?int1=1&int2=2", }); t.assert.equal(res.statusCode, 200); }); test("query parameters with array schema work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/queryParamArray?arr=1&arr=2", }); t.assert.equal(res.statusCode, 200); }); test("header parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ url: "/headerParam", method: "GET", headers: { "X-Request-ID": "test data", }, }); t.assert.equal(res.statusCode, 200); }); test("missing header parameters returns error 500", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/headerParam", }); t.assert.equal(res.statusCode, 500); }); test("missing authorization header returns error 500", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/authHeaderParam", }); t.assert.equal(res.statusCode, 500); }); test("body parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "post", url: "/bodyParam", payload: { str1: "test data", str2: "test data", }, }); t.assert.equal(res.statusCode, 200); }); test("no parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "get", url: "/noParam", }); t.assert.equal(res.statusCode, 200); }); test("prefix in opts works", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, prefixOpts); const res = await fastify.inject({ method: "get", url: "/prefix/noParam", }); t.assert.equal(res.statusCode, 200); }); test("missing operation from service returns error 500", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "get", url: "/noOperationId/1", }); t.assert.equal(res.statusCode, 500); }); test("response schema works with valid response", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "get", url: "/responses?replyType=valid", }); t.assert.equal(res.statusCode, 200); }); test("response schema works with invalid response", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "get", url: "/responses?replyType=invalid", }); t.assert.equal(res.statusCode, 500); }); test("yaml spec works", async (t) => { const fastify = Fastify(noStrict); fastify.register(fastifyOpenapiGlue, yamlOpts); const res = await fastify.inject({ method: "GET", url: "/pathParam/2", }); t.assert.equal(res.statusCode, 200); }); test("generic path parameters work", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); const res = await fastify.inject({ method: "GET", url: "/pathParam/2", }); t.assert.equal(res.statusCode, 200); }); test("generic path parameters override works", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); const res = await fastify.inject({ method: "GET", url: "/noParam", }); t.assert.equal(res.statusCode, 200); }); test("invalid openapi v3 specification throws error ", (t, done) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, invalidSwaggerOpts); fastify.ready((err) => { if (err) { t.assert.equal( err.message, "'specification' parameter must contain a valid version 2.0 or 3.0.x or 3.1.x specification", "got expected error", ); done(); } else { t.assert.fail("missed expected error"); } }); }); test("missing service definition throws error ", (t, done) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, invalidServiceOpts); fastify.ready((err) => { if (err) { t.assert.equal( err.message, "'serviceHandlers' parameter must refer to an object", "got expected error", ); done(); } else { t.assert.fail("missed expected error"); } }); }); test("full pet store V3 definition does not throw error ", (t, done) => { const fastify = Fastify(noStrict); // dummy parser to fix coverage testing fastify.addContentTypeParser( "application/xml", { parseAs: "string" }, (_req, body) => body, ); fastify.register(fastifyOpenapiGlue, petStoreOpts); fastify.ready((err) => { if (err) { t.assert.fail("got unexpected error"); } else { t.assert.ok(true, "no unexpected error"); done(); } }); }); test("V3.0.1 definition does not throw error", (t, done) => { const spec301 = structuredClone(petStoreSpec); spec301["openapi"] = "3.0.1"; const opts301 = { specification: spec301, serviceHandlers, }; const fastify = Fastify(noStrict); fastify.register(fastifyOpenapiGlue, opts301); fastify.ready((err) => { if (err) { t.assert.fail("got unexpected error"); } else { t.assert.ok(true, "no unexpected error"); done(); } }); }); test("V3.0.2 definition does not throw error", (t, done) => { const spec302 = structuredClone(petStoreSpec); spec302["openapi"] = "3.0.2"; const opts302 = { specification: spec302, serviceHandlers, }; const fastify = Fastify(noStrict); fastify.register(fastifyOpenapiGlue, opts302); fastify.ready((err) => { if (err) { t.assert.fail("got unexpected error"); } else { t.assert.ok(true, "no unexpected error"); done(); } }); }); test("V3.0.3 definition does not throw error", (t, done) => { const spec303 = structuredClone(petStoreSpec); spec303["openapi"] = "3.0.3"; const opts303 = { specification: spec303, serviceHandlers, }; const fastify = Fastify(noStrict); fastify.register(fastifyOpenapiGlue, opts303); fastify.ready((err) => { if (err) { t.assert.fail("got unexpected error"); } else { t.assert.ok(true, "no unexpected error"); done(); } }); }); test("x- props are copied", async (t) => { const fastify = Fastify(); fastify.addHook("preHandler", async (request, _reply) => { if (request.routeOptions.schema["x-tap-ok"]) { t.assert.ok(true, "found x- prop"); } else { t.assert.fail("missing x- prop"); } }); fastify.register(fastifyOpenapiGlue, opts); const res = await fastify.inject({ method: "GET", url: "/queryParam?int1=1&int2=2", }); t.assert.equal(res.statusCode, 200); }); test("x-fastify-config is applied", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, { ...opts, serviceHandlers: { operationWithFastifyConfigExtension: async (req, _reply) => { t.assert.equal( req.routeOptions.config.rawBody, true, "config.rawBody is true", ); return; }, }, }); await fastify.inject({ method: "GET", url: "/operationWithFastifyConfigExtension", }); }); test("x-no-fastify-config is applied", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, { ...opts, serviceHandlers: { ignoreRoute: async (_req, _reply) => {}, }, }); const res = await fastify.inject({ method: "GET", url: "/ignoreRoute", }); t.assert.equal(res.statusCode, 404); }); test("service and operationResolver together throw error", (t, done) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, serviceAndOperationResolver); fastify.ready((err) => { if (err) { t.assert.equal( err.message, "'serviceHandlers' and 'operationResolver' are mutually exclusive", "got expected error", ); done(); } else { t.assert.fail("missed expected error"); } }); }); test("no service and no operationResolver throw error", (t, done) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, noServiceNoResolver); fastify.ready((err) => { if (err) { t.assert.equal( err.message, "either 'serviceHandlers' or 'operationResolver' are required", "got expected error", ); done(); } else { t.assert.fail("missed expected error"); } }); }); test("operation resolver works", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, withOperationResolver); const res = await fastify.inject({ method: "get", url: "/noParam", }); t.assert.equal(res.body, "ok"); }); test("operation resolver with method and url works", async (t) => { const fastify = Fastify(); fastify.register(fastifyOpenapiGlue, withOperationResolverUsingMethodPath); const res = await fastify.inject({ method: "get", url: "/noParam", }); t.assert.equal(res.body, "ok"); }); test("create an empty body with addEmptySchema option", async (t) => { const fastify = Fastify(); let emptyBodySchemaFound = false; fastify.addHook("onRoute", (routeOptions) => { if (routeOptions.url === "/emptyBodySchema") { t.assert.deepStrictEqual(routeOptions.schema.response?.["204"], {}); t.assert.deepStrictEqual(routeOptions.schema.response?.["302"], {}); emptyBodySchemaFound = true; } }); await fastify.register(fastifyOpenapiGlue, { specification: testSpec, serviceHandlers: new Set(), addEmptySchema: true, }); t.assert.ok(emptyBodySchemaFound); });