diff --git a/package-lock.json b/package-lock.json index 1506066..2729e90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-openapi-documenter", - "version": "0.0.18", + "version": "0.0.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "serverless-openapi-documenter", - "version": "0.0.18", + "version": "0.0.19", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", diff --git a/package.json b/package.json index d3b3a71..55514ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-openapi-documenter", - "version": "0.0.18", + "version": "0.0.19", "description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config", "main": "index.js", "keywords": [ diff --git a/src/openAPIGenerator.js b/src/openAPIGenerator.js index e90d738..939f97f 100644 --- a/src/openAPIGenerator.js +++ b/src/openAPIGenerator.js @@ -106,7 +106,7 @@ class OpenAPIGenerator { async generate() { this.log(this.defaultLog, chalk.bold.underline('OpenAPI v3 Document Generation')) - const config = this.processCliInput() + this.processCliInput() const generator = new DefinitionGenerator(this.serverless); await generator.parse() @@ -124,42 +124,22 @@ class OpenAPIGenerator { if (valid) this.log('success', 'OpenAPI v3 Documentation Successfully Generated') - if (config.postmanCollection) { - const postmanGeneration = (err, result) => { - if (err) { - this.log('error', `ERROR: An error was thrown when generating the postman collection`) - throw new this.serverless.classes.Error(err) - } - - this.log('success', 'postman collection v2 Documentation Successfully Generated') - try { - fs.writeFileSync(config.postmanCollection, JSON.stringify(result.output[0].data)) - this.log('success', 'postman collection v2 Documentation Successfully Written') - } catch (err) { - this.log('error', `ERROR: An error was thrown whilst writing the postman collection`) - throw new this.serverless.classes.Error(err) - } - } - - const postmanCollection = PostmanGenerator.convert( - {type: 'json', data: JSON.parse(JSON.stringify(generator.openAPI))}, - {}, - postmanGeneration - ) + if (this.config.postmanCollection) { + this.createPostman(generator.openAPI) } let output - switch (config.format.toLowerCase()) { + switch (this.config.format.toLowerCase()) { case 'json': - output = JSON.stringify(generator.openAPI, null, config.indent); + output = JSON.stringify(generator.openAPI, null, this.config.indent); break; case 'yaml': default: - output = yaml.dump(generator.openAPI, { indent: config.indent }); + output = yaml.dump(generator.openAPI, { indent: this.config.indent }); break; } try { - fs.writeFileSync(config.file, output); + fs.writeFileSync(this.config.file, output); this.log('success', 'OpenAPI v3 Documentation Successfully Written') } catch (err) { this.log('error', `ERROR: An error was thrown whilst writing the openAPI Documentation`) @@ -167,6 +147,30 @@ class OpenAPIGenerator { } } + createPostman(openAPI) { + const postmanGeneration = (err, result) => { + if (err) { + this.log('error', `ERROR: An error was thrown when generating the postman collection`) + throw new this.serverless.classes.Error(err) + } + + this.log('success', 'postman collection v2 Documentation Successfully Generated') + try { + fs.writeFileSync(this.config.postmanCollection, JSON.stringify(result.output[0].data)) + this.log('success', 'postman collection v2 Documentation Successfully Written') + } catch (err) { + this.log('error', `ERROR: An error was thrown whilst writing the postman collection`) + throw new this.serverless.classes.Error(err) + } + } + + PostmanGenerator.convert( + {type: 'json', data: JSON.parse(JSON.stringify(openAPI))}, + {}, + postmanGeneration + ) + } + processCliInput () { const config = { format: 'json', @@ -182,7 +186,6 @@ class OpenAPIGenerator { config.postmanCollection = this.serverless.processedInput.options.postmanCollection || null if (['yaml', 'json'].indexOf(config.format.toLowerCase()) < 0) { - // throw new Error('Invalid Output Format Specified - must be one of "yaml" or "json"'); throw new this.serverless.classes.Error('Invalid Output Format Specified - must be one of "yaml" or "json"') } @@ -199,7 +202,7 @@ class OpenAPIGenerator { ${config.postmanCollection ? `postman collection: ${chalk.bold.green(config.postmanCollection)}`: `\n\n`}` ) - return config + this.config = config } validateDetails(validation) { diff --git a/test/json/valid-openAPI.json b/test/json/valid-openAPI.json new file mode 100644 index 0000000..a51fdaf --- /dev/null +++ b/test/json/valid-openAPI.json @@ -0,0 +1,274 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "serverless-openapi-doc-demo", + "description": "This is a description of what this does", + "version": "1.0.0" + }, + "components": { + "schemas": { + "username": { + "type": "string", + "pattern": "^[-a-z0-9_]+$" + }, + "membershipType": { + "type": "string", + "enum": [ + "premium", + "standard" + ] + }, + "SessionId": { + "type": "string" + }, + "PutDocumentRequest": { + "properties": { + "SomeObject": { + "type": "object", + "properties": { + "SomeAttribute": { + "type": "string" + } + } + } + } + }, + "PutDocumentResponse": { + "title": "Empty Schema", + "type": "object" + }, + "error": { + "type": "object", + "properties": { + "id": { + "description": "A unique identifier for this particular occurrence of the problem.", + "type": "string" + }, + "links": { + "$ref": "#/components/schemas/links" + }, + "status": { + "description": "The HTTP status code applicable to this problem, expressed as a string value.", + "type": "string" + }, + "code": { + "description": "An application-specific error code, expressed as a string value.", + "type": "string" + }, + "title": { + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.", + "type": "string" + }, + "detail": { + "description": "A human-readable explanation specific to this occurrence of the problem.", + "type": "string" + }, + "source": { + "type": "object", + "properties": { + "pointer": { + "description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].", + "type": "string" + }, + "parameter": { + "description": "A string indicating which query parameter caused the error.", + "type": "string" + } + } + }, + "meta": { + "$ref": "#/components/schemas/meta" + } + }, + "additionalProperties": false + }, + "meta": { + "description": "Non-standard meta-information that can not be represented as an attribute or relationship.", + "type": "object", + "additionalProperties": true + }, + "links": { + "description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.", + "type": "object", + "properties": { + "self": { + "description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.", + "type": "string", + "format": "uri" + }, + "related": { + "$ref": "#/components/schemas/link" + } + }, + "additionalProperties": true + }, + "link": { + "description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.", + "oneOf": [ + { + "description": "A string containing the link's URL.", + "type": "string", + "format": "uri" + }, + { + "type": "object", + "required": [ + "href" + ], + "properties": { + "href": { + "description": "A string containing the link's URL.", + "type": "string", + "format": "uri" + }, + "meta": { + "$ref": "#/components/schemas/meta" + } + } + } + ] + }, + "ErrorResponse": { + "title": "JSON API Schema", + "description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org", + "type": "object", + "required": [ + "errors" + ], + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/error" + }, + "uniqueItems": true + }, + "meta": { + "$ref": "#/components/schemas/meta" + }, + "links": { + "$ref": "#/components/schemas/links" + } + }, + "additionalProperties": false + } + } + }, + "paths": { + "/create/{username}": { + "post": { + "summary": "Create User", + "description": "Creates a user and then sends a generated password email", + "operationId": "serverless-openapi-doc-demo-dev-createUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The username for a user to create", + "required": true, + "schema": { + "$ref": "#/components/schemas/username" + } + }, + { + "name": "membershipType", + "in": "query", + "description": "The user's Membership Type", + "required": false, + "schema": { + "$ref": "#/components/schemas/membershipType" + } + }, + { + "name": "SessionId", + "in": "cookie", + "description": "A Session ID variable", + "required": false, + "schema": { + "$ref": "#/components/schemas/SessionId" + } + } + ], + "tags": [ + "jesus" + ], + "externalDocs": { + "url": "https://bing.com", + "description": "A link to bing" + }, + "requestBody": { + "description": "A user information object", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutDocumentRequest" + } + } + } + }, + "responses": { + "201": { + "description": "A user object along with generated API Keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutDocumentResponse" + } + } + } + }, + "500": { + "description": "An error message when creating a new user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "summary": "a function", + "description": "blah blah" + }, + "/patch/": { + "patch": { + "summary": "Patch a User", + "description": "Patch details about the user", + "operationId": "serverless-openapi-doc-demo-dev-patchUser", + "parameters": [], + "tags": [ + "patching" + ], + "responses": { + "200": { + "description": "A user object along with generated API Keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutDocumentResponse" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "jesus", + "description": "jesus was a man", + "externalDocs": { + "url": "https://whitehouse.gov", + "description": "a link to the whitehouse" + } + } + ], + "externalDocs": { + "url": "https://google.com", + "description": "A link to google" + } +} diff --git a/test/unit/openAPIGenerator.spec.js b/test/unit/openAPIGenerator.spec.js index 9fe5fae..df38cdc 100644 --- a/test/unit/openAPIGenerator.spec.js +++ b/test/unit/openAPIGenerator.spec.js @@ -1,10 +1,101 @@ 'use strict' +const fs = require('fs') +const PostmanGenerator = require('openapi-to-postmanv2') const sinon = require('sinon') const expect = require('chai').expect +const validOpenAPI = require('../json/valid-openAPI.json') + const OpenAPIGenerator = require('../../src/openAPIGenerator') -xdescribe('OpenAPIGenerator', () => { +describe('OpenAPIGenerator', () => { + let sls, logOutput + beforeEach(function() { + sls = { + version: '3.0.0', + variables: { + service: { + custom: { + + } + } + }, + configSchemaHandler: { + defineFunctionEventProperties: () => {}, + defineFunctionProperties: () => {} + }, + classes: { + Error: class ServerlessError {constructor(err) {return new Error(err)}} + }, + processedInput: { + options: { + postmanCollection: 'postman.json' + } + } + } + + logOutput = { + log: { + notice: (str) => {}, + error: (str) => {}, + success: (str) => {} + } + } + }); + describe('createPostman', () => { + it('should generate a postman collection when a valid openAPI file is generated', function() { + const fsStub = sinon.stub(fs, 'writeFileSync').returns(true) + const succSpy = sinon.spy(logOutput.log, 'success') + const errSpy = sinon.spy(logOutput.log, 'error') + const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput) + openAPIGenerator.processCliInput() + + openAPIGenerator.createPostman(validOpenAPI) + + expect(fsStub.called).to.be.true + expect(succSpy.calledTwice).to.be.true + expect(errSpy.called).to.be.false + fsStub.restore() + succSpy.restore() + errSpy.restore() + }); + + it('should throw an error when writing a file fails', function() { + const errStub = sinon.stub(logOutput.log, 'error').returns('') + const succSpy = sinon.spy(logOutput.log, 'success') + const fsStub = sinon.stub(fs, 'writeFileSync').throws(new Error()) + const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput) + openAPIGenerator.processCliInput() + + expect(() => {openAPIGenerator.createPostman(validOpenAPI)}).to.throw() + + expect(fsStub.called).to.be.true + expect(errStub.called).to.be.true + expect(succSpy.calledOnce).to.be.true + expect(succSpy.calledTwice).to.be.false + fsStub.restore() + succSpy.restore() + errStub.restore() + }); + + it('should throw an error converting an OpenAPI fails', function() { + const errStub = sinon.spy(logOutput.log, 'error') + const succSpy = sinon.spy(logOutput.log, 'success') + const pgStub = sinon.stub(PostmanGenerator, 'convert') + pgStub.yields(new Error()) + + const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput) + openAPIGenerator.processCliInput() + + expect(() => {openAPIGenerator.createPostman(validOpenAPI)}).to.throw() + + expect(errStub.called).to.be.true + expect(succSpy.calledOnce).to.be.false + expect(succSpy.calledTwice).to.be.false + succSpy.restore() + errStub.restore() + }); + }); });