From 81eacfff99f556b75914f98df0bb1bae8438ea4c Mon Sep 17 00:00:00 2001 From: Barrett Schonefeld Date: Tue, 4 Feb 2020 08:00:37 -0600 Subject: [PATCH] feat: add validator warning for binary string in "application/json" or parameter - added a findOctetSequencePaths function to handle the logic of finding schema type: string, format: binary for cases of nested arrays, objects, nested arrays of type object, objects with properties that are nested arrays, and objects with properties that are objects, and the simple case where a schema uses type: string, format: binary directly. This function takes a schema object from a resolvedSpec and returns a list of paths to octet sequences (empty list if none found). The function accepts the path both as an array and a string and returns the path in the same format received. - added logic to handle application/json request bodies that use schema type: string, format: binary - added logic to handle application/json response bodies of type: string, format: binary - added logic to handle parameters of type: string, format: binary - removed 'binary' as a valid format for type: string parameters. parameters of type: string, format: binary will result in "type+format not well-defined" error - added tests to ensure warnings are issued for request bodies, response bodies, and parameters with schema, type: string, format: binary - added complex tests to exercise combinations of nested arrays and objects that contain schema type: string, format: binary (complex tests done on response bodies) - added "json_or_param_binary_string" as to .defaultsForValidator as a warning in the oas3.schemas section - added "json_or_param_binary_string" configuration option to the README.md rules and defaults sections in the oas3 schemas section - changed findOctetSequencePaths to accept the path both as a string and an array. --- README.md | 6 + src/.defaultsForValidator.js | 3 + src/plugins/utils/findOctetSequencePaths.js | 75 ++++ .../semantic-validators/parameters-ibm.js | 2 +- .../oas3/semantic-validators/operations.js | 28 ++ .../oas3/semantic-validators/parameters.js | 41 ++ .../oas3/semantic-validators/responses.js | 125 ++++-- .../validation/2and3/parameters-ibm.js | 2 +- test/plugins/validation/oas3/operations.js | 399 ++++++++++++++++++ test/plugins/validation/oas3/parameters.js | 198 ++++++++- test/plugins/validation/oas3/responses.js | 329 ++++++++++++--- 11 files changed, 1103 insertions(+), 105 deletions(-) create mode 100644 src/plugins/utils/findOctetSequencePaths.js diff --git a/README.md b/README.md index 8750994ba..d0c781aaf 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ The supported rules are described below: | array_of_arrays | Flag any schema with a 'property' of type `array` with items of type `array`. | shared | | property_case_convention | Flag any property with a `name` that does not follow a given case convention. snake_case_only must be 'off' to use. | shared | | enum_case_convention | Flag any enum with a `value` that does not follow a given case convention. snake_case_only must be 'off' to use. | shared | +| json_or_param_binary_string | Flag parameters or application/json request/response bodies with schema type: string, format: binary. | oas3 | ##### security_definitions | Rule | Description | Spec | @@ -361,6 +362,11 @@ The default values for each rule are described below. | no_success_response_codes | warning | | no_response_body | warning | +##### schemas + +| Rule | Default | +| --------------------------- | ------- | +| json_or_param_binary_string | warning | ##### shared diff --git a/src/.defaultsForValidator.js b/src/.defaultsForValidator.js index 8dd9b8872..c51b8faa8 100644 --- a/src/.defaultsForValidator.js +++ b/src/.defaultsForValidator.js @@ -96,6 +96,9 @@ const defaults = { 'no_response_codes': 'error', 'no_success_response_codes': 'warning', 'no_response_body': 'warning' + }, + 'schemas': { + 'json_or_param_binary_string': 'warning' } } }; diff --git a/src/plugins/utils/findOctetSequencePaths.js b/src/plugins/utils/findOctetSequencePaths.js new file mode 100644 index 000000000..caf255c05 --- /dev/null +++ b/src/plugins/utils/findOctetSequencePaths.js @@ -0,0 +1,75 @@ +// Finds octet sequences (type: string, format: binary) in schemas including +// nested arrays, objects, nested arrays of type object, objects with properties +// that are nested arrays, and objects with properties that are objects This +// function takes a resolved schema object (no refs) and returns a list of +// paths to octet sequences (empty list if none found). The function accepts +// the path both as an array and a string and returns the path in the same +// format received: +// typeof(path) === 'array' => [[path1, get], [path2, get], ...] +// typeof(path) === 'string' => ['path1.get', path2.get, ...] + +const findOctetSequencePaths = (resolvedSchema, path) => { + if (!resolvedSchema) { + // schema is empty, no octet sequence + return []; + } + + const pathsToOctetSequence = []; + + if (resolvedSchema.type === 'string' && resolvedSchema.format === 'binary') { + pathsToOctetSequence.push(path); + } else if (resolvedSchema.type === 'array') { + pathsToOctetSequence.push(...arrayOctetSequences(resolvedSchema, path)); + } else if (resolvedSchema.type === 'object') { + pathsToOctetSequence.push(...objectOctetSequences(resolvedSchema, path)); + } + + return pathsToOctetSequence; +}; + +function arrayOctetSequences(resolvedSchema, path) { + const arrayPathsToOctetSequence = []; + const arrayItems = resolvedSchema.items; + if (arrayItems !== undefined) { + // supports both array and string (delimited by .) paths + const pathToSchema = Array.isArray(path) + ? path.concat('items') + : `${path}.items`; + if (arrayItems.type === 'string' && arrayItems.format === 'binary') { + arrayPathsToOctetSequence.push(pathToSchema); + } else if (arrayItems.type === 'object' || arrayItems.type === 'array') { + arrayPathsToOctetSequence.push( + ...findOctetSequencePaths(arrayItems, pathToSchema) + ); + } + } + return arrayPathsToOctetSequence; +} + +function objectOctetSequences(resolvedSchema, path) { + const objectPathsToOctetSequence = []; + const objectProperties = resolvedSchema.properties; + if (objectProperties) { + Object.keys(objectProperties).forEach(function(prop) { + const propPath = Array.isArray(path) + ? path.concat(['properties', prop]) + : `${path}.properties.${prop}`; + if ( + objectProperties[prop].type === 'string' && + objectProperties[prop].format === 'binary' + ) { + objectPathsToOctetSequence.push(propPath); + } else if ( + objectProperties[prop].type === 'object' || + objectProperties[prop].type === 'array' + ) { + objectPathsToOctetSequence.push( + ...findOctetSequencePaths(objectProperties[prop], propPath) + ); + } + }); + } + return objectPathsToOctetSequence; +} + +module.exports.findOctetSequencePaths = findOctetSequencePaths; diff --git a/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js b/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js index b16ca74af..4c54766bf 100644 --- a/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js @@ -181,7 +181,7 @@ function formatValid(obj, isOAS3) { return ( !schema.format || includes( - ['byte', 'binary', 'date', 'date-time', 'password'], + ['byte', 'date', 'date-time', 'password'], schema.format.toLowerCase() ) ); diff --git a/src/plugins/validation/oas3/semantic-validators/operations.js b/src/plugins/validation/oas3/semantic-validators/operations.js index c93159b21..33aaac200 100644 --- a/src/plugins/validation/oas3/semantic-validators/operations.js +++ b/src/plugins/validation/oas3/semantic-validators/operations.js @@ -4,15 +4,21 @@ // Assertation 2. Operations with non-form request bodies should set the `x-codegen-request-body-name` // annotation (for code generation purposes) +// Assertation 3. Request bodies with application/json content should not use schema +// type: string, format: binary. + const pick = require('lodash/pick'); const each = require('lodash/each'); const { hasRefProperty } = require('../../../utils'); +const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') + .findOctetSequencePaths; module.exports.validate = function({ resolvedSpec, jsSpec }, config) { const result = {}; result.error = []; result.warning = []; + const configSchemas = config.schemas; config = config.operations; const REQUEST_BODY_NAME = 'x-codegen-request-body-name'; @@ -82,6 +88,28 @@ module.exports.validate = function({ resolvedSpec, jsSpec }, config) { }); } } + + // Assertation 3 + const binaryStringStatus = configSchemas.json_or_param_binary_string; + if (binaryStringStatus !== 'off') { + for (const mimeType of requestBodyMimeTypes) { + if (mimeType === 'application/json') { + const schemaPath = `paths.${pathName}.${opName}.requestBody.content.${mimeType}.schema`; + const octetSequencePaths = findOctetSequencePaths( + requestBodyContent[mimeType].schema, + schemaPath + ); + const message = + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.'; + for (const p of octetSequencePaths) { + result[binaryStringStatus].push({ + path: p, + message + }); + } + } + } + } } } }); diff --git a/src/plugins/validation/oas3/semantic-validators/parameters.js b/src/plugins/validation/oas3/semantic-validators/parameters.js index d144dee15..c5919bddf 100644 --- a/src/plugins/validation/oas3/semantic-validators/parameters.js +++ b/src/plugins/validation/oas3/semantic-validators/parameters.js @@ -7,13 +7,20 @@ // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject +// Assertation 3: +// A paramater should not use schema type: string, format: binary because there is now well- +// defined way to encode an octet sequence in a URL. + const { isParameterObject, walk } = require('../../../utils'); +const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') + .findOctetSequencePaths; module.exports.validate = function({ jsSpec }, config) { const result = {}; result.error = []; result.warning = []; + const configSchemas = config.schemas; config = config.parameters; walk(jsSpec, [], function(obj, path) { @@ -66,6 +73,40 @@ module.exports.validate = function({ jsSpec }, config) { }); } } + + const binaryStringStatus = configSchemas.json_or_param_binary_string; + if (binaryStringStatus !== 'off') { + const octetSequencePaths = []; + octetSequencePaths.push( + ...findOctetSequencePaths(obj.schema, path.concat(['schema'])) + ); + if (obj.content) { + Object.keys(obj.content).forEach(function(mimeType) { + if (mimeType === 'application/json') { + const paramContentPath = path.concat([ + 'content', + mimeType, + 'schema' + ]); + octetSequencePaths.push( + ...findOctetSequencePaths( + obj.content[mimeType].schema, + paramContentPath + ) + ); + } + }); + } + + for (const p of octetSequencePaths) { + const message = + 'Parameters should not contain binary (type: string, format: binary) values.'; + result[binaryStringStatus].push({ + path: p, + message + }); + } + } } }); diff --git a/src/plugins/validation/oas3/semantic-validators/responses.js b/src/plugins/validation/oas3/semantic-validators/responses.js index 2a574dae8..5f8de7f42 100644 --- a/src/plugins/validation/oas3/semantic-validators/responses.js +++ b/src/plugins/validation/oas3/semantic-validators/responses.js @@ -12,13 +12,19 @@ // Assertation 4: // A non-204 success response should define a response body +// Assertation 5. Response bodies with application/json content should not use schema +// type: string, format: binary. + const { walk } = require('../../../utils'); +const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') + .findOctetSequencePaths; module.exports.validate = function({ resolvedSpec }, config) { const result = {}; result.error = []; result.warning = []; + const configSchemas = config.schemas; config = config.responses; walk(resolvedSpec, [], function(obj, path) { @@ -26,16 +32,19 @@ module.exports.validate = function({ resolvedSpec }, config) { path[0] === 'paths' && path[path.length - 1] === 'responses'; if (contentsOfResponsesObject) { - if (obj['204'] && obj['204'].content) { - result.error.push({ - path: path.concat(['204', 'content']), - message: `A 204 response MUST NOT include a message-body.` - }); + const [statusCodes, successCodes] = getResponseCodes(obj); + + const binaryStringStatus = configSchemas.json_or_param_binary_string; + if (binaryStringStatus !== 'off') { + validateNoBinaryStringsInResponse( + obj, + result, + path, + binaryStringStatus + ); } - const responseCodes = Object.keys(obj).filter(code => - isResponseCode(code) - ); - if (!responseCodes.length) { + + if (!statusCodes.length) { const message = 'Each `responses` object MUST have at least one response code.'; const checkStatus = config.no_response_codes; @@ -45,36 +54,33 @@ module.exports.validate = function({ resolvedSpec }, config) { message }); } + } else if (!successCodes.length) { + const message = + 'Each `responses` object SHOULD have at least one code for a successful response.'; + const checkStatus = config.no_success_response_codes; + if (checkStatus !== 'off') { + result[checkStatus].push({ + path, + message + }); + } } else { - const successCodes = responseCodes.filter( - code => code.slice(0, 1) === '2' - ); - if (!successCodes.length) { - const message = - 'Each `responses` object SHOULD have at least one code for a successful response.'; - const checkStatus = config.no_success_response_codes; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } - } else { - const checkStatus = config.no_response_body; - // if response body rule is on, loops through success codes and issues warning (by default) - // for non-204 success responses without a response body - if (checkStatus !== 'off') { - for (const successCode of successCodes) { - if (successCode != '204' && !obj[successCode].content) { - result[checkStatus].push({ - path: path.concat([successCode]), - message: - `A ` + - successCode + - ` response should include a response body. Use 204 for responses without content.` - }); - } + // validate success codes + for (const successCode of successCodes) { + if (successCode !== '204' && !obj[successCode].content) { + const checkStatus = config.no_response_body; + if (checkStatus !== 'off') { + const message = `A ${successCode} response should include a response body. Use 204 for responses without content.`; + result[checkStatus].push({ + path: path.concat([successCode]), + message + }); } + } else if (successCode === '204' && obj[successCode].content) { + result.error.push({ + path: path.concat(['204', 'content']), + message: `A 204 response MUST NOT include a message-body.` + }); } } } @@ -84,7 +90,50 @@ module.exports.validate = function({ resolvedSpec }, config) { return { errors: result.error, warnings: result.warning }; }; -function isResponseCode(code) { +function getResponseCodes(responseObj) { + const statusCodes = Object.keys(responseObj).filter(code => + isStatusCode(code) + ); + const successCodes = statusCodes.filter(code => code.slice(0, 1) === '2'); + return [statusCodes, successCodes]; +} + +function isStatusCode(code) { const allowedFirstDigits = ['1', '2', '3', '4', '5']; return code.length === 3 && allowedFirstDigits.includes(code.slice(0, 1)); } + +function validateNoBinaryStringsInResponse( + responseObj, + result, + path, + binaryStringStatus +) { + Object.keys(responseObj).forEach(function(responseCode) { + const responseBodyContent = responseObj[responseCode].content; + if (responseBodyContent) { + Object.keys(responseBodyContent).forEach(function(mimeType) { + if (mimeType === 'application/json') { + const schemaPath = path.concat([ + responseCode, + 'content', + mimeType, + 'schema' + ]); + const octetSequencePaths = findOctetSequencePaths( + responseBodyContent[mimeType].schema, + schemaPath + ); + const message = + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.'; + for (const p of octetSequencePaths) { + result[binaryStringStatus].push({ + path: p, + message + }); + } + } + }); + } + }); +} diff --git a/test/plugins/validation/2and3/parameters-ibm.js b/test/plugins/validation/2and3/parameters-ibm.js index e1dbbe311..3ea6f0fbb 100644 --- a/test/plugins/validation/2and3/parameters-ibm.js +++ b/test/plugins/validation/2and3/parameters-ibm.js @@ -594,7 +594,7 @@ describe('validation plugin - semantic - parameters-ibm', () => { in: 'query', schema: { type: 'string', - format: 'binary' + format: 'byte' } } ] diff --git a/test/plugins/validation/oas3/operations.js b/test/plugins/validation/oas3/operations.js index f0e9a435e..01eb5b830 100644 --- a/test/plugins/validation/oas3/operations.js +++ b/test/plugins/validation/oas3/operations.js @@ -263,4 +263,403 @@ describe('validation plugin - semantic - operations - oas3', function() { expect(res.errors.length).toEqual(0); expect(res.warnings.length).toEqual(0); }); + + it('should not complain about valid use of type:string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'multipart/form-data': { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(0); + }); + + it('should warn about application/json request body with type:string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema' + ); + }); + + it('should warn about application/json request body with nested array of type:string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.items' + ); + }); + + it('should warn about application/json request body with nested arrays of Objects with octet sequences', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'array', + items: { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + type: 'string', + format: 'binary' + }, + prop2: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(2); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.items.properties.prop1' + ); + expect(res.warnings[1].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[1].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.items.properties.prop2' + ); + }); + + it('should warn about json with type: string, format: binary when json is the second mime type', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'text/plain': { + schema: { + type: 'string' + } + }, + 'application/json': { + schema: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema' + ); + }); + + it('should warn about json request body with nested arrays of Objects with prop of nested array type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop1.items.items' + ); + }); + + it('should warn about json request body with nested arrays of Objects with props of type Object that have props of type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + type: 'object', + properties: { + sub_prop1: { + type: 'string', + format: 'binary' + }, + sub_prop2: { + type: 'string', + format: 'binary' + } + } + }, + prop2: { + type: 'object', + properties: { + sub_prop3: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(3); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop1.properties.sub_prop1' + ); + expect(res.warnings[1].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[1].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop1.properties.sub_prop2' + ); + expect(res.warnings[2].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[2].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop2.properties.sub_prop3' + ); + }); + + it('should warn about json request body with nested arrays of Objects with props of type Object that have props of type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + post: { + 'x-codegen-request-body-name': 'goodRequestBody', + summary: 'this is a summary', + operationId: 'operationId', + requestBody: { + description: 'body for request', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + type: 'object', + properties: { + sub_prop1: { + type: 'string', + format: 'binary' + } + } + }, + prop2: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec, jsSpec: spec }, config); + expect(res.warnings.length).toEqual(2); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[0].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop1.properties.sub_prop1' + ); + expect(res.warnings[1].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[1].path).toEqual( + 'paths./pets.post.requestBody.content.application/json.schema.items.items.properties.prop2.items' + ); + }); }); diff --git a/test/plugins/validation/oas3/parameters.js b/test/plugins/validation/oas3/parameters.js index a16317c13..3d7c63ace 100644 --- a/test/plugins/validation/oas3/parameters.js +++ b/test/plugins/validation/oas3/parameters.js @@ -224,7 +224,157 @@ describe('validation plugin - semantic - parameters - oas3', function() { expect(res.warnings.length).toEqual(0); }); - it('should not complain when parameter is a ref', function() { + it('should complain when a parameter uses json content with schema type: string, format: binary', function() { + const spec = { + components: { + parameters: { + BadParam: { + in: 'path', + name: 'path_param', + description: 'a parameter', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + prop1: { + type: 'array', + items: { + type: 'array', + items: { + type: 'object', + properties: { + sub_prop1: { + type: 'object', + properties: { + sub_sub_prop1: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'components', + 'parameters', + 'BadParam', + 'content', + 'application/json', + 'schema', + 'properties', + 'prop1', + 'items', + 'items', + 'properties', + 'sub_prop1', + 'properties', + 'sub_sub_prop1' + ]); + expect(res.warnings[0].message).toEqual( + 'Parameters should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should complain when a parameter uses json as second mime type with schema type: string, format: binary', function() { + const spec = { + components: { + parameters: { + BadParam: { + in: 'path', + name: 'path_param', + description: 'a parameter', + content: { + 'text/plain': { + type: 'string' + }, + 'application/json': { + schema: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'components', + 'parameters', + 'BadParam', + 'content', + 'application/json', + 'schema' + ]); + expect(res.warnings[0].message).toEqual( + 'Parameters should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should complain multiple times when multiple parameters use schema type: string, format: binary', function() { + const spec = { + components: { + parameters: { + BadParam1: { + schema: { + type: 'string', + format: 'binary' + } + }, + BadParam2: { + schema: { + type: 'string', + format: 'binary' + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(2); + expect(res.warnings[0].path).toEqual([ + 'components', + 'parameters', + 'BadParam1', + 'schema' + ]); + expect(res.warnings[0].message).toEqual( + 'Parameters should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should not complain when the schema field is empty', function() { + const spec = { + components: { + parameters: { + GoodParam: {} + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(0); + }); + + it('should not complain when parameter is a ref', async function() { const spec = { paths: { '/pets': { @@ -270,6 +420,52 @@ describe('validation plugin - semantic - parameters - oas3', function() { expect(res.warnings.length).toEqual(0); }); + it('should not complain twice when parameter is a ref', async function() { + const spec = { + paths: { + '/pets': { + get: { + summary: 'this is a summary', + operationId: 'operationId', + parameters: [ + { + $ref: '#/components/parameters/QueryParam' + } + ], + responses: { + '200': { + description: 'success', + content: { + 'text/plain': { + schema: { + type: 'string' + } + } + } + } + } + } + } + }, + components: { + parameters: { + QueryParam: { + in: 'query', + name: 'query_param', + schema: { + type: 'string', + format: 'binary' + }, + description: 'a parameter' + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + }); + it('should not complain about a schema property named `parameters`', function() { const spec = { components: { diff --git a/test/plugins/validation/oas3/responses.js b/test/plugins/validation/oas3/responses.js index b0ae8cc0c..e45ac35a6 100644 --- a/test/plugins/validation/oas3/responses.js +++ b/test/plugins/validation/oas3/responses.js @@ -5,16 +5,273 @@ const { validate } = require('../../../../src/plugins/validation/oas3/semantic-validators/responses'); +const config = require('../../../../src/.defaultsForValidator').defaults.oas3; + describe('validation plugin - semantic - responses - oas3', function() { - it('should complain when response object only has a default', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' + it('should not complain for valid use of type:string, format: binary', function() { + const spec = { + paths: { + '/pets': { + get: { + summary: 'this is a summary', + operationId: 'operationId', + responses: { + '200': { + description: '200 response', + content: { + 'multipart/form-data': { + schema: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec }, config); + expect(res.warnings.length).toEqual(0); + }); + + it('should complain when response body uses json and schema type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + get: { + summary: 'this is a summary', + operationId: 'operationId', + responses: { + '200': { + description: '200 response', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + '200', + 'content', + 'application/json', + 'schema', + 'items' + ]); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should complain when default response body uses json as second mime type and uses schema type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + get: { + summary: 'this is a summary', + operationId: 'operationId', + responses: { + default: { + description: 'the default response', + content: { + 'text/plain': { + schema: { + type: 'string' + } + }, + 'application/json': { + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } + } + }; + + const res = validate({ resolvedSpec: spec }, config); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + 'default', + 'content', + 'application/json', + 'schema', + 'properties', + 'prop1' + ]); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should complain multiple times when multiple json response bodies use type: string, format: binary', function() { + const spec = { + paths: { + '/pets': { + get: { + summary: 'this is a summary', + operationId: 'operationId', + responses: { + '200': { + description: '200 response', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + }, + '201': { + description: '201 response', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + format: 'binary' + }, + prop2: { + type: 'array', + items: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + }, + '204': { + description: '204 response' + }, + default: { + description: 'the default response', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + format: 'binary' + } + } + } + } + } + } + } + } + } } }; + const res = validate({ resolvedSpec: spec }, config); + expect(res.warnings.length).toEqual(4); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + '200', + 'content', + 'application/json', + 'schema', + 'items' + ]); + expect(res.warnings[0].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[1].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + '201', + 'content', + 'application/json', + 'schema', + 'properties', + 'prop1' + ]); + expect(res.warnings[1].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[2].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + '201', + 'content', + 'application/json', + 'schema', + 'properties', + 'prop2', + 'items' + ]); + expect(res.warnings[2].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + expect(res.warnings[3].path).toEqual([ + 'paths', + '/pets', + 'get', + 'responses', + 'default', + 'content', + 'application/json', + 'schema', + 'properties', + 'prop1' + ]); + expect(res.warnings[3].message).toEqual( + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.' + ); + }); + + it('should complain when response object only has a default', function() { const spec = { paths: { '/pets': { @@ -41,14 +298,6 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should complain when no response codes are valid', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/pets': { @@ -75,14 +324,6 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should not complain when there are no problems', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/pets': { @@ -105,14 +346,6 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should complain when a non-204 success does not have response body', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/example': { @@ -144,14 +377,6 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should issue multiple warnings when multiple non-204 successes do not have response bodies', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/example1': { @@ -217,15 +442,7 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should not complain when a non-204 success has a ref to a response with content', async function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - - const resolvedSpec = { + const jsSpec = { paths: { '/comments': { post: { @@ -255,21 +472,13 @@ describe('validation plugin - semantic - responses - oas3', function() { } }; - const spec = await resolver.dereference(resolvedSpec); + const spec = await resolver.dereference(jsSpec); const res = validate({ resolvedSpec: spec }, config); expect(res.warnings.length).toEqual(0); }); it('should complain about having only error responses', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/pets': { @@ -310,14 +519,6 @@ describe('validation plugin - semantic - responses - oas3', function() { }); it('should complain about 204 response that defines a response body', function() { - const config = { - responses: { - no_response_codes: 'error', - no_success_response_codes: 'warning', - no_response_body: 'warning' - } - }; - const spec = { paths: { '/pets': {