UNPKG

serverless-openapi-documenter

Version:

Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config

924 lines (816 loc) 30.7 kB
"use strict"; const fs = require("fs").promises; const path = require("path"); const expect = require("chai").expect; const nock = require("nock"); const modelsDocumentOG = require("../models/models/models.json"); const modelsAltDocumentOG = require("../models/models/models-alt.json"); const modelsListDocumentOG = require("../models/models/modelsList.json"); const modelsListAltDocumentOG = require("../models/models/modelsList-alt.json"); const serverlessMock = require("../helpers/serverless"); const SchemaHandler = require("../../src/schemaHandler"); describe(`SchemaHandler`, function () { let mockServerless; let openAPI; let modelsDocument, modelsAltDocument, modelsListDocument, modelsListAltDocument; const logger = { verbose: (str) => { console.log(str); }, warn: (str) => { console.log(str); }, }; const v4 = new RegExp( /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i ); const openAPISchema = { version: "3.0.3", components: { schemas: {}, }, }; beforeEach(function () { mockServerless = structuredClone(serverlessMock); modelsDocument = structuredClone(modelsDocumentOG); modelsAltDocument = structuredClone(modelsAltDocumentOG); modelsListDocument = structuredClone(modelsListDocumentOG); modelsListAltDocument = structuredClone(modelsListAltDocumentOG); openAPI = structuredClone(openAPISchema); }); describe(`constuctor`, function () { it("should return an instance of SchemaHandler", function () { const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected).to.be.an.instanceOf(SchemaHandler); }); describe(`standardising the models`, function () { it(`should standardise models syntax in to the correct format`, function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(1); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); it(`should standardise alternative models syntax in to the correct format`, function () { Object.assign( mockServerless.service.custom.documentation, modelsAltDocument ); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(1); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); it(`should standardise modelsList syntax in to the correct format`, function () { Object.assign( mockServerless.service.custom.documentation, modelsListDocument ); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(1); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); it(`should standardise alternative modelsList syntax in to the correct format`, function () { Object.assign( mockServerless.service.custom.documentation, modelsListAltDocument ); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(1); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); it(`should standardise mixed models syntax in to the correct format`, function () { const newModelsDocument = structuredClone(modelsDocument); Object.assign( mockServerless.service.custom.documentation, newModelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", description: "A success response", contentType: "application/json", schema: { type: "string", }, }); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(2); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(expected.models[1].name).to.equal("SuccessResponse"); expect(expected.models[1]).to.have.property("contentType"); expect(expected.models[1]).to.have.property("schema"); expect(expected.models[1].schema).to.be.eql({ type: "string" }); }); it(`should standardise mixed modelsList syntax in to the correct format`, function () { const newModelsDocument = structuredClone(modelsListDocument); Object.assign( mockServerless.service.custom.documentation, newModelsDocument ); mockServerless.service.custom.documentation.modelsList.push({ name: "SuccessResponse", description: "A success response", contentType: "application/json", schema: { type: "string", }, }); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(2); expect(expected.models[0].name).to.equal("ErrorResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(expected.models[1].name).to.equal("SuccessResponse"); expect(expected.models[1]).to.have.property("contentType"); expect(expected.models[1]).to.have.property("schema"); expect(expected.models[1].schema).to.be.eql({ type: "string" }); }); it(`should standardise mixed models and modelsList syntax in to the correct format`, function () { const newModelsDocument = structuredClone(modelsListDocument); Object.assign( mockServerless.service.custom.documentation, newModelsDocument ); Object.assign(mockServerless.service.custom.documentation, { models: [ { name: "SuccessResponse", description: "A success response", contentType: "application/json", schema: { type: "string", }, }, ], }); const expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.models).to.be.an("array"); expect(expected.models.length).to.be.equal(2); expect(expected.models[0].name).to.equal("SuccessResponse"); expect(expected.models[0]).to.have.property("contentType"); expect(expected.models[0]).to.have.property("schema"); expect(expected.models[0].schema).to.be.eql({ type: "string" }); expect(expected.models[1].name).to.equal("ErrorResponse"); expect(expected.models[1]).to.have.property("contentType"); expect(expected.models[1]).to.have.property("schema"); expect(expected.models[1].schema).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); }); it(`should correctly resolve the RefParserOptions`, async function () { let expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.refParserOptions).to.be.an("object"); expect(expected.refParserOptions).to.be.empty; await fs.mkdir(path.resolve("options")).catch((err) => { console.error(err); throw err; }); await fs .copyFile( path.resolve("test/helpers/ref-parser.js"), path.resolve("options/ref-parser.js") ) .catch((err) => { console.error(err); throw err; }); expected = new SchemaHandler(mockServerless, openAPI, logger); expect(expected.refParserOptions).to.be.an("object"); expect(expected.refParserOptions).to.have.property("continueOnError"); await fs.rm(path.resolve("options/ref-parser.js")).catch((err) => { console.error(err); throw err; }); await fs.rmdir(path.resolve("options")).catch((err) => { console.error(err); throw err; }); }); }); describe(`addModelsToOpenAPI`, function () { describe(`embedded simple schemas`, function () { it(`should add the model to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect(schemaHandler.openAPI.components.schemas.ErrorResponse).to.be.an( "object" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); }); it(`should add a model with references to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: { type: "object", properties: { name: { $ref: "#/definitions/nameObject", }, }, definitions: { nameObject: { type: "object", properties: { firstName: { type: "string", }, }, }, }, }, }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect(schemaHandler.openAPI.components.schemas.ErrorResponse).to.be.an( "object" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "SuccessResponse" ); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.eql({ type: "object", properties: { name: { type: "object", properties: { firstName: { type: "string", }, }, }, }, }); }); it(`should add a model with poorly dereferenced references to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: { type: "object", $ref: "#/definitions/nameObject", definitions: { nameObject: { type: "object", properties: { firstName: { type: "string", }, }, }, }, }, }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect(schemaHandler.openAPI.components.schemas.ErrorResponse).to.be.an( "object" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "SuccessResponse" ); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.eql({ type: "object", properties: { firstName: { type: "string" } }, }); }); }); describe(`schemas with references`, function () { describe(`file references`, function () { it(`should add schemas with file references to the openAPI schema`, async function () {}); }); describe(`component references`, function () { it(`should add schemas with component references to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: { type: "array", items: { $ref: "#/components/schemas/Agency", }, }, }); mockServerless.service.custom.documentation.models.push({ name: "Agency", contentType: "application/json", schema: { type: "string", }, }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "SuccessResponse" ); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.eql({ type: "array", items: { $ref: "#/components/schemas/Agency", }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "Agency" ); expect(schemaHandler.openAPI.components.schemas.Agency).to.be.an( "object" ); expect(schemaHandler.openAPI.components.schemas.Agency).to.be.eql({ type: "string", }); }); }); describe(`other references`, function () { it(`should add a model that is a webUrl to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: "https://google.com/build/LicensedMember.json", }); nock("https://google.com") .get("/build/LicensedMember.json") .reply(200, { type: "object", properties: { memberId: { type: "string", }, createdAt: { type: "integer", }, }, }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "SuccessResponse" ); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.eql({ type: "object", properties: { memberId: { type: "string", }, createdAt: { type: "integer", }, }, }); }); it(`should add a complex model that is a webUrl to the openAPI schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: "https://google.com/build/LicensedMember.json", }); nock("https://google.com") .get("/build/LicensedMember.json") .reply(200, { type: "object", properties: { street_address: { type: "string", }, country: { default: "United States of America", enum: ["United States of America", "Canada"], }, }, if: { properties: { country: { const: "United States of America" } }, }, then: { properties: { postal_code: { pattern: "[0-9]{5}(-[0-9]{4})?" }, }, }, else: { properties: { postal_code: { pattern: "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]" }, }, }, }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI(); expect(schemaHandler.openAPI).to.have.property("components"); expect(schemaHandler.openAPI.components).to.have.property("schemas"); expect(schemaHandler.openAPI.components.schemas).to.have.property( "ErrorResponse" ); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.ErrorResponse ).to.be.eql({ type: "object", properties: { error: { type: "string" } }, }); expect(schemaHandler.openAPI.components.schemas).to.have.property( "SuccessResponse" ); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.be.an("object"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse.properties ).to.be.eql({ street_address: { type: "string", }, country: { default: "United States of America", enum: ["United States of America", "Canada"], }, }); expect( schemaHandler.openAPI.components.schemas.SuccessResponse ).to.have.property("oneOf"); expect( schemaHandler.openAPI.components.schemas.SuccessResponse.oneOf .length ).to.be.equal(2); expect( Object.keys(schemaHandler.openAPI.components.schemas).length ).to.be.equal(3); }); it(`should throw when a webUrl returns a 404`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); mockServerless.service.custom.documentation.models.push({ name: "SuccessResponse", contentType: "application/json", schema: "https://google.com/build/LicensedMember.json", }); nock("https://google.com") .get("/build/LicensedMember.json") .reply(404, { body: "Bad Request" }); const schemaHandler = new SchemaHandler( mockServerless, openAPI, logger ); await schemaHandler.addModelsToOpenAPI().catch((err) => { expect(err).to.not.be.undefined; }); }); }); }); }); describe(`createSchema`, function () { it(`returns a reference to the schema when the schema already exists in components and we don't pass through a schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const expected = await schemaHandler.createSchema("ErrorResponse"); expect(expected).to.be.equal("#/components/schemas/ErrorResponse"); }); it(`throws an error when the name of the schema does not exist in components and we don't pass through a schema`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const expected = await schemaHandler .createSchema("PUTRequest") .catch((err) => { expect(err).to.not.be.undefined; expect(err.message).to.be.equal( "Expected a file path, URL, or object. Got undefined" ); }); expect(expected).to.be.undefined; }); it(`returns a reference to a schema when the schema does not exist in components already`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const schema = { type: "object", properties: { createdAt: { type: "number", }, }, }; const expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.be.undefined; }); expect(expected).to.be.equal("#/components/schemas/PUTRequest"); expect(schemaHandler.openAPI.components.schemas.PUTRequest).to.be.eql( schema ); }); it(`returns a reference to a schema when the schema exists in components already and the same schema is being passed through`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const schema = { type: "object", properties: { createdAt: { type: "number", }, }, }; let expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.be.undefined; }); expect(expected).to.be.equal("#/components/schemas/PUTRequest"); expect(schemaHandler.openAPI.components.schemas.PUTRequest).to.be.eql( schema ); expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.be.undefined; }); expect(expected).to.be.equal("#/components/schemas/PUTRequest"); expect(schemaHandler.openAPI.components.schemas.PUTRequest).to.be.eql( schema ); }); it(`returns a reference to a new schema when the schema exists in components already and a different schema is being passed through`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const schema = { type: "object", properties: { createdAt: { type: "number", }, }, }; let expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.be.undefined; }); expect(expected).to.be.equal("#/components/schemas/PUTRequest"); expect(schemaHandler.openAPI.components.schemas.PUTRequest).to.be.eql( schema ); const differentSchema = { type: "object", properties: { updatedAt: { type: "number", }, }, }; expected = await schemaHandler .createSchema("PUTRequest", differentSchema) .catch((err) => { expect(err).to.be.undefined; }); const splitPath = expected.split("/"); expect(v4.test(splitPath[3].split("PUTRequest-")[1])).to.be.true; expect(expected).to.be.equal(`#/components/schemas/${splitPath[3]}`); expect(schemaHandler.openAPI.components.schemas[splitPath[3]]).to.be.eql( differentSchema ); }); it(`returns a reference to a new schema when the schema passed through is a URL`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const schema = "https://google.com/build/LicensedMember.json"; const schemaObj = { type: "object", properties: { name: { type: "string", }, address: { type: "string", }, }, }; nock("https://google.com") .get("/build/LicensedMember.json") .reply(200, schemaObj); let expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.be.undefined; }); expect(expected).to.be.equal("#/components/schemas/PUTRequest"); expect(schemaHandler.openAPI.components.schemas.PUTRequest).to.be.eql( schemaObj ); }); it(`should throw an error when a schema as a URL can not be resolved correctly`, async function () { Object.assign( mockServerless.service.custom.documentation, modelsDocument ); const schemaHandler = new SchemaHandler(mockServerless, openAPI, logger); await schemaHandler.addModelsToOpenAPI(); const schema = "https://google.com/build/LicensedMember.json"; nock("https://google.com").get("/build/LicensedMember.json").reply(404); let expected = await schemaHandler .createSchema("PUTRequest", schema) .catch((err) => { expect(err).to.not.undefined; }); expect(expected).to.be.undefined; }); }); });