From 1802907573e300d67f0d60c06cd850b88a0df6b9 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 findOctetSequences 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). - 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 shared.schemas section - added "json_or_param_binary_string" configuration option to the README.md rules and defaults sections in the schemas section --- README.md | 2 + src/.defaultsForValidator.js | 3 +- src/plugins/utils/findOctetSequencePaths.js | 66 +++ .../semantic-validators/parameters-ibm.js | 2 +- .../oas3/semantic-validators/operations.js | 28 + .../oas3/semantic-validators/parameters.js | 39 ++ .../oas3/semantic-validators/responses.js | 122 +++-- .../validation/2and3/parameters-ibm.js | 2 +- test/plugins/validation/oas3/operations.js | 514 +++++++++++++++++- test/plugins/validation/oas3/parameters.js | 346 +++++++++++- test/plugins/validation/oas3/responses.js | 295 +++++++++- 11 files changed, 1373 insertions(+), 46 deletions(-) create mode 100644 src/plugins/utils/findOctetSequencePaths.js diff --git a/README.md b/README.md index 8750994ba..82fb304eb 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 and response bodies with schema type: string, format: binary. | shared | ##### security_definitions | Rule | Description | Spec | @@ -424,6 +425,7 @@ The default values for each rule are described below. | array_of_arrays | warning | | property_case_convention | error, lower_snake_case | | enum_case_convention | error, lower_snake_case | +| json_or_param_binary_string | warning | ###### walker | Rule | Default | diff --git a/src/.defaultsForValidator.js b/src/.defaultsForValidator.js index 8dd9b8872..450ce19bb 100644 --- a/src/.defaultsForValidator.js +++ b/src/.defaultsForValidator.js @@ -64,7 +64,8 @@ const defaults = { 'description_mentions_json': 'warning', 'array_of_arrays': 'warning', 'property_case_convention': [ 'error', 'lower_snake_case'], - 'enum_case_convention': [ 'error', 'lower_snake_case'] + 'enum_case_convention': [ 'error', 'lower_snake_case'], + 'binary_string': 'warning' }, 'walker': { 'no_empty_descriptions': 'error', diff --git a/src/plugins/utils/findOctetSequencePaths.js b/src/plugins/utils/findOctetSequencePaths.js new file mode 100644 index 000000000..23d4d8664 --- /dev/null +++ b/src/plugins/utils/findOctetSequencePaths.js @@ -0,0 +1,66 @@ +// 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). + +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) { + const arrayPath = `${path}.items`; + if (arrayItems.type === 'string' && arrayItems.format === 'binary') { + arrayPathsToOctetSequence.push(arrayPath); + } else if (arrayItems.type === 'object' || arrayItems.type === 'array') { + arrayPathsToOctetSequence.push( + ...findOctetSequencePaths(arrayItems, arrayPath) + ); + } + } + return arrayPathsToOctetSequence; +} + +function objectOctetSequences(resolvedSchema, path) { + const objectPathsToOctetSequence = []; + const objectProperties = resolvedSchema.properties; + if (objectProperties) { + Object.keys(objectProperties).forEach(function(prop) { + const propPath = `${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..eaf391820 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,38 @@ 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.join('.')}.schema`) + ); + if (obj.content) { + Object.keys(obj.content).forEach(function(mimeType) { + if (mimeType === 'application/json') { + const paramContentPath = `${path.join( + '.' + )}.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.split('.'), + message + }); + } + } } }); diff --git a/src/plugins/validation/oas3/semantic-validators/responses.js b/src/plugins/validation/oas3/semantic-validators/responses.js index 2a574dae8..bdeacae0e 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 3. 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,47 @@ 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.join( + '.' + )}.${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.split('.'), + 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..693da44b4 100644 --- a/test/plugins/validation/oas3/operations.js +++ b/test/plugins/validation/oas3/operations.js @@ -2,10 +2,19 @@ const expect = require('expect'); const { validate } = require('../../../../src/plugins/validation/oas3/semantic-validators/operations'); -const config = require('../../../../src/.defaultsForValidator').defaults.oas3; describe('validation plugin - semantic - operations - oas3', function() { it('should complain about a request body not having a content field', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -30,6 +39,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should warn about an operation with a non-form, array schema request body that does not set a name', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -64,6 +83,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not warn about an operation with a non-array json request body that does not set a name', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -91,6 +120,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not warn about an operation with a non-form request body that sets a name', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -119,6 +158,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not warn about an operation with a form request body that does not set a name', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -151,6 +200,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not warn about an operation with a referenced request body that does not set a name', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const resolvedSpec = { paths: { '/pets': { @@ -191,6 +250,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not crash in request body name check when path name contains a period', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/other.pets': { @@ -225,6 +294,16 @@ describe('validation plugin - semantic - operations - oas3', function() { }); it('should not crash when request body is behind a ref', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const jsSpec = { paths: { '/resource': { @@ -263,4 +342,437 @@ describe('validation plugin - semantic - operations - oas3', function() { expect(res.errors.length).toEqual(0); expect(res.warnings.length).toEqual(0); }); + + it('should warn about application/json request body with type:string, format: binary', function() { + const config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + operations: { + no_request_body_content: 'error', + no_request_body_name: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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..ceb24ce81 100644 --- a/test/plugins/validation/oas3/parameters.js +++ b/test/plugins/validation/oas3/parameters.js @@ -1,11 +1,23 @@ const expect = require('expect'); + const { validate } = require('../../../../src/plugins/validation/oas3/semantic-validators/parameters'); -const config = require('../../../../src/.defaultsForValidator').defaults.oas3; describe('validation plugin - semantic - parameters - oas3', function() { it('should not complain when parameter is valid', function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -45,6 +57,18 @@ describe('validation plugin - semantic - parameters - oas3', function() { }); it('should complain when `in` is missing', function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -93,6 +117,18 @@ describe('validation plugin - semantic - parameters - oas3', function() { }); it('should complain when `in` is an invalid value', function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -143,6 +179,18 @@ describe('validation plugin - semantic - parameters - oas3', function() { }); it('should complain when the parameter has an undescribed data type', function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -189,6 +237,18 @@ describe('validation plugin - semantic - parameters - oas3', function() { }); it('should complain when a parameter describes data type with both `schema` and `content`', function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { components: { parameters: { @@ -224,7 +284,219 @@ 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + const spec = { + components: { + parameters: { + GoodParam: { + schema: {} + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.warnings.length).toEqual(0); + }); + + it('should not complain when parameter is a ref', async function() { + const config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { paths: { '/pets': { @@ -270,7 +542,77 @@ 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + parameters: { + no_in_property: 'error', + invalid_in_property: 'error', + missing_schema_or_content: 'error', + has_schema_and_content: 'error' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + const spec = { components: { schemas: { diff --git a/test/plugins/validation/oas3/responses.js b/test/plugins/validation/oas3/responses.js index b0ae8cc0c..753f49ea5 100644 --- a/test/plugins/validation/oas3/responses.js +++ b/test/plugins/validation/oas3/responses.js @@ -6,12 +6,282 @@ const { } = require('../../../../src/plugins/validation/oas3/semantic-validators/responses'); describe('validation plugin - semantic - responses - oas3', function() { + it('should complain when response body uses json and schema type: string, format: binary', function() { + const config = { + responses: { + no_response_codes: 'error', + no_success_response_codes: 'warning', + no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + responses: { + no_response_codes: 'error', + no_success_response_codes: 'warning', + no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { + responses: { + no_response_codes: 'error', + no_success_response_codes: 'warning', + no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' + } + }; + + 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 config = { responses: { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -46,6 +316,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -80,6 +353,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -110,6 +386,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -149,6 +428,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -222,10 +504,13 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; - const resolvedSpec = { + const jsSpec = { paths: { '/comments': { post: { @@ -255,7 +540,7 @@ 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); @@ -267,6 +552,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } }; @@ -315,6 +603,9 @@ describe('validation plugin - semantic - responses - oas3', function() { no_response_codes: 'error', no_success_response_codes: 'warning', no_response_body: 'warning' + }, + schemas: { + json_or_param_binary_string: 'warning' } };