From 65d7e9af238b7c5d5bc2d2cfb98f4cbc052d3fbe Mon Sep 17 00:00:00 2001 From: Vishal Shingala Date: Thu, 21 Oct 2021 00:00:48 +0530 Subject: [PATCH] Added support for anyOf and oneOf keywords in validation flow --- .eslintrc | 2 +- lib/deref.js | 20 +- lib/schemaUtils.js | 36 ++- .../compositeSchemaCollection.json | 252 ++++++++++++++++++ .../validationData/compositeSchemaSpec.yaml | 80 ++++++ test/unit/deref.test.js | 13 + test/unit/validator.test.js | 53 ++++ 7 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 test/data/validationData/compositeSchemaCollection.json create mode 100644 test/data/validationData/compositeSchemaSpec.yaml diff --git a/.eslintrc b/.eslintrc index 989cacb..d8397de 100644 --- a/.eslintrc +++ b/.eslintrc @@ -61,7 +61,7 @@ "no-caller": "error", "no-case-declarations": "error", "no-div-regex": "error", - "no-else-return": "error", + // "no-else-return": "error", "no-empty-function": "error", "no-empty-pattern": "error", "no-eq-null": "error", diff --git a/lib/deref.js b/lib/deref.js index 19ca419..7c28095 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -151,12 +151,24 @@ module.exports = { } if (schema.anyOf) { - return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, - resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + if (resolveFor === 'CONVERSION') { + return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, + resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + } + return { anyOf: _.map(schema.anyOf, (schemaElement) => { + return this.resolveRefs(schemaElement, parameterSourceOption, components, schemaResolutionCache, resolveFor, + resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + }) }; } if (schema.oneOf) { - return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, - resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + if (resolveFor === 'CONVERSION') { + return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, + resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + } + return { oneOf: _.map(schema.oneOf, (schemaElement) => { + return this.resolveRefs(schemaElement, parameterSourceOption, components, schemaResolutionCache, resolveFor, + resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + }) }; } if (schema.allOf) { return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor, diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index a26896c..5b96813 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -2986,7 +2986,8 @@ module.exports = { // This is dereferenced schema (converted to JSON schema for validation) schema = deref.resolveRefs(openApiSchemaObj, parameterSourceOption, components, - schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit); + schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit), + compositeSchema = schema.oneOf || schema.anyOf; if (needJsonMatching) { try { @@ -3001,8 +3002,29 @@ module.exports = { } } + // For anyOf and oneOf schemas, validate value against each schema and report result with least mismatches + if (compositeSchema) { + // get mismatches of value against each schema + async.map(compositeSchema, (elementSchema, cb) => { + setTimeout(() => { + this.checkValueAgainstSchema(property, jsonPathPrefix, txnParamName, value, + `${schemaPathPrefix}.${schema.oneOf ? 'oneOf' : 'anyOf'}[${_.findIndex(compositeSchema, elementSchema)}]`, + elementSchema, parameterSourceOption, components, options, schemaCache, cb); + }, 0); + }, (err, results) => { + let sortedResults; + + if (err) { + return callback(err, []); + } + + // return mismatches of schema against which least validation mismatches were found + sortedResults = _.sortBy(results, (res) => { return res.length; }); + return callback(null, sortedResults[0]); + }); + } // When processing a reference, schema.type could also be undefined - if (schema && schema.type) { + else if (schema && schema.type) { if (typeof schemaTypeToJsValidator[schema.type] === 'function') { let isCorrectType; @@ -3168,6 +3190,7 @@ module.exports = { return callback(null, mismatches); } // result passed. No AJV mismatch + return callback(null, []); } // Schema was not AJV or object @@ -3186,15 +3209,20 @@ module.exports = { } }]); } + else { + return callback(null, []); + } } else { // unknown schema.type found // TODO: Decide how to handle. Log? + return callback(null, []); } } // Schema not defined - return callback(null, []); - + else { + return callback(null, []); + } // if (!schemaTypeToJsValidator[schema.type](value)) { // callback(null, [{ // property, diff --git a/test/data/validationData/compositeSchemaCollection.json b/test/data/validationData/compositeSchemaCollection.json new file mode 100644 index 0000000..f235358 --- /dev/null +++ b/test/data/validationData/compositeSchemaCollection.json @@ -0,0 +1,252 @@ +{ + "item": [ + { + "id": "e3a46ab9-3e5d-4209-8a1c-3c2d9b51d488", + "name": "composite schema with anyOf keyword", + "request": { + "name": "composite schema with anyOf keyword", + "description": {}, + "url": { + "path": [ + "pets", + "anyOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw": "{\n \"objectType\": \"a string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "response": [ + { + "id": "586bc4d3-3e99-4996-ae41-492b9528a09c", + "name": "ok", + "originalRequest": { + "url": { + "path": [ + "pets", + "anyOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "method": "POST", + "body": { + "mode": "raw", + "raw": "{\n \"objectType\": \"a string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "status": "Internal Server Error", + "code": 500, + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "body": "", + "cookie": [], + "_postman_previewlanguage": "text" + } + ], + "event": [] + }, + { + "id": "07e822eb-3b75-44ca-8f95-47a5d529e64d", + "name": "composite schema with oneOf keyword", + "request": { + "name": "composite schema with oneOf keyword", + "description": {}, + "url": { + "path": [ + "pets", + "oneOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw":"{\n \"objectType2\": \"a string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "response": [ + { + "id": "8e4bffe2-c6b9-490a-9d65-3d16997d54fa", + "name": "ok", + "originalRequest": { + "url": { + "path": [ + "pets", + "oneOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "method": "POST", + "body": { + "mode": "raw", + "raw":"{\n \"objectType2\": \"a string\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "status": "Internal Server Error", + "code": 500, + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "body": "", + "cookie": [], + "_postman_previewlanguage": "text" + } + ], + "event": [] + }, + { + "id": "b7c13480-5b84-4acc-9749-1b9949f99a7d", + "name": "composite schema with allOf keyword", + "request": { + "name": "composite schema with allOf keyword", + "description": {}, + "url": { + "path": [ + "pets", + "allOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "auth": null, + "body": { + "mode": "raw", + "raw": "{\n \"objectType\": \"not an integer\",\n \"objectType2\": \"prop named objectType2\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "response": [ + { + "id": "292a946f-874a-4274-a6a2-66715f3b37f3", + "name": "ok", + "originalRequest": { + "url": { + "path": [ + "pets", + "allOf" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "method": "POST", + "body": { + "mode": "raw", + "raw": "{\n \"objectType\": \"not an integer\",\n \"objectType2\": \"prop named objectType2\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "status": "Internal Server Error", + "code": 500, + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "body": "", + "cookie": [], + "_postman_previewlanguage": "text" + } + ], + "event": [] + } + ], + "event": [], + "variable": [ + { + "type": "string", + "value": "http://petstore.swagger.io/v1", + "key": "baseUrl" + } + ], + "info": { + "_postman_id": "2cccac2f-9007-4df9-ae5e-a6630ad8fc3f", + "name": "Swagger Petstore", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "description": { + "content": "", + "type": "text/plain" + } + } +} \ No newline at end of file diff --git a/test/data/validationData/compositeSchemaSpec.yaml b/test/data/validationData/compositeSchemaSpec.yaml new file mode 100644 index 0000000..a318aa8 --- /dev/null +++ b/test/data/validationData/compositeSchemaSpec.yaml @@ -0,0 +1,80 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets/anyOf: + post: + summary: composite schema with anyOf keyword + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/anyOfExample" + responses: + default: + description: ok + /pets/oneOf: + post: + summary: composite schema with oneOf keyword + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/oneOfExample" + responses: + default: + description: ok + /pets/allOf: + post: + summary: composite schema with allOf keyword + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/allOfExample" + responses: + default: + description: ok +components: + schemas: + anyOfExample: + anyOf: + - $ref: "#/components/schemas/schema1" + - $ref: "#/components/schemas/schema2" + oneOfExample: + oneOf: + - $ref: "#/components/schemas/schema1" + - $ref: "#/components/schemas/schema3" + allOfExample: + allOf: + - $ref: "#/components/schemas/schema1" + - $ref: "#/components/schemas/schema3" + schema1: + type: object + required: + - objectType + properties: + objectType: + type: integer + example: 4321 + schema2: + type: object + required: + - objectType + properties: + objectType: + type: string + example: prop named objectType + schema3: + type: object + required: + - objectType2 + properties: + objectType2: + type: string + example: prop named objectType2 diff --git a/test/unit/deref.test.js b/test/unit/deref.test.js index 1e0f2c8..0d7956b 100644 --- a/test/unit/deref.test.js +++ b/test/unit/deref.test.js @@ -95,6 +95,8 @@ describe('DEREF FUNCTION TESTS ', function() { parameterSource = 'REQUEST', // deref.resolveRefs modifies the input schema and components so cloning to keep tests independent of each other output = deref.resolveRefs(schema, parameterSource, _.cloneDeep(componentsAndPaths)), + output_validation = deref.resolveRefs(schema, parameterSource, _.cloneDeep(componentsAndPaths), + {}, 'VALIDATION'), output_withdot = deref.resolveRefs(schemaWithDotInKey, parameterSource, _.cloneDeep(componentsAndPaths)), output_customFormat = deref.resolveRefs(schemaWithCustomFormat, parameterSource, _.cloneDeep(componentsAndPaths)), @@ -107,6 +109,17 @@ describe('DEREF FUNCTION TESTS ', function() { required: ['id'], properties: { id: { default: '', type: 'integer' } } }); + expect(output_validation).to.deep.include({ anyOf: [ + { type: 'object', + required: ['id'], + description: 'Schema 2', + properties: { id: { type: 'integer' } } + }, { + type: 'object', + properties: { emailField: { type: 'string', format: 'email' } } + } + ] }); + expect(output_withdot).to.deep.include({ type: 'object', required: ['id'], properties: { id: { default: '', type: 'integer' } } }); diff --git a/test/unit/validator.test.js b/test/unit/validator.test.js index 24aa989..4e1f1fa 100644 --- a/test/unit/validator.test.js +++ b/test/unit/validator.test.js @@ -693,6 +693,59 @@ describe('VALIDATE FUNCTION TESTS ', function () { done(); }); }); + + it('Should be able to correctly validate composite schemas with anyOf, oneOf and allOf keywords correctly ' + + 'against corresponding transactions', function (done) { + let compositeSchemaSpec = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH + + '/compositeSchemaSpec.yaml'), 'utf-8'), + compositeSchemaCollection = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH + + '/compositeSchemaCollection.json'), 'utf-8'), + resultObjAnyOf, + resultObjOneOf, + resultObjAllOf, + historyRequest = [], + schemaPack = new Converter.SchemaPack({ type: 'string', data: compositeSchemaSpec }, + { suggestAvailableFixes: true, showMissingInSchemaErrors: true }); + + getAllTransactions(JSON.parse(compositeSchemaCollection), historyRequest); + + schemaPack.validateTransaction(historyRequest, (err, result) => { + expect(err).to.be.null; + expect(result).to.be.an('object'); + resultObjAnyOf = result.requests[historyRequest[0].id].endpoints[0]; + resultObjOneOf = result.requests[historyRequest[1].id].endpoints[0]; + resultObjAllOf = result.requests[historyRequest[2].id].endpoints[0]; + + /** + * no mismatches should be found here even though value present in collection + * is only valid as per 2nd element of anyOf keyword here + */ + expect(resultObjAnyOf.mismatches).to.have.lengthOf(0); + + /** + * no mismatches should be found here even though key present in collection request body + * is only valid as per 2nd element of oneOf keyword here + */ + expect(resultObjOneOf.mismatches).to.have.lengthOf(0); + + // + expect(resultObjAllOf.mismatches).to.have.lengthOf(1); + expect(resultObjAllOf.mismatches[0].reasonCode).to.eql('INVALID_BODY'); + expect(resultObjAllOf.mismatches[0].transactionJsonPath).to.eql('$.request.body'); + expect(resultObjAllOf.mismatches[0].schemaJsonPath).to + .eql('$.paths[/pets/allOf].post.requestBody.content[application/json].schema'); + expect(resultObjAllOf.mismatches[0].suggestedFix.actualValue).to.eql({ + objectType: 'not an integer', + objectType2: 'prop named objectType2' + }); + expect(resultObjAllOf.mismatches[0].suggestedFix.suggestedValue).to.eql({ + objectType: 4321, + objectType2: 'prop named objectType2' + }); + + done(); + }); + }); }); describe('getPostmanUrlSuffixSchemaScore function', function () {