Added support for anyOf and oneOf keywords in validation flow

This commit is contained in:
Vishal Shingala
2021-10-21 00:00:48 +05:30
parent d97ca06c55
commit 65d7e9af23
7 changed files with 447 additions and 9 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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: '<long>', 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: '<long>', type: 'integer' } } });

View File

@@ -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 () {