diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index e7f7d94..b809c18 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -3504,13 +3504,20 @@ module.exports = { components, options, schemaCache, callback) { // check for body modes // TODO: The application/json can be anything that's application/*+json - let jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']), + let jsonSchemaBody, mismatchProperty = 'BODY'; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { 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 if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) { setTimeout(() => { @@ -3589,6 +3596,19 @@ module.exports = { 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) { // still didn't find a response responseCallback(null); diff --git a/lib/schemapack.js b/lib/schemapack.js index 1f35420..af81a7e 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -447,6 +447,14 @@ class SchemaPack { 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); // 3. validation involves checking these individual properties async.parallel({ diff --git a/test/data/validationData/internalRefsCollection.json b/test/data/validationData/internalRefsCollection.json new file mode 100644 index 0000000..0254fd6 --- /dev/null +++ b/test/data/validationData/internalRefsCollection.json @@ -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": "" + } + ], + "variable": [ + { + "type": "any", + "key": "searchString" + } + ] + }, + "header": [ + { + "description": "maximum number of records to return", + "key": "limit", + "value": "" + } + ], + "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": "" + } + ], + "variable": [ + { + "type": "any", + "key": "searchString" + } + ] + }, + "header": [ + { + "description": "maximum number of records to return", + "key": "limit", + "value": "" + } + ], + "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" + } + } + } \ No newline at end of file diff --git a/test/data/validationData/internalRefsSpec.yaml b/test/data/validationData/internalRefsSpec.yaml new file mode 100644 index 0000000..9fa1791 --- /dev/null +++ b/test/data/validationData/internalRefsSpec.yaml @@ -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" \ No newline at end of file diff --git a/test/unit/validator.test.js b/test/unit/validator.test.js index 6881d30..241abb2 100644 --- a/test/unit/validator.test.js +++ b/test/unit/validator.test.js @@ -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(); + }); + }); }); });