diff --git a/lib/mocker/express/server.js b/lib/mocker/express/server.js index 5f6799d..9e60f99 100644 --- a/lib/mocker/express/server.js +++ b/lib/mocker/express/server.js @@ -8,6 +8,7 @@ const cors = require('cors'); const cookieParser = require('cookie-parser'); const logger = require('lllog')(); const colors = require('colors'); +const parsePreferHeader = require('parse-prefer-header'); const openApiMockSymbol = Symbol('openApiMock'); @@ -95,14 +96,14 @@ class Server { return this.sendResponse(res, { errors: failedValidations }, 400); const preferHeader = req.header('prefer') || ''; - const [, preferStatusCode] = preferHeader.match(/statusCode=(\d{3})/) || []; + const { example: preferredExampleName, statusCode: preferredStatusCode } = parsePreferHeader(preferHeader) || {}; - if(preferStatusCode) - logger.debug(`Searching requested response with status code ${preferStatusCode}`); + if(preferredStatusCode) + logger.debug(`Searching requested response with status code ${preferredStatusCode}`); else logger.debug('Searching first response'); - const { statusCode, headers: responseHeaders, body } = path.getResponse(preferStatusCode); + const { statusCode, headers: responseHeaders, body } = path.getResponse(preferredStatusCode, preferredExampleName); return this.sendResponse(res, body, statusCode, responseHeaders); }); diff --git a/lib/paths/path.js b/lib/paths/path.js index 0f278ab..244b6a2 100644 --- a/lib/paths/path.js +++ b/lib/paths/path.js @@ -209,7 +209,7 @@ class Path { return !possibleValues || !possibleValues.length || possibleValues.includes(value); } - getResponse(preferredStatusCode) { + getResponse(preferredStatusCode, preferredExampleName) { const { statusCode, @@ -220,7 +220,7 @@ class Path { return { statusCode: Number(statusCode), headers: headers && this.generateResponseHeaders(headers), - body: schema ? ResponseGenerator.generate(schema) : null + body: schema ? ResponseGenerator.generate(schema, preferredExampleName) : null }; } diff --git a/lib/response-generator/index.js b/lib/response-generator/index.js index 7d451b8..41c8590 100644 --- a/lib/response-generator/index.js +++ b/lib/response-generator/index.js @@ -2,13 +2,16 @@ class ResponseGenerator { - static generate(schemaResponse) { + static generate(schemaResponse, preferredExampleName) { if(schemaResponse.example) return schemaResponse.example; - if(schemaResponse.examples && schemaResponse.examples.length) + if(schemaResponse.examples && Object.values(schemaResponse.examples).length) { + if(preferredExampleName) + return schemaResponse.examples[preferredExampleName] || Object.values(schemaResponse.examples)[0]; return schemaResponse.examples[0]; + } if(schemaResponse.enum && schemaResponse.enum.length) return this.generateByEnum(schemaResponse.enum); diff --git a/package-lock.json b/package-lock.json index fb288db..1a754d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2187,6 +2187,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -2952,6 +2957,14 @@ "error-ex": "^1.2.0" } }, + "parse-prefer-header": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-prefer-header/-/parse-prefer-header-1.0.0.tgz", + "integrity": "sha1-da63N2a7ZTHd4GPuAjh26tSWSPc=", + "requires": { + "lodash.camelcase": "^4.3.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index c226aa1..eb0165e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "js-yaml": "^3.14.0", "json-refs": "^3.0.13", "lllog": "^1.1.2", + "parse-prefer-header": "^1.0.0", "superstruct": "^0.6.1", "yargs": "^13.3.0" }, diff --git a/tests/paths/path.js b/tests/paths/path.js index 646a89c..17a272b 100644 --- a/tests/paths/path.js +++ b/tests/paths/path.js @@ -1803,6 +1803,78 @@ describe('Paths', () => { }); }); + it('Should call the response generator with the first available response with given example if no preferred statusCode is passed', () => { + + const path = new Path({ + uri: '/hello', + httpMethod: 'get', + parameters: undefined, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + examples: { + hello: { + message: 'world' + }, + goodbye: { + message: 'yellow brick road' + } + } + } + } + } + } + }); + + const response = path.getResponse(undefined, 'goodbye'); + + assert.deepStrictEqual(response, { + statusCode: 200, + headers: undefined, + body: { + message: 'yellow brick road' + } + }); + }); + + it('Should call the response generator with the first available response with prefer example & no prefered statusCode, but no match', () => { + + const path = new Path({ + uri: '/hello', + httpMethod: 'get', + parameters: undefined, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + examples: { + hello: { + message: 'world' + }, + goodbye: { + message: 'yellow brick road' + } + } + } + } + } + } + }); + + const response = path.getResponse(undefined, 'sup'); + + assert.deepStrictEqual(response, { + statusCode: 200, + headers: undefined, + body: { + message: 'world' + } + }); + }); + it('Should call the response generator with the preferred response based on the passed statusCode', () => { const path = new Path({ @@ -1844,6 +1916,52 @@ describe('Paths', () => { }); }); + it('Should call the response generator with the preferred response based on the passed statusCode and passed example', () => { + + const path = new Path({ + uri: '/hello', + httpMethod: 'get', + parameters: undefined, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + example: { + hello: 'world' + } + } + } + }, + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + examples: { + invalid: { + message: 'Unauthorized - token invalid' + }, + expired: { + message: 'Unauthorized - token expired' + } + } + } + } + } + } + }); + + const response = path.getResponse('401', 'expired'); + + assert.deepStrictEqual(response, { + statusCode: 401, + headers: undefined, + body: { + message: 'Unauthorized - token expired' + } + }); + }); + it('Should call the response generator with the first response if preferred response is not available', () => { const path = new Path({ diff --git a/tests/response-generator/index.js b/tests/response-generator/index.js index db57534..4c292b0 100644 --- a/tests/response-generator/index.js +++ b/tests/response-generator/index.js @@ -38,6 +38,47 @@ describe('Response Generator', () => { }); }); + it('Should return the preferred example if prefer header is set', () => { + + const responseSchema = { + examples: { + cat: { + summary: 'An example of a cat', + value: { + name: 'Fluffy', + petType: 'Cat', + color: 'White', + gender: 'male', + breed: 'Persian' + } + }, + dog: { + summary: 'An example of a dog with a cat\'s name', + value: { + name: 'Puma', + petType: 'Dog', + color: 'Black', + gender: 'Female', + breed: 'Mixed' + } + } + } + }; + + const response = ResponseGenerator.generate(responseSchema, 'cat'); + + assert.deepStrictEqual(response, { + summary: 'An example of a cat', + value: { + name: 'Fluffy', + petType: 'Cat', + color: 'White', + gender: 'male', + breed: 'Persian' + } + }); + }); + it('Should return the schema\'s example if it\'s defined', () => { const responseSchema = {