From 6a151d996031d2d2be6f2ff155cce0cdf26dc84a Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Fri, 13 Sep 2024 15:45:55 +0200 Subject: [PATCH] fix(execute): do not encode server variables by default This will allow Saas and On-Premise usecase to be handled correctly. Encoding can be turned on again by providing serverVariableEncoder option. Refs #3656 --- docs/usage/http-client-for-oas-operations.md | 1 + package-lock.json | 1 + package.json | 1 + src/execute/index.js | 25 ++- test/execute/server-variable-encoder.js | 157 +++++++++++++++++++ 5 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 test/execute/server-variable-encoder.js diff --git a/docs/usage/http-client-for-oas-operations.md b/docs/usage/http-client-for-oas-operations.md index f703eea63..10e1957bd 100644 --- a/docs/usage/http-client-for-oas-operations.md +++ b/docs/usage/http-client-for-oas-operations.md @@ -28,6 +28,7 @@ Property | Description `userFetch` | `Function=cross-fetch`. Custom **asynchronous** fetch function that accepts two arguments: the `url` and the `Request` object and must return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object. More info in [HTTP Client](http-client.md) documentation. `signal` | `AbortSignal=null`. AbortSignal object instance, which can be used to abort a request as desired. `server` | `String`. URL (`https://example.com`) or relative URI Reference (`/path/subpath`). Must match with of the defined `Server Objects`. If matched, it will be prepended to every requested path. +`serverVariableEncoder` | `Function=identity`. An encoder function that is run on a server variable before substituted to the URL template. `contextUrl` | `String`. URL, e.g. `https://example.com`. Used in following situations:

If `server` option is not matched and there is no `Server Object` defined in the definition, this URL will be prepended to every requested path.

If matched `Server Object` is defined as relative URI Reference its `url` fixed field is resolved against `contenxtUrl`. Resolved URL will be prepended to every requested path. For all later references, we will always use following OpenAPI 3.0.0 definition when referring diff --git a/package-lock.json b/package-lock.json index f3ec18485..94a3a2a22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "node-fetch-commonjs": "^3.3.2", "openapi-path-templating": "^1.5.1", "openapi-server-url-templating": "^1.0.0", + "ramda": "^0.30.1", "ramda-adjunct": "^5.0.0" }, "devDependencies": { diff --git a/package.json b/package.json index cdc081a09..c99f6cee3 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "node-fetch-commonjs": "^3.3.2", "openapi-path-templating": "^1.5.1", "openapi-server-url-templating": "^1.0.0", + "ramda": "^0.30.1", "ramda-adjunct": "^5.0.0", "neotraverse": "=0.6.18" }, diff --git a/src/execute/index.js b/src/execute/index.js index 00571f429..3ceb48300 100755 --- a/src/execute/index.js +++ b/src/execute/index.js @@ -1,4 +1,5 @@ import cookie from 'cookie'; +import { identity } from 'ramda'; import { isPlainObject } from 'ramda-adjunct'; import { test as testServerURLTemplate, @@ -129,6 +130,7 @@ export function buildRequest(options) { serverVariables, http, signal, + serverVariableEncoder, } = options; let { parameters, parameterBuilders } = options; @@ -183,6 +185,7 @@ export function buildRequest(options) { serverVariables, pathName, method, + serverVariableEncoder, }); req.url += baseURL; @@ -321,7 +324,15 @@ export function baseUrl(obj) { const isNonEmptyServerList = (value) => Array.isArray(value) && value.length > 0; -function oas3BaseUrl({ spec, pathName, method, server, contextUrl, serverVariables = {} }) { +function oas3BaseUrl({ + spec, + pathName, + method, + server, + contextUrl, + serverVariables = {}, + serverVariableEncoder, +}) { let servers = []; let selectedServerUrl = ''; let selectedServerObj; @@ -359,10 +370,14 @@ function oas3BaseUrl({ spec, pathName, method, server, contextUrl, serverVariabl {} ); - selectedServerUrl = substituteServerURLTemplate(selectedServerUrl, { - ...selectedServerVariables, - ...serverVariables, - }); + selectedServerUrl = substituteServerURLTemplate( + selectedServerUrl, + { + ...selectedServerVariables, + ...serverVariables, + }, + { encoder: typeof serverVariableEncoder === 'function' ? serverVariableEncoder : identity } + ); } return buildOas3UrlWithContext(selectedServerUrl, contextUrl); diff --git a/test/execute/server-variable-encoder.js b/test/execute/server-variable-encoder.js new file mode 100644 index 000000000..ebf2f3320 --- /dev/null +++ b/test/execute/server-variable-encoder.js @@ -0,0 +1,157 @@ +import { execute, buildRequest } from '../../src/execute/index.js'; + +describe('execute/serverVariableEncoder', () => { + test('should encode when encoder provided', () => { + const spec = { + openapi: '3.0.3', + servers: [ + { + url: '{server}/v1', + variables: { + server: { + default: 'https://swagger.io', + }, + }, + }, + ], + paths: { + '/one': { + get: { + operationId: 'getMe', + parameters: [{ name: 'petId', in: 'query' }], + }, + }, + }, + }; + + const spy = jest.fn().mockImplementation(() => Promise.resolve()); + + execute({ + fetch: spy, + spec, + operationId: 'getMe', + serverVariableEncoder: encodeURIComponent, + }); + + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0][0]).toEqual({ + method: 'GET', + url: 'https%3A%2F%2Fswagger.io/v1/one', + credentials: 'same-origin', + headers: {}, + }); + }); + + test('should not encode when encoder not provided', () => { + const spec = { + openapi: '3.0.3', + servers: [ + { + url: '{server}/v1', + variables: { + server: { + default: 'https://swagger.io', + }, + }, + }, + ], + paths: { + '/one': { + get: { + operationId: 'getMe', + parameters: [{ name: 'petId', in: 'query' }], + }, + }, + }, + }; + + const spy = jest.fn().mockImplementation(() => Promise.resolve()); + + execute({ + fetch: spy, + spec, + operationId: 'getMe', + }); + + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0][0]).toEqual({ + method: 'GET', + url: 'https://swagger.io/v1/one', + credentials: 'same-origin', + headers: {}, + }); + }); +}); + +describe('buildRequest/serverVariableEncoder', () => { + test('should encode when encoder provided', () => { + const spec = { + openapi: '3.0.3', + servers: [ + { + url: '{server}/v1', + variables: { + server: { + default: 'https://swagger.io', + }, + }, + }, + ], + paths: { + '/one': { + get: { + operationId: 'getMe', + parameters: [{ name: 'petId', in: 'query' }], + }, + }, + }, + }; + + const req = buildRequest({ + spec, + operationId: 'getMe', + parameters: { petId: 123 }, + serverVariableEncoder: encodeURIComponent, + }); + + expect(req).toEqual({ + url: 'https%3A%2F%2Fswagger.io/v1/one?petId=123', + method: 'GET', + credentials: 'same-origin', + headers: {}, + }); + }); + + test('should not encode when encoder not provided', () => { + const spec = { + openapi: '3.0.3', + servers: [ + { + url: '{server}/v1', + variables: { + server: { + default: 'https://swagger.io', + }, + }, + }, + ], + paths: { + '/one': { + get: { + operationId: 'getMe', + parameters: [{ name: 'petId', in: 'query' }], + }, + }, + }, + }; + + const req = buildRequest({ spec, operationId: 'getMe', parameters: { petId: 123 } }); + + expect(req).toEqual({ + url: 'https://swagger.io/v1/one?petId=123', + method: 'GET', + credentials: 'same-origin', + headers: {}, + }); + }); +});