diff --git a/__tests__/__fixtures__/ref-oas/external-components.json b/__tests__/__fixtures__/ref-oas/external-components.json new file mode 100644 index 000000000..0a87758ce --- /dev/null +++ b/__tests__/__fixtures__/ref-oas/external-components.json @@ -0,0 +1,179 @@ +{ + "requestBodies": { + "Pet": { + "content": { + "application/json": { + "schema": { + "$ref": "#/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/schemas/Pet" + } + } + }, + "description": "Pet object that needs to be added to the store", + "required": true + } + }, + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "default": 40, + "example": 25 + }, + "category": { + "$ref": "#/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string", + "example": "https://example.com/photo.png" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + } + } +} diff --git a/__tests__/__fixtures__/ref-oas/petstore.json b/__tests__/__fixtures__/ref-oas/petstore.json new file mode 100644 index 000000000..a9633840e --- /dev/null +++ b/__tests__/__fixtures__/ref-oas/petstore.json @@ -0,0 +1,132 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Example petstore to demo our handling of external $ref pointers" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v2" + } + ], + "paths": { + "/pet": { + "post": { + "tags": ["pet"], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "requestBody": { + "$ref": "__tests__/__fixtures__/ref-oas/external-components.json#/requestBodies/Pet" + }, + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + }, + "put": { + "tags": ["pet"], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "requestBody": { + "$ref": "__tests__/__fixtures__/ref-oas/external-components.json#/requestBodies/Pet" + }, + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": ["pet"], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "__tests__/__fixtures__/ref-oas/external-components.json#/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "__tests__/__fixtures__/ref-oas/external-components.json#/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "default": { + "description": "successful response" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} diff --git a/__tests__/cmds/__snapshots__/openapi.test.js.snap b/__tests__/cmds/__snapshots__/openapi.test.js.snap new file mode 100644 index 000000000..68b124db7 --- /dev/null +++ b/__tests__/cmds/__snapshots__/openapi.test.js.snap @@ -0,0 +1,630 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rdme openapi should bundle and upload the expected content 1`] = ` +Object { + "components": Object { + "securitySchemes": Object { + "api_key": Object { + "in": "header", + "name": "api_key", + "type": "apiKey", + }, + "petstore_auth": Object { + "flows": Object { + "implicit": Object { + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "scopes": Object { + "read:pets": "read your pets", + "write:pets": "modify pets in your account", + }, + }, + }, + "type": "oauth2", + }, + }, + }, + "info": Object { + "title": "Example petstore to demo our handling of external $ref pointers", + "version": "1.0.0", + }, + "openapi": "3.0.0", + "paths": Object { + "/pet": Object { + "post": Object { + "description": "", + "operationId": "addPet", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + "application/xml": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + }, + "description": "Pet object that needs to be added to the store", + "required": true, + }, + "responses": Object { + "405": Object { + "description": "Invalid input", + }, + }, + "security": Array [ + Object { + "petstore_auth": Array [ + "write:pets", + "read:pets", + ], + }, + ], + "summary": "Add a new pet to the store", + "tags": Array [ + "pet", + ], + }, + "put": Object { + "description": "", + "operationId": "updatePet", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + "application/xml": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + }, + "description": "Pet object that needs to be added to the store", + "required": true, + }, + "responses": Object { + "400": Object { + "description": "Invalid ID supplied", + }, + "404": Object { + "description": "Pet not found", + }, + "405": Object { + "description": "Validation exception", + }, + }, + "security": Array [ + Object { + "petstore_auth": Array [ + "write:pets", + "read:pets", + ], + }, + ], + "summary": "Update an existing pet", + "tags": Array [ + "pet", + ], + }, + }, + "/pet/{petId}": Object { + "get": Object { + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": Array [ + Object { + "description": "ID of pet to return", + "in": "path", + "name": "petId", + "required": true, + "schema": Object { + "format": "int64", + "type": "integer", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + "application/xml": Object { + "schema": Object { + "properties": Object { + "category": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Category", + }, + }, + "id": Object { + "default": 40, + "example": 25, + "format": "int64", + "type": "integer", + }, + "name": Object { + "example": "doggie", + "type": "string", + }, + "photoUrls": Object { + "items": Object { + "example": "https://example.com/photo.png", + "type": "string", + }, + "type": "array", + "xml": Object { + "name": "photoUrl", + "wrapped": true, + }, + }, + "status": Object { + "description": "pet status in the store", + "enum": Array [ + "available", + "pending", + "sold", + ], + "type": "string", + }, + "tags": Object { + "items": Object { + "properties": Object { + "id": Object { + "format": "int64", + "type": "integer", + }, + "name": Object { + "type": "string", + }, + }, + "type": "object", + "xml": Object { + "name": "Tag", + }, + }, + "type": "array", + "xml": Object { + "name": "tag", + "wrapped": true, + }, + }, + }, + "required": Array [ + "name", + "photoUrls", + ], + "type": "object", + "xml": Object { + "name": "Pet", + }, + }, + }, + }, + "description": "successful operation", + }, + "400": Object { + "description": "Invalid ID supplied", + }, + "404": Object { + "description": "Pet not found", + }, + "default": Object { + "description": "successful response", + }, + }, + "security": Array [ + Object { + "api_key": Array [], + }, + ], + "summary": "Find pet by ID", + "tags": Array [ + "pet", + ], + }, + }, + }, + "servers": Array [ + Object { + "url": "http://petstore.swagger.io/v2", + }, + ], +} +`; diff --git a/__tests__/cmds/openapi.test.js b/__tests__/cmds/openapi.test.js index 628a8579b..f170683ab 100644 --- a/__tests__/cmds/openapi.test.js +++ b/__tests__/cmds/openapi.test.js @@ -200,6 +200,38 @@ describe('rdme openapi', () => { }); }); + it('should bundle and upload the expected content', () => { + let requestBody = null; + const mock = nock(config.host) + .get(`/api/v1/api-specification`) + .basicAuth({ user: key }) + .reply(200, []) + .get(`/api/v1/version/${version}`) + .basicAuth({ user: key }) + .reply(200, { version: '1.0.0' }) + .post('/api/v1/api-specification', body => { + requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); + requestBody = JSON.parse(requestBody); + + return body.match('form-data; name="spec"'); + }) + .basicAuth({ user: key }) + .reply(201, { _id: 1 }, { location: exampleRefLocation }); + + return openapi.run({ spec: './__tests__/__fixtures__/ref-oas/petstore.json', key, version }).then(() => { + expect(console.log).toHaveBeenCalledTimes(1); + + expect(requestBody).toMatchSnapshot(); + + const output = getCommandOutput(); + expect(output).toMatch(/successfully uploaded/); + expect(output).toMatch(exampleRefLocation); + expect(output).toMatch(/to update your swagger or openapi file/i); + + mock.done(); + }); + }); + it('should error if no api key provided', async () => { await expect(openapi.run({ spec: './__tests__/__fixtures__/swagger.json' })).rejects.toThrow( 'No project API key provided. Please use `--key`.' diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index a94d1e900..cb1affa03 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -1,7 +1,6 @@ require('colors'); const request = require('request-promise-native'); const fs = require('fs'); -const path = require('path'); const config = require('config'); const { prompt } = require('enquirer'); const OASNormalize = require('oas-normalize'); @@ -98,9 +97,23 @@ exports.run = async function (opts) { } } + let bundledSpec; + const oas = new OASNormalize(specPath, { enablePaths: true }); + await oas.validate().catch(err => { + return Promise.reject(err); + }); + await oas + .bundle() + .then(res => { + bundledSpec = JSON.stringify(res); + }) + .catch(err => { + return Promise.reject(err); + }); + const options = { formData: { - spec: fs.createReadStream(path.resolve(process.cwd(), specPath)), + spec: bundledSpec, }, headers: { 'x-readme-version': versionCleaned, @@ -120,13 +133,6 @@ exports.run = async function (opts) { return request.put(`${config.host}/api/v1/api-specification/${specId}`, options).then(success, error); } - if (spec) { - const oas = new OASNormalize(spec, { enablePaths: true }); - await oas.validate().catch(err => { - return Promise.reject(err); - }); - } - /* Create a new OAS file in Readme: - Enter flow if user does not pass an id as cli arg