From 0d0718ece1b9f0ffe0fb462dd9986e38fa406c23 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Tue, 11 May 2021 14:06:06 -0700 Subject: [PATCH 1/8] feat: adding support for server variables to generated snippets --- packages/httpsnippet-client-api/README.md | 10 ++ .../__fixtures__/output/alternate-server.js | 7 + .../__fixtures__/output/issue-119.js | 2 +- .../__fixtures__/output/issue-128.js | 2 +- .../__tests__/__fixtures__/output/issue-76.js | 2 +- .../__tests__/__fixtures__/output/issue-78.js | 2 +- .../__tests__/__fixtures__/output/petstore.js | 2 +- .../__fixtures__/output/query-auth.js | 2 +- .../request/alternate-server/definition.json | 104 ++++++++++++ .../request/alternate-server/har.json | 31 ++++ .../__fixtures__/request/issue-78/har.json | 2 +- .../__snapshots__/index.test.js.snap | 6 +- .../__tests__/index.test.js | 26 +-- .../httpsnippet-client-api/package-lock.json | 151 ++++++++++++------ packages/httpsnippet-client-api/package.json | 2 +- packages/httpsnippet-client-api/src/index.js | 61 +++++-- 16 files changed, 328 insertions(+), 84 deletions(-) create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/definition.json create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/har.json diff --git a/packages/httpsnippet-client-api/README.md b/packages/httpsnippet-client-api/README.md index 20e90509..60fdffa3 100644 --- a/packages/httpsnippet-client-api/README.md +++ b/packages/httpsnippet-client-api/README.md @@ -31,3 +31,13 @@ console.log( }) ); ``` + +Results in the following: + +```js +const sdk = require('api')('https://example.com/openapi.json'); + +sdk.get('/har') + .then(res => console.log(res)) + .catch(err => console.error(err)); +``` diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js new file mode 100644 index 00000000..9a7af611 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js @@ -0,0 +1,7 @@ +const sdk = require('api')('https://example.com/openapi.json'); + +sdk.auth('123'); +sdk.config({server: 'http://dev.local/v2'}); +sdk.create({foo: 'bar', hello: 'world'}, {id: '1234'}) + .then(res => console.log(res)) + .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-119.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-119.js index 41aae53c..1c0582d4 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-119.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-119.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('123'); +sdk.auth('123'); sdk['find/pets-by-status']({status: 'available'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-128.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-128.js index 0a2d40e9..cd9cf09b 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-128.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-128.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('authKey\'With\'Apostrophes'); +sdk.auth('authKey\'With\'Apostrophes'); sdk.getItem({Accept: 'application/xml'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-76.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-76.js index 4f7ccb20..0e030653 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-76.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-76.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('a5a220e'); +sdk.auth('a5a220e'); sdk.get('/pet/findByStatus', {status: 'available', Accept: 'application/xml'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-78.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-78.js index 37db9e5d..372f3ea2 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-78.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/issue-78.js @@ -1,5 +1,5 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.get('/store/order/1234/tracking/{trackingId}', {Accept: 'application/xml'}) +sdk.get('/store/order/1234/tracking/5678', {Accept: 'application/xml'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/petstore.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/petstore.js index 2726d563..626d8520 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/petstore.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/petstore.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('123'); +sdk.auth('123'); sdk.findPetsByStatus({status: 'available', Accept: 'application/xml'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/query-auth.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/query-auth.js index 56c92ead..42e20358 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/query-auth.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/query-auth.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('a5a220e'); +sdk.auth('a5a220e'); sdk.findPetsByStatus({status: 'available', Accept: 'application/xml'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/definition.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/definition.json new file mode 100644 index 00000000..a4792d24 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/definition.json @@ -0,0 +1,104 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Server variables", + "description": "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#serverVariableObject", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://{name}.example.com:{port}/{basePath}", + "variables": { + "name": { + "default": "demo" + }, + "port": { + "default": "443" + }, + "basePath": { + "default": "v2" + } + } + }, + { + "url": "http://{name}.local/{basePath}", + "variables": { + "name": { + "default": "demo" + }, + "basePath": { + "default": "v2" + } + } + }, + { + "url": "http://{subdomain}.local/{subdomain}", + "variables": { + "subdomain": { + "default": "demo" + } + } + } + ], + "paths": { + "/create/{id}": { + "post": { + "operationId": "create", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "petstore_auth": [] + } + ] + } + } + }, + "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" + } + } + } + } + } + } +} diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/har.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/har.json new file mode 100644 index 00000000..76cc19c3 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/alternate-server/har.json @@ -0,0 +1,31 @@ +{ + "log": { + "entries": [ + { + "request": { + "method": "POST", + "url": "http://dev.local/v2/create/1234", + "headers": [ + { + "name": "Authorization", + "value": "Bearer 123" + } + ], + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "params": [ + { + "name": "foo", + "value": "bar" + }, + { + "name": "hello", + "value": "world" + } + ] + } + } + } + ] + } +} diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/issue-78/har.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/issue-78/har.json index 82320e25..cafa82b5 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/issue-78/har.json +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/issue-78/har.json @@ -17,7 +17,7 @@ "paramsObj": false, "size": 0 }, - "url": "http://petstore.swagger.io/v2/store/order/1234/tracking/{trackingId}" + "url": "http://petstore.swagger.io/v2/store/order/1234/tracking/5678" } } ] diff --git a/packages/httpsnippet-client-api/__tests__/__snapshots__/index.test.js.snap b/packages/httpsnippet-client-api/__tests__/__snapshots__/index.test.js.snap index 18839cb5..8067b8dd 100644 --- a/packages/httpsnippet-client-api/__tests__/__snapshots__/index.test.js.snap +++ b/packages/httpsnippet-client-api/__tests__/__snapshots__/index.test.js.snap @@ -2,8 +2,8 @@ exports[`auth handling basic should be able to handle basic auth that's just a password 1`] = ` "const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('', 'pug') +sdk.auth('', 'pug') sdk.getAPISpecification({perPage: '10', page: '1'}) .then(res => console.log(res)) .catch(err => console.error(err));" @@ -11,8 +11,8 @@ sdk.getAPISpecification({perPage: '10', page: '1'}) exports[`auth handling basic should be able to handle basic auth that's just a username 1`] = ` "const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('buster') +sdk.auth('buster') sdk.getAPISpecification({perPage: '10', page: '1'}) .then(res => console.log(res)) .catch(err => console.error(err));" @@ -20,8 +20,8 @@ sdk.getAPISpecification({perPage: '10', page: '1'}) exports[`auth handling basic should not encode basic auth in the \`.auth()\` call 1`] = ` "const sdk = require('api')('https://example.com/openapi.json'); -sdk.auth('buster', 'pug') +sdk.auth('buster', 'pug') sdk.getAPISpecification({perPage: '10', page: '1'}) .then(res => console.log(res)) .catch(err => console.error(err));" diff --git a/packages/httpsnippet-client-api/__tests__/index.test.js b/packages/httpsnippet-client-api/__tests__/index.test.js index 4950d08d..ebc5ec22 100644 --- a/packages/httpsnippet-client-api/__tests__/index.test.js +++ b/packages/httpsnippet-client-api/__tests__/index.test.js @@ -1,11 +1,8 @@ -/* eslint-disable import/no-dynamic-require, global-require */ - -// Most of this has been copied over from the httpsnippet target unit test file. It'd be ideal if this were in a -// helper library we could use instead. const fs = require('fs').promises; const HTTPSnippet = require('@readme/httpsnippet'); const path = require('path'); const client = require('../src'); +const readme = require('@readme/oas-examples/3.0/json/readme.json'); HTTPSnippet.addTargetClient('node', client); @@ -24,7 +21,7 @@ test('it should have info', () => { }); test('it should error if no apiDefinitionUri was supplied', async () => { - const har = await fs.readFile(path.join(__dirname, `./__fixtures__/request/petstore/har.json`), 'utf8'); + const har = await fs.readFile(path.join(__dirname, './__fixtures__/request/petstore/har.json'), 'utf8'); const snippet = new HTTPSnippet(JSON.parse(har)); expect(() => { @@ -33,7 +30,7 @@ test('it should error if no apiDefinitionUri was supplied', async () => { }); test('it should error if no apiDefinition was supplied', async () => { - const har = await fs.readFile(path.join(__dirname, `./__fixtures__/request/petstore/har.json`), 'utf8'); + const har = await fs.readFile(path.join(__dirname, './__fixtures__/request/petstore/har.json'), 'utf8'); const snippet = new HTTPSnippet(JSON.parse(har)); expect(() => { @@ -45,7 +42,6 @@ test('it should error if no apiDefinition was supplied', async () => { // This test should fail because the url in the HAR is missing `/v1` in the path. test('it should error if no matching operation was found in the apiDefinition', () => { - const definition = require('@readme/oas-examples/3.0/json/readme.json'); const har = { bodySize: 0, cookies: [], @@ -65,7 +61,7 @@ test('it should error if no matching operation was found in the apiDefinition', expect(() => { snippet.convert('node', 'api', { apiDefinitionUri: 'https://example.com/openapi.json', - apiDefinition: definition, + apiDefinition: readme, }); }).toThrow(/unable to locate a matching operation/i); }); @@ -77,7 +73,6 @@ describe('auth handling', () => { ["should be able to handle basic auth that's just a username", 'buster:'], ["should be able to handle basic auth that's just a password", ':pug'], ])('%s', (testCase, authKey) => { - const definition = require('@readme/oas-examples/3.0/json/readme.json'); const har = { bodySize: 0, cookies: [], @@ -99,7 +94,7 @@ describe('auth handling', () => { const code = new HTTPSnippet(har).convert('node', 'api', { apiDefinitionUri: 'https://example.com/openapi.json', - apiDefinition: definition, + apiDefinition: readme, }); expect(code).toMatchSnapshot(); @@ -109,6 +104,7 @@ describe('auth handling', () => { describe('snippets', () => { it.each([ + ['alternate-server'], ['application-form-encoded'], ['application-json'], // ['cookies'], // Cookies test needs to get built out. @@ -132,8 +128,14 @@ describe('snippets', () => { ['short'], ['text-plain'], ])('should generate `%s` snippet', async testCase => { - const har = require(`./__fixtures__/request/${testCase}/har.json`); - const definition = require(`./__fixtures__/request/${testCase}/definition.json`); + const har = JSON.parse( + await fs.readFile(path.join(__dirname, `./__fixtures__/request/${testCase}/har.json`), 'utf8') + ); + + const definition = JSON.parse( + await fs.readFile(path.join(__dirname, `./__fixtures__/request/${testCase}/definition.json`), 'utf8') + ); + const expected = await fs.readFile(path.join(__dirname, `./__fixtures__/output/${testCase}.js`), 'utf8'); const code = new HTTPSnippet(har).convert('node', 'api', { diff --git a/packages/httpsnippet-client-api/package-lock.json b/packages/httpsnippet-client-api/package-lock.json index e3417ac3..ff69aab3 100644 --- a/packages/httpsnippet-client-api/package-lock.json +++ b/packages/httpsnippet-client-api/package-lock.json @@ -5,7 +5,7 @@ "requires": true, "packages": { "": { - "version": "3.0.2", + "version": "3.0.3", "license": "MIT", "dependencies": { "content-type": "^1.0.4", @@ -24,7 +24,7 @@ }, "peerDependencies": { "@readme/httpsnippet": "^2.4.4", - "oas": "^10.7.4" + "oas": "^11.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -2717,9 +2717,9 @@ } }, "node_modules/comment-patterns": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/comment-patterns/-/comment-patterns-0.11.0.tgz", - "integrity": "sha512-YgQOR0QcCIE0mYFywZ0ToK8r7V+48FjEWA6Jflfvxf5JlGZtJEi8BzMcc4BXcaVUyU1DUSGEhppSUfJOI2YC/w==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/comment-patterns/-/comment-patterns-0.12.0.tgz", + "integrity": "sha512-LhP+aYhloN+w6fh+U/Vwb+zjRvz7igV6V9YDPtSkdIctaUWb2NDasssTu1ujU8Z6/X5oKE3vWjRCKjCPii2FCg==", "peer": true, "dependencies": { "lodash": "^4.17.11" @@ -2947,6 +2947,15 @@ "node": ">=0.10.0" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -8004,12 +8013,12 @@ "dev": true }, "node_modules/multilang-extract-comments": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/multilang-extract-comments/-/multilang-extract-comments-0.3.3.tgz", - "integrity": "sha512-9NqT+Cf1yvM/eYp+ILUczzz2ca4upYbdQQwSn550ldFA7hX2WVZzo7jaddFkSfq6EkIkPaeWBL+LP+BZuHK0oA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/multilang-extract-comments/-/multilang-extract-comments-0.4.0.tgz", + "integrity": "sha512-8mXCo9Q42Wyfho9nn7hHkG/0sKxH0nJWfmBLl8+c+FLv++XhFkFC1sntOk4NFZ+nSpoMjlF/8ILeOLyMRTFbIw==", "peer": true, "dependencies": { - "comment-patterns": "^0.11.0", + "comment-patterns": "^0.12.0", "line-counter": "^1.0.3", "lodash": "^4.17.11", "quotemeta": "0.0.0" @@ -8250,9 +8259,9 @@ "dev": true }, "node_modules/oas": { - "version": "10.7.4", - "resolved": "https://registry.npmjs.org/oas/-/oas-10.7.4.tgz", - "integrity": "sha512-VPsFDYrfTzQltHl07h7EbgUP97yUoMJNSr3My5X1xck3tx7Qw+GekfjaL7jmjvvyIbo9JxvSg1ECWei/UD4cTA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/oas/-/oas-11.0.0.tgz", + "integrity": "sha512-uULn+eSHg7KzjGmMwpGGF93xcWgLY8tv1qrUm1fiRMNLopJbEt4GpM0UIiUfrbltbP5VNpcMFec66GSQhvIhlQ==", "peer": true, "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.6", @@ -8271,16 +8280,16 @@ "minimist": "^1.2.0", "node-status": "^1.0.0", "oas-normalize": "2.3.1", - "open": "^7.0.0", + "open": "^8.0.8", "path-to-regexp": "^6.2.0", "request": "^2.88.0", - "swagger-inline": "3.2.2" + "swagger-inline": "4.0.0" }, "bin": { "oas": "bin/oas" }, "engines": { - "node": ">=10" + "node": "^12 || ^14 || ^16" } }, "node_modules/oas-normalize": { @@ -8473,16 +8482,17 @@ } }, "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/open/-/open-8.0.8.tgz", + "integrity": "sha512-3XmKIU8+H/TVr8wB8C4vj0z748+yBydSvtpzZVS6vQ1dKNHB6AiPbhaoG+89zb80717GPk9y/7OvK0R6FXkNmQ==", "peer": true, "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10213,20 +10223,41 @@ } }, "node_modules/swagger-inline": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/swagger-inline/-/swagger-inline-3.2.2.tgz", - "integrity": "sha512-hYibOKWADD0z2nv5WqO+Uf1xYcObf4HzZ7BldLKVAOo5ZRoXajHhQPhpINM8XYD12auwtMDvpSQ3P2qg87cy1A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/swagger-inline/-/swagger-inline-4.0.0.tgz", + "integrity": "sha512-2ahHw63YbAbaA1wuvbqyojZB4oj3iF3f9hzPUEXqKKuWRgMIZKrKFIceBLbBJX4X+9LOOXhFoI0qzIB25nlwfQ==", "peer": true, "dependencies": { "bluebird": "^3.4.1", "commander": "^6.0.0", "globby": "^11.0.1", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "lodash": "^4.17.11", - "multilang-extract-comments": "^0.3.2" + "multilang-extract-comments": "^0.4.0" }, "bin": { "swagger-inline": "build/index.js" + }, + "engines": { + "node": "^12 || ^14 || ^16" + } + }, + "node_modules/swagger-inline/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "peer": true + }, + "node_modules/swagger-inline/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/swagger-parser": { @@ -13537,9 +13568,9 @@ "peer": true }, "comment-patterns": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/comment-patterns/-/comment-patterns-0.11.0.tgz", - "integrity": "sha512-YgQOR0QcCIE0mYFywZ0ToK8r7V+48FjEWA6Jflfvxf5JlGZtJEi8BzMcc4BXcaVUyU1DUSGEhppSUfJOI2YC/w==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/comment-patterns/-/comment-patterns-0.12.0.tgz", + "integrity": "sha512-LhP+aYhloN+w6fh+U/Vwb+zjRvz7igV6V9YDPtSkdIctaUWb2NDasssTu1ujU8Z6/X5oKE3vWjRCKjCPii2FCg==", "peer": true, "requires": { "lodash": "^4.17.11" @@ -13735,6 +13766,12 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "peer": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -17748,12 +17785,12 @@ "dev": true }, "multilang-extract-comments": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/multilang-extract-comments/-/multilang-extract-comments-0.3.3.tgz", - "integrity": "sha512-9NqT+Cf1yvM/eYp+ILUczzz2ca4upYbdQQwSn550ldFA7hX2WVZzo7jaddFkSfq6EkIkPaeWBL+LP+BZuHK0oA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/multilang-extract-comments/-/multilang-extract-comments-0.4.0.tgz", + "integrity": "sha512-8mXCo9Q42Wyfho9nn7hHkG/0sKxH0nJWfmBLl8+c+FLv++XhFkFC1sntOk4NFZ+nSpoMjlF/8ILeOLyMRTFbIw==", "peer": true, "requires": { - "comment-patterns": "^0.11.0", + "comment-patterns": "^0.12.0", "line-counter": "^1.0.3", "lodash": "^4.17.11", "quotemeta": "0.0.0" @@ -17958,9 +17995,9 @@ "dev": true }, "oas": { - "version": "10.7.4", - "resolved": "https://registry.npmjs.org/oas/-/oas-10.7.4.tgz", - "integrity": "sha512-VPsFDYrfTzQltHl07h7EbgUP97yUoMJNSr3My5X1xck3tx7Qw+GekfjaL7jmjvvyIbo9JxvSg1ECWei/UD4cTA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/oas/-/oas-11.0.0.tgz", + "integrity": "sha512-uULn+eSHg7KzjGmMwpGGF93xcWgLY8tv1qrUm1fiRMNLopJbEt4GpM0UIiUfrbltbP5VNpcMFec66GSQhvIhlQ==", "peer": true, "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.6", @@ -17979,10 +18016,10 @@ "minimist": "^1.2.0", "node-status": "^1.0.0", "oas-normalize": "2.3.1", - "open": "^7.0.0", + "open": "^8.0.8", "path-to-regexp": "^6.2.0", "request": "^2.88.0", - "swagger-inline": "3.2.2" + "swagger-inline": "4.0.0" } }, "oas-normalize": { @@ -18135,13 +18172,14 @@ } }, "open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/open/-/open-8.0.8.tgz", + "integrity": "sha512-3XmKIU8+H/TVr8wB8C4vj0z748+yBydSvtpzZVS6vQ1dKNHB6AiPbhaoG+89zb80717GPk9y/7OvK0R6FXkNmQ==", "peer": true, "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" } }, "openapi-types": { @@ -19556,17 +19594,34 @@ } }, "swagger-inline": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/swagger-inline/-/swagger-inline-3.2.2.tgz", - "integrity": "sha512-hYibOKWADD0z2nv5WqO+Uf1xYcObf4HzZ7BldLKVAOo5ZRoXajHhQPhpINM8XYD12auwtMDvpSQ3P2qg87cy1A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/swagger-inline/-/swagger-inline-4.0.0.tgz", + "integrity": "sha512-2ahHw63YbAbaA1wuvbqyojZB4oj3iF3f9hzPUEXqKKuWRgMIZKrKFIceBLbBJX4X+9LOOXhFoI0qzIB25nlwfQ==", "peer": true, "requires": { "bluebird": "^3.4.1", "commander": "^6.0.0", "globby": "^11.0.1", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "lodash": "^4.17.11", - "multilang-extract-comments": "^0.3.2" + "multilang-extract-comments": "^0.4.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "peer": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "peer": true, + "requires": { + "argparse": "^2.0.1" + } + } } }, "swagger-parser": { diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index cbd2869b..bb2116ab 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -29,7 +29,7 @@ }, "peerDependencies": { "@readme/httpsnippet": "^2.4.4", - "oas": "^10.7.4" + "oas": "^11.0.0" }, "devDependencies": { "@readme/eslint-config": "^5.0.0", diff --git a/packages/httpsnippet-client-api/src/index.js b/packages/httpsnippet-client-api/src/index.js index 49a02e64..e4410159 100644 --- a/packages/httpsnippet-client-api/src/index.js +++ b/packages/httpsnippet-client-api/src/index.js @@ -4,6 +4,10 @@ const CodeBuilder = require('@readme/httpsnippet/src/helpers/code-builder'); const contentType = require('content-type'); const Oas = require('oas/tooling'); +function stringify(obj, opts = {}) { + return stringifyObject(obj, { indent: ' ', ...opts }); +} + function buildAuthSnippet(authKey) { // Auth key will be an array for Basic auth cases. if (Array.isArray(authKey)) { @@ -86,24 +90,43 @@ module.exports = function (source, options) { const method = source.method.toLowerCase(); const oas = new Oas(opts.apiDefinition); - const operation = oas.getOperation(source.url, method); - - if (!operation) { + const foundOperation = oas.findOperation(source.url, method); + if (!foundOperation) { throw new Error( `Unable to locate a matching operation in the supplied \`apiDefinition\` for: ${source.method} ${source.url}` ); } - // For cases where a server URL in the OAS has a path attached to it, we don't want to include that path with the - // operation path. - const path = source.url.replace(oas.url(), ''); - + const operationSlugs = foundOperation.url.slugs; + const operation = oas.operation(foundOperation.url.nonNormalizedPath, method); + const path = operation.path; const authData = []; const authSources = getAuthSources(operation); const code = new CodeBuilder(opts.indent); code.push(`const sdk = require('api')('${opts.apiDefinitionUri}');`); + code.blank(); + + // If we have multiple servers configured and our source URL differs from the stock URL that we receive from our + // `oas` library then the URL either has server variables contained in it (that don't match the defaults), or the + // OAS offers alternate server URLs and we should expose that in the generated snippet. + const configData = []; + if ((oas.servers || []).length > 1) { + const stockUrl = oas.url(); + const baseUrl = source.url.replace(path, ''); + if (baseUrl !== stockUrl) { + const config = {}; + const serverVars = oas.splitVariables(baseUrl); + if (serverVars) { + config.server = oas.url(serverVars.selected, serverVars.variables); + } else { + config.server = baseUrl; + } + + configData.push(`sdk.config(${stringify(config, { inlineCharacterLimit: 40 })});`); + } + } let metadata = {}; if (Object.keys(source.queryObj).length) { @@ -123,10 +146,14 @@ module.exports = function (source, options) { // If we have path parameters present, we should only add them in if we have an operationId as we don't want metadata // to duplicate what we'll be setting the path in the snippet to. if ('operationId' in operation.schema) { - const pathParams = getParamsInPath(operation, path); + const pathParams = getParamsInPath(operation, operation.path); if (Object.keys(pathParams).length) { Object.keys(pathParams).forEach(param => { - metadata[param] = pathParams[param]; + if (`:${param}` in operationSlugs) { + metadata[param] = operationSlugs[`:${param}`]; + } else { + metadata[param] = pathParams[param]; + } }); } } @@ -220,7 +247,13 @@ module.exports = function (source, options) { if ('operationId' in operation.schema && operation.schema.operationId.length > 0) { accessor = operation.schema.operationId; } else { - args.push(`'${decodeURIComponent(path)}'`); + // Since we're not using an operationId as our primary accessor we need to take the current operation that we're + // working with and transpile back our path parameters on top of it. + const slugs = Object.fromEntries( + Object.keys(operationSlugs).map(slug => [slug.replace(/:(.*)/, '$1'), operationSlugs[slug]]) + ); + + args.push(`'${decodeURIComponent(oas.replaceUrl(path, slugs))}'`); } // If the operation or method accessor is non-alphanumeric, we need to add it to the SDK object as an array key. @@ -235,18 +268,20 @@ module.exports = function (source, options) { // we'll be rendering them in their own lines. const inlineCharacterLimit = typeof body !== 'undefined' && Object.keys(metadata).length > 0 ? 40 : 80; if (typeof body !== 'undefined') { - args.push(stringifyObject(body, { indent: ' ', inlineCharacterLimit })); + args.push(stringify(body, { inlineCharacterLimit })); } if (Object.keys(metadata).length > 0) { - args.push(stringifyObject(metadata, { indent: ' ', inlineCharacterLimit })); + args.push(stringify(metadata, { inlineCharacterLimit })); } if (authData.length) { code.push(authData.join('\n')); } - code.blank(); + if (configData.length) { + code.push(configData.join('\n')); + } code .push(`sdk${accessor}(${args.join(', ')})`) From 3a709351b29f46ddf54fb608b690240b23f22203 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 12 May 2021 11:42:47 -0700 Subject: [PATCH 2/8] test: refactoring some tests to make them easier to follow --- .../api/__tests__/__fixtures__/createOas.js | 20 -- .../__tests__/__fixtures__/payloads.oas.json | 58 +++++ .../__tests__/__fixtures__/security.oas.json | 240 ++++++++++++++++++ packages/api/__tests__/auth.test.js | 122 ++------- packages/api/__tests__/index.test.js | 5 +- .../api/__tests__/lib/prepareParams.test.js | 41 +-- packages/api/src/index.js | 2 + 7 files changed, 332 insertions(+), 156 deletions(-) delete mode 100644 packages/api/__tests__/__fixtures__/createOas.js create mode 100644 packages/api/__tests__/__fixtures__/payloads.oas.json create mode 100644 packages/api/__tests__/__fixtures__/security.oas.json diff --git a/packages/api/__tests__/__fixtures__/createOas.js b/packages/api/__tests__/__fixtures__/createOas.js deleted file mode 100644 index 7a5dd719..00000000 --- a/packages/api/__tests__/__fixtures__/createOas.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = serverUrl => { - return function (method = 'get', path = '/', operation = {}) { - return { - openapi: '3.0.0', - info: { - title: 'OAS test', - }, - servers: [ - { - url: serverUrl, - }, - ], - paths: { - [path]: { - [method]: operation, - }, - }, - }; - }; -}; diff --git a/packages/api/__tests__/__fixtures__/payloads.oas.json b/packages/api/__tests__/__fixtures__/payloads.oas.json new file mode 100644 index 00000000..64cb6649 --- /dev/null +++ b/packages/api/__tests__/__fixtures__/payloads.oas.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "OAS test cases", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/arraySchema": { + "put": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/primitiveBody": { + "put": { + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} diff --git a/packages/api/__tests__/__fixtures__/security.oas.json b/packages/api/__tests__/__fixtures__/security.oas.json new file mode 100644 index 00000000..b138d727 --- /dev/null +++ b/packages/api/__tests__/__fixtures__/security.oas.json @@ -0,0 +1,240 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "Support for different security types", + "description": "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#securitySchemeObject" + }, + "servers": [ + { + "url": "https://httpbin.org" + } + ], + "paths": { + "/apiKey": { + "get": { + "summary": "`apiKey` auth supplied as query param", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "apiKey_query": [] + } + ] + }, + "post": { + "summary": "`apiKey` auth supplied in cookie", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "apiKey_cookie": [] + } + ] + }, + "put": { + "summary": "`apiKey` auth supplied in header", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "apiKey_header": [] + } + ] + } + }, + "/basic": { + "post": { + "summary": "`basic` auth", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "basic": [] + } + ] + } + }, + "/bearer": { + "post": { + "summary": "`bearer` auth", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "bearer": [] + } + ] + }, + "put": { + "summary": "`bearer` auth with a `jwt` format", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "bearer_jwt": [] + } + ] + } + }, + "/mutualTLS": { + "post": { + "summary": "`mutualTLS` auth", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "mutualTLS": [] + } + ] + } + }, + "/oauth2": { + "post": { + "summary": "`oauth2` auth", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "oauth2": ["write:things"] + } + ] + } + }, + "/openIdConnect": { + "post": { + "summary": "`openIdConnect` auth", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "openIdConnect": [] + } + ] + } + }, + "/no-auth": { + "post": { + "summary": "No auth requirements", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/status/401": { + "post": { + "summary": "Auth required but all auth tokens will fail", + "responses": { + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "apiKey_header": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "apiKey_cookie": { + "type": "apiKey", + "in": "cookie", + "name": "api_key", + "description": "An API key that will be supplied in a named cookie. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" + }, + "apiKey_header": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY", + "description": "An API key that will be supplied in a named header. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" + }, + "apiKey_query": { + "type": "apiKey", + "in": "query", + "name": "apiKey", + "description": "An API key that will be supplied in a named query parameter. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" + }, + "basic": { + "type": "http", + "scheme": "basic", + "description": "Basic auth that takes a base64'd combination of `user:password`. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#basic-authentication-sample" + }, + "bearer": { + "type": "http", + "scheme": "bearer", + "description": "A bearer token that will be supplied within an `Authentication` header as `bearer `. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#basic-authentication-sample" + }, + "bearer_jwt": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "A bearer token that will be supplied within an `Authentication` header as `bearer `. In this case, the format of the token is specified as JWT. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#jwt-bearer-sample" + }, + "mutualTLS": { + "type": "mutualTLS", + "description": "Requires a specific mutual TLS certificate to use when making a HTTP request. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23" + }, + "oauth2": { + "type": "oauth2", + "description": "An OAuth 2 security flow. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23", + "flows": { + "implicit": { + "authorizationUrl": "http://example.com/oauth/dialog", + "scopes": { + "write:things": "Add things to your account" + } + } + } + }, + "oauth2_alternate": { + "type": "oauth2", + "description": "An alternate OAuth 2 security flow. Functions identially to the other `oauth2` scheme, just with alternate URLs to authenticate against. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23", + "flows": { + "implicit": { + "authorizationUrl": "http://alt.example.com/oauth/dialog", + "scopes": { + "write:things": "Add things to your account" + } + } + } + }, + "openIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "https://example.com/.well-known/openid-configuration", + "description": "OpenAPI authentication. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23" + } + } + } +} diff --git a/packages/api/__tests__/auth.test.js b/packages/api/__tests__/auth.test.js index af7455dd..b11727b6 100644 --- a/packages/api/__tests__/auth.test.js +++ b/packages/api/__tests__/auth.test.js @@ -1,99 +1,63 @@ const nock = require('nock'); const api = require('../src'); -const serverUrl = 'https://api.example.com'; -const createOas = require('./__fixtures__/createOas')(serverUrl); +const securityOas = require('./__fixtures__/security.oas.json'); describe('#auth()', () => { - const baseSecurityOas = createOas('get', '/', { - operationId: 'getSomething', - security: [ - { - auth: [], - }, - ], - }); - describe('API Keys', () => { const apiKey = '123457890'; describe('in: query', () => { - const securityOas = { - ...baseSecurityOas, - components: { - securitySchemes: { - auth: { - type: 'apiKey', - name: 'apiKeyParam', - in: 'query', - }, - }, - }, - }; - it.each([ ['should allow you to supply auth', false], ['should allow you to supply auth when unchained from an operation', true], ])('%s', (testCase, chained) => { const sdk = api(securityOas); - const mock = nock(serverUrl).get('/').query({ apiKeyParam: apiKey }).reply(200, {}); + const mock = nock('https://httpbin.org').get('/apiKey').query({ apiKey }).reply(200, {}); if (chained) { return sdk .auth(apiKey) - .getSomething() + .get('/apiKey') .then(() => mock.done()); } sdk.auth(apiKey); - return sdk.getSomething().then(() => mock.done()); + return sdk.get('/apiKey').then(() => mock.done()); }); it('should throw if you supply multiple auth keys', () => { const sdk = api(securityOas); - return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single key is needed/i); + return expect(sdk.auth(apiKey, apiKey).get('/apiKey')).rejects.toThrow(/only a single key is needed/i); }); }); describe('in: header', () => { - const securityOas = { - ...baseSecurityOas, - components: { - securitySchemes: { - auth: { - type: 'apiKey', - name: 'apiKeyHeader', - in: 'header', - }, - }, - }, - }; - it.each([ ['should allow you to supply auth', false], ['should allow you to supply auth when unchained from an operation', true], ])('%s', (testCase, chained) => { const sdk = api(securityOas); - const mock = nock(serverUrl, { reqheaders: { apiKeyHeader: apiKey } }) - .get('/') + const mock = nock('https://httpbin.org', { reqheaders: { 'X-API-KEY': apiKey } }) + .put('/apiKey') .reply(200, {}); if (chained) { return sdk .auth(apiKey) - .getSomething() + .put('/apiKey') .then(() => mock.done()); } sdk.auth(apiKey); - return sdk.getSomething().then(() => mock.done()); + return sdk.put('/apiKey').then(() => mock.done()); }); it('should throw if you supply multiple auth keys', () => { const sdk = api(securityOas); - return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single key is needed/i); + return expect(sdk.auth(apiKey, apiKey).put('/apiKey')).rejects.toThrow(/only a single key is needed/i); }); }); }); @@ -102,33 +66,22 @@ describe('#auth()', () => { describe('scheme: basic', () => { const user = 'username'; const pass = 'changeme'; - const securityOas = { - ...baseSecurityOas, - components: { - securitySchemes: { - auth: { - type: 'http', - scheme: 'basic', - }, - }, - }, - }; it.each([ ['should allow you to supply auth', false], ['should allow you to supply auth when unchained from an operation', true], ])('%s', (testCase, chained) => { const sdk = api(securityOas); - const mock = nock(serverUrl, { + const mock = nock('https://httpbin.org', { reqheaders: { authorization: `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}` }, }) - .get('/') + .post('/basic') .reply(200, { id: 1 }); if (chained) { return sdk .auth(user, pass) - .getSomething() + .post('/basic') .then(res => { // eslint-disable-next-line jest/no-conditional-expect expect(res.id).toBe(1); @@ -137,7 +90,7 @@ describe('#auth()', () => { } sdk.auth(user, pass); - return sdk.getSomething().then(res => { + return sdk.post('/basic').then(res => { expect(res.id).toBe(1); mock.done(); }); @@ -145,98 +98,77 @@ describe('#auth()', () => { it('should allow you to not pass in a password', () => { const sdk = api(securityOas); - const mock = nock(serverUrl, { + const mock = nock('https://httpbin.org', { reqheaders: { authorization: `Basic ${Buffer.from(`${user}:`).toString('base64')}` }, }) - .get('/') + .post('/basic') .reply(200, {}); return sdk .auth(user) - .getSomething() + .post('/basic') .then(() => mock.done()); }); }); describe('scheme: bearer', () => { const apiKey = '123457890'; - const securityOas = { - ...baseSecurityOas, - components: { - securitySchemes: { - auth: { - type: 'http', - scheme: 'bearer', - }, - }, - }, - }; it.each([ ['should allow you to supply auth', false], ['should allow you to supply auth when unchained from an operation', true], ])('%s', (testCase, chained) => { const sdk = api(securityOas); - const mock = nock(serverUrl, { reqheaders: { authorization: `Bearer ${apiKey}` } }) - .get('/') + const mock = nock('https://httpbin.org', { reqheaders: { authorization: `Bearer ${apiKey}` } }) + .post('/bearer') .reply(200, {}); if (chained) { return sdk .auth(apiKey) - .getSomething() + .post('/bearer') .then(() => mock.done()); } sdk.auth(apiKey); - return sdk.getSomething().then(() => mock.done()); + return sdk.post('/bearer').then(() => mock.done()); }); it('should throw if you pass in multiple bearer tokens', () => { const sdk = api(securityOas); - return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single token is needed/i); + return expect(sdk.auth(apiKey, apiKey).post('/bearer')).rejects.toThrow(/only a single token is needed/i); }); }); }); describe('OAuth 2', () => { const apiKey = '123457890'; - const securityOas = { - ...baseSecurityOas, - components: { - securitySchemes: { - auth: { - type: 'oauth2', - }, - }, - }, - }; it.each([ ['should allow you to supply auth', false], ['should allow you to supply auth when unchained from an operation', true], ])('%s', (testCase, chained) => { const sdk = api(securityOas); - const mock = nock(serverUrl, { reqheaders: { authorization: `Bearer ${apiKey}` } }) - .get('/') + const mock = nock('https://httpbin.org', { reqheaders: { authorization: `Bearer ${apiKey}` } }) + .post('/oauth2') .reply(200, {}); if (chained) { return sdk .auth(apiKey) - .getSomething() + .post('/oauth2') .then(() => mock.done()); } sdk.auth(apiKey); - return sdk.getSomething().then(() => mock.done()); + return sdk.post('/oauth2').then(() => mock.done()); }); it('should throw if you pass in multiple bearer tokens', () => { const sdk = api(securityOas); - return expect(sdk.auth(apiKey, apiKey).getSomething()).rejects.toThrow(/only a single token is needed/i); + return expect(sdk.auth(apiKey, apiKey).post('/oauth2')).rejects.toThrow(/only a single token is needed/i); }); }); }); diff --git a/packages/api/__tests__/index.test.js b/packages/api/__tests__/index.test.js index f384bfc9..58a392c9 100644 --- a/packages/api/__tests__/index.test.js +++ b/packages/api/__tests__/index.test.js @@ -10,9 +10,6 @@ const realFs = jest.requireActual('fs').promises; // eslint-disable-next-line global-require jest.mock('fs', () => require('memfs').fs); -const serverUrl = 'https://api.example.com'; -const createOas = require('./__fixtures__/createOas')(serverUrl); - const examplesDir = path.join(__dirname, 'examples'); let petstoreSdk; @@ -93,7 +90,7 @@ describe('#preloading', () => { }); it('should support supplying a raw JSON OAS object', () => { - const sdk = api(createOas()); + const sdk = api(uspto); expect(typeof sdk.get).toBe('function'); }); }); diff --git a/packages/api/__tests__/lib/prepareParams.test.js b/packages/api/__tests__/lib/prepareParams.test.js index bd11d228..40067a60 100644 --- a/packages/api/__tests__/lib/prepareParams.test.js +++ b/packages/api/__tests__/lib/prepareParams.test.js @@ -3,31 +3,10 @@ const Oas = require('oas/tooling'); const $RefParser = require('@apidevtools/json-schema-ref-parser'); const readmeExample = require('@readme/oas-examples/3.0/json/readme.json'); const usptoExample = require('@readme/oas-examples/3.0/json/uspto.json'); +const payloadExamples = require('../__fixtures__/payloads.oas.json'); -const serverUrl = 'https://api.example.com'; -const createOas = require('../__fixtures__/createOas')(serverUrl); const prepareParams = require('../../src/lib/prepareParams'); -const arraySchema = createOas('put', '/', { - requestBody: { - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, -}); - describe('#prepareParams', () => { let readmeSpec; let usptoSpec; @@ -70,19 +49,7 @@ describe('#prepareParams', () => { }); it('should prepare body if body is a primitive', async () => { - const schema = createOas('put', '/', { - requestBody: { - content: { - 'text/plain': { - schema: { - type: 'string', - }, - }, - }, - }, - }); - - const operation = new Oas(schema).operation('/', 'put'); + const operation = new Oas(payloadExamples).operation('/primitiveBody', 'put'); const body = 'Brie cheeseburger ricotta.'; expect(await prepareParams(operation, body, {})).toStrictEqual({ @@ -91,7 +58,7 @@ describe('#prepareParams', () => { }); it('should prepare body if body is an array', async () => { - const operation = new Oas(arraySchema).operation('/', 'put'); + const operation = new Oas(payloadExamples).operation('/arraySchema', 'put'); const body = [ { name: 'Buster', @@ -181,7 +148,7 @@ describe('#prepareParams', () => { }); it('should prepare just a body if supplied argument is an array', async () => { - const operation = new Oas(arraySchema).operation('/', 'put'); + const operation = new Oas(payloadExamples).operation('/arraySchema', 'put'); const body = [ { name: 'Buster', diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 2db45b83..f8602384 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -142,6 +142,8 @@ class Sdk { return new Proxy(sdk, sdkProxy); }, config: opts => { + // Downside to having `opts` be merged into the existing `config` is that there isn't a clean way to reset your + // current config to the default, so having `opts` assigned directly to the existing config should be okay. config = opts; return new Proxy(sdk, sdkProxy); }, From 5b8647dba5d19ca2391ea8abe59857fd83d92d91 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 12 May 2021 11:56:24 -0700 Subject: [PATCH 3/8] test: upgrading oas-examples to pull in the new security example --- .../__tests__/__fixtures__/security.oas.json | 240 ------------------ packages/api/__tests__/auth.test.js | 2 +- packages/api/package-lock.json | 14 +- packages/api/package.json | 2 +- 4 files changed, 9 insertions(+), 249 deletions(-) delete mode 100644 packages/api/__tests__/__fixtures__/security.oas.json diff --git a/packages/api/__tests__/__fixtures__/security.oas.json b/packages/api/__tests__/__fixtures__/security.oas.json deleted file mode 100644 index b138d727..00000000 --- a/packages/api/__tests__/__fixtures__/security.oas.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "version": "1.0.0", - "title": "Support for different security types", - "description": "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#securitySchemeObject" - }, - "servers": [ - { - "url": "https://httpbin.org" - } - ], - "paths": { - "/apiKey": { - "get": { - "summary": "`apiKey` auth supplied as query param", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "apiKey_query": [] - } - ] - }, - "post": { - "summary": "`apiKey` auth supplied in cookie", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "apiKey_cookie": [] - } - ] - }, - "put": { - "summary": "`apiKey` auth supplied in header", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "apiKey_header": [] - } - ] - } - }, - "/basic": { - "post": { - "summary": "`basic` auth", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "basic": [] - } - ] - } - }, - "/bearer": { - "post": { - "summary": "`bearer` auth", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "bearer": [] - } - ] - }, - "put": { - "summary": "`bearer` auth with a `jwt` format", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "bearer_jwt": [] - } - ] - } - }, - "/mutualTLS": { - "post": { - "summary": "`mutualTLS` auth", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "mutualTLS": [] - } - ] - } - }, - "/oauth2": { - "post": { - "summary": "`oauth2` auth", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "oauth2": ["write:things"] - } - ] - } - }, - "/openIdConnect": { - "post": { - "summary": "`openIdConnect` auth", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "openIdConnect": [] - } - ] - } - }, - "/no-auth": { - "post": { - "summary": "No auth requirements", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/status/401": { - "post": { - "summary": "Auth required but all auth tokens will fail", - "responses": { - "401": { - "description": "Unauthorized" - } - }, - "security": [ - { - "apiKey_header": [] - } - ] - } - } - }, - "components": { - "securitySchemes": { - "apiKey_cookie": { - "type": "apiKey", - "in": "cookie", - "name": "api_key", - "description": "An API key that will be supplied in a named cookie. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" - }, - "apiKey_header": { - "type": "apiKey", - "in": "header", - "name": "X-API-KEY", - "description": "An API key that will be supplied in a named header. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" - }, - "apiKey_query": { - "type": "apiKey", - "in": "query", - "name": "apiKey", - "description": "An API key that will be supplied in a named query parameter. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object" - }, - "basic": { - "type": "http", - "scheme": "basic", - "description": "Basic auth that takes a base64'd combination of `user:password`. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#basic-authentication-sample" - }, - "bearer": { - "type": "http", - "scheme": "bearer", - "description": "A bearer token that will be supplied within an `Authentication` header as `bearer `. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#basic-authentication-sample" - }, - "bearer_jwt": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "A bearer token that will be supplied within an `Authentication` header as `bearer `. In this case, the format of the token is specified as JWT. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#jwt-bearer-sample" - }, - "mutualTLS": { - "type": "mutualTLS", - "description": "Requires a specific mutual TLS certificate to use when making a HTTP request. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23" - }, - "oauth2": { - "type": "oauth2", - "description": "An OAuth 2 security flow. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23", - "flows": { - "implicit": { - "authorizationUrl": "http://example.com/oauth/dialog", - "scopes": { - "write:things": "Add things to your account" - } - } - } - }, - "oauth2_alternate": { - "type": "oauth2", - "description": "An alternate OAuth 2 security flow. Functions identially to the other `oauth2` scheme, just with alternate URLs to authenticate against. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23", - "flows": { - "implicit": { - "authorizationUrl": "http://alt.example.com/oauth/dialog", - "scopes": { - "write:things": "Add things to your account" - } - } - } - }, - "openIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "https://example.com/.well-known/openid-configuration", - "description": "OpenAPI authentication. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields-23" - } - } - } -} diff --git a/packages/api/__tests__/auth.test.js b/packages/api/__tests__/auth.test.js index b11727b6..0d89dbdb 100644 --- a/packages/api/__tests__/auth.test.js +++ b/packages/api/__tests__/auth.test.js @@ -1,7 +1,7 @@ const nock = require('nock'); const api = require('../src'); -const securityOas = require('./__fixtures__/security.oas.json'); +const securityOas = require('@readme/oas-examples/3.0/json/security.json'); describe('#auth()', () => { describe('API Keys', () => { diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index c8eb881b..fdfaa35a 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.0.0", + "@readme/oas-examples": "^4.1.0", "eslint": "^7.6.0", "jest": "^26.0.1", "memfs": "^3.2.0", @@ -1256,9 +1256,9 @@ } }, "node_modules/@readme/oas-examples": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.0.0.tgz", - "integrity": "sha512-MSm5mJoNa9AJ6PfKFOSO4BBzC3oQn0CdJHfP5l6v1ATVX3LUy/+X2Xgx5RZNcefRkJ7TWlfPoCiphjiEDSaAVQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.1.0.tgz", + "integrity": "sha512-Ye82inHJkDlpuE7bcLXUqrxwVtrtncAfEf/8PScHWeIYCTqkivB5zOvazlHSUYhOtbnGLGIAXVcfRDJaVIC8sA==", "dev": true }, "node_modules/@readme/oas-extensions": { @@ -12080,9 +12080,9 @@ } }, "@readme/oas-examples": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.0.0.tgz", - "integrity": "sha512-MSm5mJoNa9AJ6PfKFOSO4BBzC3oQn0CdJHfP5l6v1ATVX3LUy/+X2Xgx5RZNcefRkJ7TWlfPoCiphjiEDSaAVQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.1.0.tgz", + "integrity": "sha512-Ye82inHJkDlpuE7bcLXUqrxwVtrtncAfEf/8PScHWeIYCTqkivB5zOvazlHSUYhOtbnGLGIAXVcfRDJaVIC8sA==", "dev": true }, "@readme/oas-extensions": { diff --git a/packages/api/package.json b/packages/api/package.json index 304546ae..546957d8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.0.0", + "@readme/oas-examples": "^4.1.0", "eslint": "^7.6.0", "jest": "^26.0.1", "memfs": "^3.2.0", From 10b8a6ce4ad9fe084aa5f91d652b783c3540e574 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 12 May 2021 15:54:38 -0700 Subject: [PATCH 4/8] feat: adding support for server variables to `api` --- package-lock.json | 17 ++--- packages/api/__tests__/config.test.js | 94 ++++++++++++++++++++++----- packages/api/package-lock.json | 14 ++-- packages/api/package.json | 2 +- packages/api/src/index.js | 15 ++++- packages/api/src/lib/index.js | 6 +- packages/api/src/lib/prepareServer.js | 66 +++++++++++++++++++ 7 files changed, 177 insertions(+), 37 deletions(-) create mode 100644 packages/api/src/lib/prepareServer.js diff --git a/package-lock.json b/package-lock.json index ebc714ef..507ff9d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "name": "api-monorepo", + "hasInstallScript": true, "workspaces": [ "./packages/*" ], @@ -4256,9 +4257,9 @@ } }, "node_modules/@readme/oas-examples": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.0.0.tgz", - "integrity": "sha512-MSm5mJoNa9AJ6PfKFOSO4BBzC3oQn0CdJHfP5l6v1ATVX3LUy/+X2Xgx5RZNcefRkJ7TWlfPoCiphjiEDSaAVQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.2.0.tgz", + "integrity": "sha512-WS/Yb4yXM2ZlOFOA+vyRO/3MxFZvu2os742dUZ996R0x57bD47i/IP8N0PkkQFEujStrl7dMpPPi3GWoyBdVPQ==", "dev": true }, "node_modules/@readme/oas-extensions": { @@ -20488,7 +20489,7 @@ }, "devDependencies": { "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.0.0", + "@readme/oas-examples": "^4.2.0", "eslint": "^7.6.0", "jest": "^26.0.1", "memfs": "^3.2.0", @@ -23943,9 +23944,9 @@ } }, "@readme/oas-examples": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.0.0.tgz", - "integrity": "sha512-MSm5mJoNa9AJ6PfKFOSO4BBzC3oQn0CdJHfP5l6v1ATVX3LUy/+X2Xgx5RZNcefRkJ7TWlfPoCiphjiEDSaAVQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.2.0.tgz", + "integrity": "sha512-WS/Yb4yXM2ZlOFOA+vyRO/3MxFZvu2os742dUZ996R0x57bD47i/IP8N0PkkQFEujStrl7dMpPPi3GWoyBdVPQ==", "dev": true }, "@readme/oas-extensions": { @@ -24542,7 +24543,7 @@ "@apidevtools/json-schema-ref-parser": "^9.0.1", "@apidevtools/swagger-parser": "^10.0.1", "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.0.0", + "@readme/oas-examples": "^4.2.0", "@readme/oas-to-har": "^13.4.5", "datauri": "^3.0.0", "eslint": "^7.6.0", diff --git a/packages/api/__tests__/config.test.js b/packages/api/__tests__/config.test.js index 0de6ac1e..3724aacf 100644 --- a/packages/api/__tests__/config.test.js +++ b/packages/api/__tests__/config.test.js @@ -2,29 +2,89 @@ const nock = require('nock'); const api = require('../src'); const { Response } = require('node-fetch'); -const serverUrl = 'https://api.example.com'; -const createOas = require('./__fixtures__/createOas')(serverUrl); +const petstore = require('@readme/oas-examples/3.0/json/petstore.json'); +const serverVariables = require('@readme/oas-examples/3.0/json/server-variables.json'); + +let sdk; +const petId = 123; +const response = { + id: petId, + name: 'Buster', +}; describe('#config()', () => { + describe('server variables', () => { + beforeEach(() => { + sdk = api(serverVariables); + }); + + it('should use server variable defaults if no server or variables are supplied', () => { + const mock = nock('https://demo.example.com:443/v2/').post('/').reply(200, response); + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it('should support supplying a full server url', () => { + const mock = nock('https://buster.example.com:3000/v14').post('/').reply(200, response); + + sdk.config({ server: 'https://buster.example.com:3000/v14' }); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it('should support supplying a full server url within the `server` object', () => { + const mock = nock('http://dev.local/v14').post('/').reply(200, response); + + sdk.config({ + server: { + url: 'http://dev.local/v14', + }, + }); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it('should support supplying a server url with server variables', () => { + const mock = nock('http://dev.local/v14').post('/').reply(200, response); + + sdk.config({ + server: { + url: 'http://{name}.local/{basePath}', + name: 'dev', + basePath: 'v14', + }, + }); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it.todo('should be able to supply a url on an OAS that has no servers defined'); + + it.todo("should be able to supply a url that doesn't match any defined server"); + }); + describe('parseResponse', () => { - const petId = 123; - const response = { - id: petId, - name: 'Buster', - }; - - const sdk = api( - createOas('delete', `/pets/${petId}`, { - operationId: 'deletePet', - }) - ); + beforeEach(() => { + sdk = api(petstore); + }); it('should give access to the Response object if `parseResponse` is `false`', () => { - const mock = nock(serverUrl).delete(`/pets/${petId}`).reply(200, response); + const mock = nock('http://petstore.swagger.io/v2').delete(`/pet/${petId}`).reply(200, response); sdk.config({ parseResponse: false }); - return sdk.deletePet({ id: petId }).then(async res => { + return sdk.deletePet({ petId }).then(async res => { expect(res instanceof Response).toBe(true); expect(res.status).toStrictEqual(200); expect(await res.json()).toStrictEqual(response); @@ -33,11 +93,11 @@ describe('#config()', () => { }); it('should parse the response if `parseResponse` is `undefined`', () => { - const mock = nock(serverUrl).delete(`/pets/${petId}`).reply(200, response); + const mock = nock('http://petstore.swagger.io/v2').delete(`/pet/${petId}`).reply(200, response); sdk.config({ unrecognizedConfigParameter: false }); - return sdk.deletePet({ id: petId }).then(res => { + return sdk.deletePet({ petId }).then(res => { expect(res instanceof Response).toBe(false); expect(res).toStrictEqual(response); mock.done(); diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index fdfaa35a..9ae7facb 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.1.0", + "@readme/oas-examples": "^4.2.0", "eslint": "^7.6.0", "jest": "^26.0.1", "memfs": "^3.2.0", @@ -1256,9 +1256,9 @@ } }, "node_modules/@readme/oas-examples": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.1.0.tgz", - "integrity": "sha512-Ye82inHJkDlpuE7bcLXUqrxwVtrtncAfEf/8PScHWeIYCTqkivB5zOvazlHSUYhOtbnGLGIAXVcfRDJaVIC8sA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.2.0.tgz", + "integrity": "sha512-WS/Yb4yXM2ZlOFOA+vyRO/3MxFZvu2os742dUZ996R0x57bD47i/IP8N0PkkQFEujStrl7dMpPPi3GWoyBdVPQ==", "dev": true }, "node_modules/@readme/oas-extensions": { @@ -12080,9 +12080,9 @@ } }, "@readme/oas-examples": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.1.0.tgz", - "integrity": "sha512-Ye82inHJkDlpuE7bcLXUqrxwVtrtncAfEf/8PScHWeIYCTqkivB5zOvazlHSUYhOtbnGLGIAXVcfRDJaVIC8sA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@readme/oas-examples/-/oas-examples-4.2.0.tgz", + "integrity": "sha512-WS/Yb4yXM2ZlOFOA+vyRO/3MxFZvu2os742dUZ996R0x57bD47i/IP8N0PkkQFEujStrl7dMpPPi3GWoyBdVPQ==", "dev": true }, "@readme/oas-extensions": { diff --git a/packages/api/package.json b/packages/api/package.json index 546957d8..5a7ed355 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@readme/eslint-config": "^5.0.5", - "@readme/oas-examples": "^4.1.0", + "@readme/oas-examples": "^4.2.0", "eslint": "^7.6.0", "jest": "^26.0.1", "memfs": "^3.2.0", diff --git a/packages/api/src/index.js b/packages/api/src/index.js index f8602384..e940274d 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -5,7 +5,7 @@ const oasToHar = require('@readme/oas-to-har'); const pkg = require('../package.json'); const Cache = require('./cache'); -const { prepareAuth, prepareParams, parseResponse } = require('./lib/index'); +const { parseResponse, prepareAuth, prepareParams, prepareServer } = require('./lib/index'); global.fetch = fetch; global.Request = fetch.Request; @@ -42,7 +42,18 @@ class Sdk { return new Promise(resolve => { resolve(prepareParams(operation, body, metadata)); }).then(params => { - const har = oasToHar(spec, operation, params, prepareAuth(authKeys, operation)); + const data = { ...params }; + + // If `server` has been passed into the `config` method then we need to do some extra work to figure out how to + // use that supplied server, and also handle any server variables that were supplied alongside it. + if (config.server) { + const server = prepareServer(config, spec); + if (server) { + data.server = server; + } + } + + const har = oasToHar(spec, operation, data, prepareAuth(authKeys, operation)); return fetchHar(har, self.userAgent).then(res => { if (res.status >= 400 && res.status <= 599) { diff --git a/packages/api/src/lib/index.js b/packages/api/src/lib/index.js index 3ff1c716..59d8d852 100644 --- a/packages/api/src/lib/index.js +++ b/packages/api/src/lib/index.js @@ -1,9 +1,11 @@ +const parseResponse = require('./parseResponse'); const prepareAuth = require('./prepareAuth'); const prepareParams = require('./prepareParams'); -const parseResponse = require('./parseResponse'); +const prepareServer = require('./prepareServer'); module.exports = { + parseResponse, prepareAuth, prepareParams, - parseResponse, + prepareServer, }; diff --git a/packages/api/src/lib/prepareServer.js b/packages/api/src/lib/prepareServer.js new file mode 100644 index 00000000..ba1fdbbf --- /dev/null +++ b/packages/api/src/lib/prepareServer.js @@ -0,0 +1,66 @@ +/** + * With an SDK config object and an instance of OAS we should extract and prepare the server and any server variables + * to be supplied to `@readme/oas-to-har`. + * + * @param {Object} config + * @param {Oas} spec + * @returns {Object|Boolean} + */ +module.exports = (config, spec) => { + function stripTrailingSlash(url) { + if (url[url.length - 1] === '/') { + return url.slice(0, -1); + } + + return url; + } + + if (typeof config.server === 'string') { + const server = spec.splitVariables(config.server); + if (server) { + return { + selected: server.selected, + variables: server.variables, + }; + } + + // @todo we should pass `config.server` directly into `@readme/oas-to-har` as the base URL + } else if (typeof config.server === 'object') { + if ('url' in config.server) { + // eslint-disable-next-line prefer-const + let { url, ...variables } = config.server; + url = stripTrailingSlash(url); + + let serverIdx; + (spec.servers || []).forEach((server, i) => { + if (server.url === url) { + serverIdx = i; + } + }); + + // If we were able to find the passed in server in the OAS servers, we should use that! If we couldn't + // and server variables were passed in we should try our best to handle that, otherwise we should ignore + // the passed in server and use whever the default from the OAS is. + if (serverIdx) { + return { + selected: serverIdx, + variables, + }; + } else if (Object.keys(variables).length) { + // @todo we should run `oas.replaceUrl(url)` and pass that unto `@readme/oas-to-har` + } else { + const server = spec.splitVariables(config.server.url); + if (server) { + return { + selected: server.selected, + variables: server.variables, + }; + } + + // @todo we should pass `config.server.url` directly into `@readme/oas-to-har` as the base URL + } + } + } + + return false; +}; From 01ef50f6f10f9b22cf87a3176af8b47fe223d645 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 12 May 2021 16:05:29 -0700 Subject: [PATCH 5/8] docs: updating the readme with docs on server variables --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 73658e59..e5111051 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,30 @@ sdk.get('/pets/{petId}', { petId: 1234 }).then(...) The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests. +### Server configurations + +If the API you're using offers alternate server URLs and server variables in its [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject) definition you can supply this to the SDK with `.config()`: + +```js +sdk.config({ + server: { + url: 'https://{region}.api.example.com/{basePath}', + name: 'eu', + basePath: 'v14', + }, +}); + +sdk.get('/pets').then(...) +``` + +When your request is executed it will be made to `https://eu.api.example.com/v14/pets`. Alternatively if you don't want to deal with URL templates you can opt to pass the full URL in instead: + +```js +sdk.config({ + server: 'https://eu.api.example.com/v14' +}); +``` + ## How does it work? Behind the scenes, `api` will: From 9802e85d0656a9c70bae7506aa98c7cf98105c21 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 12 May 2021 16:06:47 -0700 Subject: [PATCH 6/8] docs: adding server docs to the TOC --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e5111051..98a2c029 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Automatic SDK generation from an OpenAPI definition. * [Authentication](#authentication) * [Parameters and Payloads](#parameters-and-payloads) * [HTTP requests](#http-requests) + * [Server configurations](#server-configurations) * [How does it work?](#how-does-it-work) * [Interested in contributing?](#interested-in-contributing) * [FAQ](#faq) From df625a1ebe08a8df85835a22aad483a66994bd39 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 13 May 2021 10:32:40 -0700 Subject: [PATCH 7/8] feat: moving server configs to be used with `sdk.server()` --- README.md | 23 ++++---- packages/api/README.md | 34 ++++++------ packages/api/__tests__/config.test.js | 62 --------------------- packages/api/__tests__/index.test.js | 3 +- packages/api/__tests__/server.test.js | 55 +++++++++++++++++++ packages/api/src/index.js | 19 ++++--- packages/api/src/lib/prepareServer.js | 78 +++++++++++---------------- 7 files changed, 132 insertions(+), 142 deletions(-) create mode 100644 packages/api/__tests__/server.test.js diff --git a/README.md b/README.md index 98a2c029..4060eb4f 100644 --- a/README.md +++ b/README.md @@ -99,16 +99,12 @@ sdk.get('/pets/{petId}', { petId: 1234 }).then(...) The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests. ### Server configurations - -If the API you're using offers alternate server URLs and server variables in its [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject) definition you can supply this to the SDK with `.config()`: +If the API you're using offers alternate server URLs and server variables in its [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject) definition you can supply this to the SDK with `.server()`: ```js -sdk.config({ - server: { - url: 'https://{region}.api.example.com/{basePath}', - name: 'eu', - basePath: 'v14', - }, +sdk.server('https://{region}.api.example.com/{basePath}', { + name: 'eu', + basePath: 'v14', }); sdk.get('/pets').then(...) @@ -117,9 +113,7 @@ sdk.get('/pets').then(...) When your request is executed it will be made to `https://eu.api.example.com/v14/pets`. Alternatively if you don't want to deal with URL templates you can opt to pass the full URL in instead: ```js -sdk.config({ - server: 'https://eu.api.example.com/v14' -}); +sdk.server('https://eu.api.example.com/v14'); ``` ## How does it work? @@ -160,3 +154,10 @@ Not yet! The URL that you give the module must be publicy accessible. If it isn' ```js const sdk = require('api')('/path/to/downloaded.json'); ``` + +#### How do I access the Response object (for status and headers)? +By default we parse the response based on the `content-type` header for you. You can disable this by doing the following: + +```js +sdk.config({ parseResponse: false }); +``` diff --git a/packages/api/README.md b/packages/api/README.md index 565dba34..4060eb4f 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -9,6 +9,7 @@ Automatic SDK generation from an OpenAPI definition. * [Authentication](#authentication) * [Parameters and Payloads](#parameters-and-payloads) * [HTTP requests](#http-requests) + * [Server configurations](#server-configurations) * [How does it work?](#how-does-it-work) * [Interested in contributing?](#interested-in-contributing) * [FAQ](#faq) @@ -24,7 +25,7 @@ Using `api` is as simple as supplying it an OpenAPI and using the SDK as you wou ```js const sdk = require('api')('https://raw.githubusercontent.com/readmeio/oas/master/packages/examples/3.0/json/petstore.json'); -sdk.listPets().then(res => { +sdk.listPets().then(res => res.json()).then(res => { console.log(`My pets name is ${res[0].name}!`); }); ``` @@ -88,30 +89,33 @@ You can also give it a stream and it'll handle all of the hard work for you. sdk.uploadFile({ file: fs.createReadStream('/path/to/a/file.txt') }).then(...) ``` -### Responses -Since we know the `Content-Type` of the returned response, we automatically parse it for you before returning it. So no more superfluous `.then(res => res.json())` calls. If your API returned with JSON, we'll give you the parsed JSON. +### HTTP requests +If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead: + +```js +sdk.get('/pets/{petId}', { petId: 1234 }).then(...) +``` + +The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests. -If you need access to the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object you can disable our automatic parsing using `.config()` like so: +### Server configurations +If the API you're using offers alternate server URLs and server variables in its [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject) definition you can supply this to the SDK with `.server()`: ```js -sdk.config({ parseResponse: false }); +sdk.server('https://{region}.api.example.com/{basePath}', { + name: 'eu', + basePath: 'v14', +}); -sdk.createPets({ name: 'Buster' }) - .then(res => { - // `res` will be a Response object - // so you can access status and headers - }) +sdk.get('/pets').then(...) ``` -### HTTP requests -If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead: +When your request is executed it will be made to `https://eu.api.example.com/v14/pets`. Alternatively if you don't want to deal with URL templates you can opt to pass the full URL in instead: ```js -sdk.get('/pets/{petId}', { petId: 1234 }).then(...) +sdk.server('https://eu.api.example.com/v14'); ``` -The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests. - ## How does it work? Behind the scenes, `api` will: diff --git a/packages/api/__tests__/config.test.js b/packages/api/__tests__/config.test.js index 3724aacf..e436f261 100644 --- a/packages/api/__tests__/config.test.js +++ b/packages/api/__tests__/config.test.js @@ -3,7 +3,6 @@ const api = require('../src'); const { Response } = require('node-fetch'); const petstore = require('@readme/oas-examples/3.0/json/petstore.json'); -const serverVariables = require('@readme/oas-examples/3.0/json/server-variables.json'); let sdk; const petId = 123; @@ -13,67 +12,6 @@ const response = { }; describe('#config()', () => { - describe('server variables', () => { - beforeEach(() => { - sdk = api(serverVariables); - }); - - it('should use server variable defaults if no server or variables are supplied', () => { - const mock = nock('https://demo.example.com:443/v2/').post('/').reply(200, response); - return sdk.post('/').then(res => { - expect(res).toStrictEqual(response); - mock.done(); - }); - }); - - it('should support supplying a full server url', () => { - const mock = nock('https://buster.example.com:3000/v14').post('/').reply(200, response); - - sdk.config({ server: 'https://buster.example.com:3000/v14' }); - - return sdk.post('/').then(res => { - expect(res).toStrictEqual(response); - mock.done(); - }); - }); - - it('should support supplying a full server url within the `server` object', () => { - const mock = nock('http://dev.local/v14').post('/').reply(200, response); - - sdk.config({ - server: { - url: 'http://dev.local/v14', - }, - }); - - return sdk.post('/').then(res => { - expect(res).toStrictEqual(response); - mock.done(); - }); - }); - - it('should support supplying a server url with server variables', () => { - const mock = nock('http://dev.local/v14').post('/').reply(200, response); - - sdk.config({ - server: { - url: 'http://{name}.local/{basePath}', - name: 'dev', - basePath: 'v14', - }, - }); - - return sdk.post('/').then(res => { - expect(res).toStrictEqual(response); - mock.done(); - }); - }); - - it.todo('should be able to supply a url on an OAS that has no servers defined'); - - it.todo("should be able to supply a url that doesn't match any defined server"); - }); - describe('parseResponse', () => { beforeEach(() => { sdk = api(petstore); diff --git a/packages/api/__tests__/index.test.js b/packages/api/__tests__/index.test.js index 58a392c9..f1aa1fb3 100644 --- a/packages/api/__tests__/index.test.js +++ b/packages/api/__tests__/index.test.js @@ -59,7 +59,7 @@ describe('#preloading', () => { // SDK should still not be loaded since we haven't officially called it yet. expect(new Cache(uspto).isCached()).toBe(false); - expect(Object.keys(sdk)).toStrictEqual(['auth', 'config']); + expect(Object.keys(sdk)).toStrictEqual(['auth', 'config', 'server']); await sdk.get('/').then(() => { mock.done(); @@ -70,6 +70,7 @@ describe('#preloading', () => { expect(Object.keys(sdk)).toStrictEqual([ 'auth', 'config', + 'server', 'get', 'put', 'post', diff --git a/packages/api/__tests__/server.test.js b/packages/api/__tests__/server.test.js new file mode 100644 index 00000000..b8071185 --- /dev/null +++ b/packages/api/__tests__/server.test.js @@ -0,0 +1,55 @@ +const nock = require('nock'); +const api = require('../src'); + +const serverVariables = require('@readme/oas-examples/3.0/json/server-variables.json'); + +let sdk; +const petId = 123; +const response = { + id: petId, + name: 'Buster', +}; + +describe('#server()', () => { + beforeEach(() => { + sdk = api(serverVariables); + }); + + it('should use server variable defaults if no server or variables are supplied', () => { + const mock = nock('https://demo.example.com:443/v2/').post('/').reply(200, response); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it('should support supplying a full server url', () => { + const mock = nock('https://buster.example.com:3000/v14').post('/').reply(200, response); + + sdk.server('https://buster.example.com:3000/v14'); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it('should support supplying a server url with server variables', () => { + const mock = nock('http://dev.local/v14').post('/').reply(200, response); + + sdk.server('http://{name}.local/{basePath}', { + name: 'dev', + basePath: 'v14', + }); + + return sdk.post('/').then(res => { + expect(res).toStrictEqual(response); + mock.done(); + }); + }); + + it.todo('should be able to supply a url on an OAS that has no servers defined'); + + it.todo("should be able to supply a url that doesn't match any defined server"); +}); diff --git a/packages/api/src/index.js b/packages/api/src/index.js index e940274d..33f6fa26 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -33,6 +33,7 @@ class Sdk { const cache = new Cache(this.uri); const self = this; let config = { parseResponse: true }; + let server = false; let isLoaded = false; let isCached = cache.isCached(); @@ -44,12 +45,12 @@ class Sdk { }).then(params => { const data = { ...params }; - // If `server` has been passed into the `config` method then we need to do some extra work to figure out how to - // use that supplied server, and also handle any server variables that were supplied alongside it. - if (config.server) { - const server = prepareServer(config, spec); - if (server) { - data.server = server; + // If `sdk.server()` has been issued data then we need to do some extra work to figure out how to use that + // supplied server, and also handle any server variables that were sent alongside it. + if (server) { + const preparedServer = prepareServer(spec, server.url, server.variables); + if (preparedServer) { + data.server = preparedServer; } } @@ -158,6 +159,12 @@ class Sdk { config = opts; return new Proxy(sdk, sdkProxy); }, + server: (url, variables = {}) => { + server = { + url, + variables, + }; + }, }; return new Proxy(sdk, sdkProxy); diff --git a/packages/api/src/lib/prepareServer.js b/packages/api/src/lib/prepareServer.js index ba1fdbbf..50b44e57 100644 --- a/packages/api/src/lib/prepareServer.js +++ b/packages/api/src/lib/prepareServer.js @@ -1,22 +1,41 @@ +function stripTrailingSlash(url) { + if (url[url.length - 1] === '/') { + return url.slice(0, -1); + } + + return url; +} + /** - * With an SDK config object and an instance of OAS we should extract and prepare the server and any server variables + * With an SDK server config and an instance of OAS we should extract and prepare the server and any server variables * to be supplied to `@readme/oas-to-har`. * - * @param {Object} config * @param {Oas} spec + * @param {String} url + * @param {Object} variables * @returns {Object|Boolean} */ -module.exports = (config, spec) => { - function stripTrailingSlash(url) { - if (url[url.length - 1] === '/') { - return url.slice(0, -1); +module.exports = (spec, url, variables = {}) => { + let serverIdx; + const sanitizedUrl = stripTrailingSlash(url); + (spec.servers || []).forEach((server, i) => { + if (server.url === sanitizedUrl) { + serverIdx = i; } + }); - return url; - } - - if (typeof config.server === 'string') { - const server = spec.splitVariables(config.server); + // If we were able to find the passed in server in the OAS servers, we should use that! If we couldn't + // and server variables were passed in we should try our best to handle that, otherwise we should ignore + // the passed in server and use whever the default from the OAS is. + if (serverIdx) { + return { + selected: serverIdx, + variables, + }; + } else if (Object.keys(variables).length) { + // @todo we should run `oas.replaceUrl(url)` and pass that unto `@readme/oas-to-har` + } else { + const server = spec.splitVariables(url); if (server) { return { selected: server.selected, @@ -24,42 +43,7 @@ module.exports = (config, spec) => { }; } - // @todo we should pass `config.server` directly into `@readme/oas-to-har` as the base URL - } else if (typeof config.server === 'object') { - if ('url' in config.server) { - // eslint-disable-next-line prefer-const - let { url, ...variables } = config.server; - url = stripTrailingSlash(url); - - let serverIdx; - (spec.servers || []).forEach((server, i) => { - if (server.url === url) { - serverIdx = i; - } - }); - - // If we were able to find the passed in server in the OAS servers, we should use that! If we couldn't - // and server variables were passed in we should try our best to handle that, otherwise we should ignore - // the passed in server and use whever the default from the OAS is. - if (serverIdx) { - return { - selected: serverIdx, - variables, - }; - } else if (Object.keys(variables).length) { - // @todo we should run `oas.replaceUrl(url)` and pass that unto `@readme/oas-to-har` - } else { - const server = spec.splitVariables(config.server.url); - if (server) { - return { - selected: server.selected, - variables: server.variables, - }; - } - - // @todo we should pass `config.server.url` directly into `@readme/oas-to-har` as the base URL - } - } + // @todo we should pass `url` directly into `@readme/oas-to-har` as the base URL } return false; From c15d5e952df4f3f683436bce6c160484e5900010 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 13 May 2021 10:42:43 -0700 Subject: [PATCH 8/8] fix: moving snippets over to use `sdk.server()` --- .../__tests__/__fixtures__/output/alternate-server.js | 2 +- packages/httpsnippet-client-api/src/index.js | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js index 9a7af611..cfa267ee 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/alternate-server.js @@ -1,7 +1,7 @@ const sdk = require('api')('https://example.com/openapi.json'); sdk.auth('123'); -sdk.config({server: 'http://dev.local/v2'}); +sdk.server('http://dev.local/v2'); sdk.create({foo: 'bar', hello: 'world'}, {id: '1234'}) .then(res => console.log(res)) .catch(err => console.error(err)); diff --git a/packages/httpsnippet-client-api/src/index.js b/packages/httpsnippet-client-api/src/index.js index e4410159..1d23f9d4 100644 --- a/packages/httpsnippet-client-api/src/index.js +++ b/packages/httpsnippet-client-api/src/index.js @@ -116,15 +116,10 @@ module.exports = function (source, options) { const stockUrl = oas.url(); const baseUrl = source.url.replace(path, ''); if (baseUrl !== stockUrl) { - const config = {}; const serverVars = oas.splitVariables(baseUrl); - if (serverVars) { - config.server = oas.url(serverVars.selected, serverVars.variables); - } else { - config.server = baseUrl; - } + const serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl; - configData.push(`sdk.config(${stringify(config, { inlineCharacterLimit: 40 })});`); + configData.push(`sdk.server('${serverUrl}');`); } }