Added support for internal $ref resolution in validation flows

This commit is contained in:
Vishal Shingala
2020-09-01 17:55:51 +05:30
parent 55ee719a23
commit 1215d9de15
5 changed files with 427 additions and 1 deletions

View File

@@ -3504,13 +3504,20 @@ module.exports = {
components, options, schemaCache, callback) { components, options, schemaCache, callback) {
// check for body modes // check for body modes
// TODO: The application/json can be anything that's application/*+json // TODO: The application/json can be anything that's application/*+json
let jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']), let jsonSchemaBody,
mismatchProperty = 'BODY'; mismatchProperty = 'BODY';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []); return callback(null, []);
} }
// resolve $ref in requestBody object if present
if (!_.isEmpty(_.get(schemaPath, 'requestBody.$ref'))) {
schemaPath.requestBody = this.getRefObject(schemaPath.requestBody.$ref, components, options);
}
jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']);
// only raw for now // only raw for now
if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) { if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) {
setTimeout(() => { setTimeout(() => {
@@ -3589,6 +3596,19 @@ module.exports = {
responsePathPrefix = 'default'; responsePathPrefix = 'default';
} }
// resolve $ref in response object if present
if (!_.isEmpty(_.get(thisSchemaResponse, '$ref'))) {
thisSchemaResponse = this.getRefObject(thisSchemaResponse.$ref, components, options);
}
// resolve $ref in all header objects if present
_.forEach(_.get(thisSchemaResponse, 'headers'), (header) => {
if (header.hasOwnProperty('$ref')) {
_.assign(header, this.getRefObject(header.$ref, components, options));
_.unset(header, '$ref');
}
});
if (!thisSchemaResponse) { if (!thisSchemaResponse) {
// still didn't find a response // still didn't find a response
responseCallback(null); responseCallback(null);

View File

@@ -447,6 +447,14 @@ class SchemaPack {
pathVar.value = _.get(mappedPathVar, 'value', pathVar.value); pathVar.value = _.get(mappedPathVar, 'value', pathVar.value);
}); });
// resolve $ref in all parameter objects if present
_.forEach(_.get(matchedPath, 'path.parameters'), (param) => {
if (param.hasOwnProperty('$ref')) {
_.assign(param, schemaUtils.getRefObject(param.$ref, componentsAndPaths, options));
_.unset(param, '$ref');
}
});
matchedEndpoints.push(matchedPath.jsonPath); matchedEndpoints.push(matchedPath.jsonPath);
// 3. validation involves checking these individual properties // 3. validation involves checking these individual properties
async.parallel({ async.parallel({

View File

@@ -0,0 +1,175 @@
{
"item": [
{
"id": "9b8ff406-3176-49ca-b91a-e99277130d40",
"name": "searches inventory",
"request": {
"name": "searches inventory",
"description": {
"content": "By passing in the appropriate options, you can search for\navailable inventory in the system\n",
"type": "text/plain"
},
"url": {
"path": [
"inventory",
":searchString"
],
"host": [
"{{baseUrl}}"
],
"query": [
{
"description": "number of records to skip for pagination",
"key": "skip",
"value": "71616628"
}
],
"variable": [
{
"description": "pass an optional search string for looking up inventory",
"type": "any",
"value": "magna",
"key": "searchString"
}
]
},
"header": [
{
"description": "maximum number of records to return",
"key": "limit",
"value": "25"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"d290f1ee-6c54-4b01-90e6-d701748f0851\",\n \"name\": \"Widget Adapter\",\n \"manufacturer\": {\n \"name\": \"ACME Corporation\",\n \"homePage\": \"https://www.acme-corp.com\",\n \"phone\": \"408-867-5309\"\n },\n \"releaseDate\": \"2016-08-29T09:12:33.001Z\"\n}"
}
},
"response": [
{
"id": "103e959b-3d59-4a3b-9a0f-eef9206733e9",
"name": "An array of profiles",
"originalRequest": {
"url": {
"path": [
"inventory",
":searchString"
],
"host": [
"{{baseUrl}}"
],
"query": [
{
"key": "skip",
"value": "<integer>"
}
],
"variable": [
{
"type": "any",
"key": "searchString"
}
]
},
"header": [
{
"description": "maximum number of records to return",
"key": "limit",
"value": "<integer>"
}
],
"method": "POST",
"body": {}
},
"status": "OK",
"code": 200,
"header": [
{
"description": "The date.",
"key": "x-date",
"value": "2012-06-15"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": "{\n \"id\": \"urn:uuid:986f1de0-b002-2c4e-63a7-724ac665365b\",\n \"name\": \"ioneyed\",\n \"given_name\": \"Robert\",\n \"family_name\": \"Buchanan\"\n}",
"cookie": [],
"_postman_previewlanguage": "json"
},
{
"id": "7e2062e0-d928-4ae5-924b-858d3bcce494",
"name": "The user is unauthorized for this action",
"originalRequest": {
"url": {
"path": [
"inventory",
":searchString"
],
"host": [
"{{baseUrl}}"
],
"query": [
{
"key": "skip",
"value": "<integer>"
}
],
"variable": [
{
"type": "any",
"key": "searchString"
}
]
},
"header": [
{
"description": "maximum number of records to return",
"key": "limit",
"value": "<integer>"
}
],
"method": "POST",
"body": {}
},
"status": "Unauthorized",
"code": 401,
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": "{\n \"code\": \"PROFILE-108-401\",\n \"message\": \"you do not have appropriate credentials\"\n}",
"cookie": [],
"_postman_previewlanguage": "json"
}
],
"event": []
}
],
"event": [],
"variable": [
{
"id": "baseUrl",
"type": "string",
"value": "https://example.com"
}
],
"info": {
"_postman_id": "758b4b2c-df93-4250-a287-6e526f194a75",
"name": "Simple Inventory API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": {
"content": "This is a simple API\n\nContact Support:\n Email: you@your-company.com",
"type": "text/plain"
}
}
}

View File

@@ -0,0 +1,188 @@
openapi: 3.0.0
servers:
- description: Internal $refs
url: https://example.com
info:
description: This is a simple API
version: "1.0.0"
title: Simple Inventory API
contact:
email: you@your-company.com
license:
name: Apache 2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
paths:
/inventory/{searchString}:
parameters:
- $ref: '#/components/parameters/searchString'
post:
summary: searches inventory
operationId: searchInventory
description: |
By passing in the appropriate options, you can search for
available inventory in the system
parameters:
- $ref: '#/components/parameters/skip'
- $ref: '#/components/parameters/limit'
requestBody:
$ref: '#/components/requestBodies/inventoryBody'
responses:
'200':
description: 'An array of profiles'
content:
application/json:
schema:
$ref: '#/components/schemas/Profile'
headers:
x-date:
$ref: '#/components/headers/x-date'
'401':
$ref: '#/components/responses/Unauthorized'
components:
headers:
x-date:
description: The date.
schema:
type: string
format: date
parameters:
limit:
in: header
name: limit
description: maximum number of records to return
schema:
type: integer
format: int32
minimum: 0
maximum: 50
searchString:
in: path
name: searchString
description: pass an optional search string for looking up inventory
required: false
schema:
$ref: '#/components/schemas/SearchString'
skip:
in: query
name: skip
description: number of records to skip for pagination
schema:
type: integer
format: int32
minimum: 0
requestBodies:
inventoryBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InventoryItem'
description: Inventory item to add
responses:
UnexpectedError:
description: An unexpected error has occured
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: 'PROFILE-107-500'
message: 'fixing problems'
Unauthorized:
description: The user is unauthorized for this action
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: 'PROFILE-108-401'
message: 'you do not have appropriate credentials'
Forbidden:
description: The user in forbidden from completing that action
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: 'PROFILE-109-403'
message: 'thou shall not pass for you are forbidden'
NotFound:
description: The resource was not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: 'PROFILE-110-404'
message: 'resource was not found'
schemas:
SearchString:
type: string
InventoryItem:
type: object
required:
- id
- name
- manufacturer
- releaseDate
properties:
id:
type: string
format: uuid
example: d290f1ee-6c54-4b01-90e6-d701748f0851
name:
type: string
example: Widget Adapter
releaseDate:
type: string
format: date-time
example: '2016-08-29T09:12:33.001Z'
manufacturer:
$ref: '#/components/schemas/Manufacturer'
Manufacturer:
required:
- name
properties:
name:
type: string
example: ACME Corporation
homePage:
type: string
format: url
example: 'https://www.acme-corp.com'
phone:
type: string
example: 408-867-5309
type: object
Profile:
type: object
required:
- id
- name
- given_name
- family_name
properties:
id:
type: string
format: uuid
name:
type: string
example: "ioneyed"
given_name:
type: string
example: "Robert"
family_name:
type: string
example: "Buchanan"
Error:
type: object
required:
- code
- message
properties:
code:
type: string
pattern: 'PROFILE-\d{3}-\d{3}'
example: "PROFILE-100-507"
message:
type: string
example: "we have run out of storage; this is embarrassing, and someone have been paged"

View File

@@ -480,5 +480,40 @@ describe('VALIDATE FUNCTION TESTS ', function () {
}); });
}); });
}); });
it('Should correctly handle internal $ref when present', function (done) {
let internalRefsSpec = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH +
'/internalRefsSpec.yaml'), 'utf-8'),
internalRefsCollection = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH +
'/internalRefsCollection.json'), 'utf-8'),
resultObj,
options = {
showMissingInSchemaErrors: true,
strictRequestMatching: true,
ignoreUnresolvedVariables: true,
validateMetadata: true,
suggestAvailableFixes: true,
detailedBlobValidation: false
},
historyRequest = [],
schemaPack = new Converter.SchemaPack({ type: 'string', data: internalRefsSpec }, options);
getAllTransactions(JSON.parse(internalRefsCollection), historyRequest);
schemaPack.validateTransaction(historyRequest, (err, result) => {
expect(err).to.be.null;
expect(result).to.be.an('object');
resultObj = result.requests[historyRequest[0].id].endpoints[0];
// no mismatches should be found when resolved correctly
expect(resultObj.matched).to.be.true;
expect(resultObj.mismatches).to.have.lengthOf(0);
_.forEach(resultObj.responses, (response) => {
expect(response.matched).to.be.true;
expect(response.mismatches).to.have.lengthOf(0);
});
done();
});
});
}); });
}); });