diff --git a/lib/deref.js b/lib/deref.js index 5394ff2..aa21127 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -16,7 +16,17 @@ const _ = require('lodash'), 'date-time': '', password: '' }, - boolean: '' + boolean: '', + array: '', + object: '' + }, + SCHEMA_TYPES = { + array: 'array', + boolean: 'boolean', + integer: 'integer', + number: 'number', + object: 'object', + string: 'string' }, PARAMETER_SOURCE = { REQUEST: 'REQUEST', @@ -141,14 +151,6 @@ module.exports = { return { value: ERR_TOO_MANY_LEVELS }; } - // Update max stack reached for all current refs that's being resolved - if (!_.isEmpty(this._currentRefStack)) { - _.forEach(this._currentRefStack, (refKey) => { - _.set(schemaResolutionCache, [refKey, 'maxStack'], - Math.max(_.get(schemaResolutionCache, [refKey, 'maxStack'], 0), stack)); - }); - } - if (!schema) { return { value: '' }; } @@ -177,10 +179,6 @@ module.exports = { return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit); } - if (schema.additionalProperties && !_.isBoolean(schema.additionalProperties)) { - schema.additionalProperties = this.resolveRefs(schema.additionalProperties, parameterSourceOption, - components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit); - } if (schema.$ref && _.isFunction(schema.$ref.split)) { let refKey = schema.$ref, outerProperties = concreteUtils.getOuterPropsIfIsSupported(schema); @@ -201,20 +199,6 @@ module.exports = { return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' }; } - if (_.get(schemaResolutionCache, [refKey, 'schema'])) { - // maxStack for cached schema is how deep of nesting level we reached while resolving that schema - let maxStack = _.get(schemaResolutionCache, [refKey, 'maxStack'], 0), - // resLevel of perticuler cached schema is nesting level at which schema was resolved - resLevel = _.get(schemaResolutionCache, [refKey, 'resLevel'], stackLimit); - - /** - * use cached schema if it was resolved at level lower or equal then at current stack level or - * if cached schema is resolved fully (it does not contain ERR_TOO_MANY_LEVELS value in sub schema) - */ - if (resLevel <= stack || maxStack < stackLimit) { - return schemaResolutionCache[refKey].schema; - } - } // something like #/components/schemas/PaginationEnvelope/properties/page // will be resolved - we don't care about anything after the components part // splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page'] @@ -235,32 +219,33 @@ module.exports = { resolvedSchema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProperties); } if (resolvedSchema) { - // add current ref that's being resolved in ref stack - !_.isArray(this._currentRefStack) && (this._currentRefStack = []); - this._currentRefStack.push(refKey); - let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption, components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit); - // remove current ref that's being resolved from stack as soon as resolved - _.isArray(this._currentRefStack) && (this._currentRefStack.pop()); - - if (refResolvedSchema && refResolvedSchema.value !== ERR_TOO_MANY_LEVELS) { - _.set(schemaResolutionCache, [refKey, 'resLevel'], stack); - _.set(schemaResolutionCache, [refKey, 'schema'], refResolvedSchema); - } - return refResolvedSchema; } return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' }; } - if (concreteUtils.compareTypes(schema.type, 'objects') || schema.hasOwnProperty('properties')) { + if (concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.object) || schema.hasOwnProperty('properties') || + schema.hasOwnProperty('additionalProperties')) { // go through all props - schema.type = 'object'; - if (schema.hasOwnProperty('properties')) { + schema.type = SCHEMA_TYPES.object; + if (_.has(schema, 'properties') || _.has(schema, 'additionalProperties')) { // shallow cloning schema object except properties object - let tempSchema = _.omit(schema, 'properties'); - tempSchema.properties = {}; + let tempSchema = _.omit(schema, ['properties', 'additionalProperties']); + + if (_.has(schema, 'additionalProperties')) { + // don't resolve boolean values + if (_.isBoolean(schema.additionalProperties)) { + tempSchema.additionalProperties = schema.additionalProperties; + } + else { + tempSchema.additionalProperties = this.resolveRefs(schema.additionalProperties, parameterSourceOption, + components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit); + } + } + + !_.isEmpty(schema.properties) && (tempSchema.properties = {}); for (prop in schema.properties) { if (schema.properties.hasOwnProperty(prop)) { /* eslint-disable max-depth */ @@ -296,10 +281,10 @@ module.exports = { // Override deefault value to appropriate type representation for parameter resolution to schema if (resolveFor === 'CONVERSION' && resolveTo === 'schema') { - schema.default = ''; + schema.default = type.object; } } - else if (concreteUtils.compareTypes(schema.type, 'array') && schema.items) { + else if (concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.array) && schema.items) { /* For VALIDATION - keep minItems and maxItems properties defined by user in schema as is FOR CONVERSION - @@ -358,11 +343,11 @@ module.exports = { } else { return { - type: 'object' + type: SCHEMA_TYPES.object }; } if (!schema.type) { - schema.type = 'string'; + schema.type = SCHEMA_TYPES.string; } // Discard format if not supported by both json-schema-faker and ajv or pattern is also defined diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 3a9a520..c9e6377 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -406,7 +406,7 @@ module.exports = { let example = _.get(parameter, 'example'), examples = _.values(_.get(parameter, 'examples')); - if (example) { + if (example !== undefined) { _.set(parameter, 'schema.example', example); } else if (examples) { @@ -915,7 +915,7 @@ module.exports = { convertPathVariables: function(type, providedPathVars, commonPathVars, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); - var variables = providedPathVars; + var variables = []; // converting the base uri path variables, if any // commonPathVars is an object for type = root/method // array otherwise @@ -948,7 +948,8 @@ module.exports = { }); } - return variables; + // keep already provided varables (server variables) at last + return _.concat(variables, providedPathVars); }, /** @@ -1429,8 +1430,13 @@ module.exports = { else if (bodyObj.schema) { if (bodyObj.schema.hasOwnProperty('$ref')) { let outerProps = concreteUtils.getOuterPropsIfIsSupported(bodyObj.schema), + resolvedSchema; + + // skip beforehand resolution for OAS 3.0 + if (outerProps) { resolvedSchema = this.getRefObject(bodyObj.schema.$ref, components, options); - bodyObj.schema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProps); + bodyObj.schema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProps); + } } if (options.schemaFaker) { if (this.getHeaderFamily(contentType) === HEADER_TYPE.XML) { @@ -1438,7 +1444,7 @@ module.exports = { } // Do not fake schemas if the complexity score is 10 if (options.complexityScore === 10) { - schemaType = bodyObj.schema.type; + schemaType = _.get(this.getRefObject(bodyObj.schema.$ref, components, options), 'type'); if (schemaType === 'object') { return { value: '' @@ -1760,12 +1766,14 @@ module.exports = { // handling for the urlencoded media type if (contentObj.hasOwnProperty(URLENCODED)) { rDataMode = 'urlencoded'; - if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) { - contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options); - } bodyData = this.convertToPmBodyData(contentObj[URLENCODED], requestType, URLENCODED, PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache); encoding = contentObj[URLENCODED].encoding ? contentObj[URLENCODED].encoding : {}; + + if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) { + contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options); + } + // create query parameters and add it to the request body object _.forOwn(bodyData, (value, key) => { @@ -2054,6 +2062,10 @@ module.exports = { options = _.merge({}, defaultOptions, options); var refObj, savedSchema; + if (typeof $ref !== 'string') { + return { value: `Invalid $ref: ${$ref} was found` }; + } + savedSchema = $ref.split('/').slice(1).map((elem) => { // https://swagger.io/docs/specification/using-ref#escape // since / is the default delimiter, slashes are escaped with ~1 diff --git a/test/data/valid_openapi/issue#10229.json b/test/data/valid_openapi/issue#10229.json new file mode 100644 index 0000000..5dbc306 --- /dev/null +++ b/test/data/valid_openapi/issue#10229.json @@ -0,0 +1,194 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "#10229", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "servers": [ + { + "url": "http://petstore.swagger.io:{port}/{basePath}", + "variables": { + "port": { + "enum": [ + "8443", + "443" + ], + "default": "8443" + }, + "basePath": { + "default": "v2" + } + } + } + ], + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 0 + }, + { + "name": "otherString", + "in": "query", + "description": "other parameter", + "required": false, + "schema": { + "type": "string" + }, + "example": "" + }, + { + "name": "otherBoolean", + "in": "query", + "description": "other parameter", + "required": false, + "schema": { + "type": "boolean" + }, + "example": false + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "a": { + "type": "null", + "example": null + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "a": { + "type": "null", + "example": null + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/test/data/valid_openapi/petstore-detailed.yaml b/test/data/valid_openapi/petstore-detailed.yaml index 8346a2d..ecfdcef 100644 --- a/test/data/valid_openapi/petstore-detailed.yaml +++ b/test/data/valid_openapi/petstore-detailed.yaml @@ -424,9 +424,10 @@ paths: application/json: schema: type: object - additionalProperties: - type: integer - format: int32 + properties: + prop123: + type: integer + format: int32 security: - api_key: [] /store/order: diff --git a/test/data/valid_openapi/readOnly.json b/test/data/valid_openapi/readOnly.json index a9c2b9c..b711591 100644 --- a/test/data/valid_openapi/readOnly.json +++ b/test/data/valid_openapi/readOnly.json @@ -34,7 +34,20 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Pet" + "properties": { + "id": { + "type": "integer", + "format": "int64", + "readOnly": true + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string", + "writeOnly": true + } + } } } } diff --git a/test/unit/base.test.js b/test/unit/base.test.js index e59817c..7317cd3 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -43,7 +43,8 @@ describe('CONVERT FUNCTION TESTS ', function() { securityTestCases = path.join(__dirname, VALID_OPENAPI_PATH + '/security-test-cases.yaml'), emptySecurityTestCase = path.join(__dirname, VALID_OPENAPI_PATH + '/empty-security-test-case.yaml'), rootUrlServerWithVariables = path.join(__dirname, VALID_OPENAPI_PATH + '/root_url_server_with_variables.json'), - parameterExamples = path.join(__dirname, VALID_OPENAPI_PATH + '/parameteres_with_examples.yaml'); + parameterExamples = path.join(__dirname, VALID_OPENAPI_PATH + '/parameteres_with_examples.yaml'), + issue10229 = path.join(__dirname, VALID_OPENAPI_PATH, '/issue#10229.json'); it('Should add collection level auth with type as `bearer`' + @@ -393,6 +394,25 @@ describe('CONVERT FUNCTION TESTS ', function() { done(); }); }); + it('#GITHUB-10229 should generate correct example is out of the schema and is falsy' + + issue10229, function(done) { + var openapi = fs.readFileSync(issue10229, 'utf8'); + Converter.convert({ type: 'string', data: openapi }, { requestParametersResolution: 'Example' }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item[0].request.url.query[0].value).to.equal('0'); + expect(conversionResult.output[0].data.item[0].item[0].request.url.query[1].value).to.equal(''); + expect(conversionResult.output[0].data.item[0].item[0].request.url.query[2].value).to.equal('false'); + expect(conversionResult.output[0].data.item[0].item[1].request.body.raw).to.equal('{\n "a": null\n}'); + expect(conversionResult.output[0].data.item[0].item[1].response[1].body).to.equal('{\n "a": null\n}'); + done(); + }); + }); describe('[Github #108]- Parameters resolution option', function() { it('Should respect schema faking for root request and example for example request' + examplesInSchemaSpec, function(done) { diff --git a/test/unit/deref.test.js b/test/unit/deref.test.js index 5dbb0bc..ebf6168 100644 --- a/test/unit/deref.test.js +++ b/test/unit/deref.test.js @@ -100,6 +100,14 @@ describe('DEREF FUNCTION TESTS ', function() { name: { type: 'string' } + }, + additionalProperties: { + type: 'object', + properties: { + hello: { + type: 'string' + } + } } } } @@ -119,7 +127,8 @@ describe('DEREF FUNCTION TESTS ', function() { _.cloneDeep(componentsAndPaths), {}, 'VALIDATION'), output_emptyObject = deref.resolveRefs(schemaWithEmptyObject, parameterSource, _.cloneDeep(componentsAndPaths)), output_additionalProps = deref.resolveRefs(schemaWithAdditionPropRef, parameterSource, - _.cloneDeep(componentsAndPaths), {}, 'VALIDATION'); + _.cloneDeep(componentsAndPaths), {}, 'VALIDATION'), + output_additionalPropsOverride; expect(output).to.deep.include({ type: 'object', required: ['id'], @@ -170,42 +179,15 @@ describe('DEREF FUNCTION TESTS ', function() { // additionalProperties $ref should be resolved expect(output_additionalProps).to.deep.include(componentsAndPaths.components.schemas.schemaAdditionalProps); - done(); - }); + // add default to above resolved schema + output_additionalProps.additionalProperties.properties.hello.default = ''; - it('should populate schemaResolutionCache having key as the ref provided', function (done) { - var schema = { - $ref: '#/components/schema/request' - }, - componentsAndPaths = { - components: { - schema: { - request: { - properties: { - name: { - type: 'string', - example: 'example name' - } - } - } - } - }, - concreteUtils: schemaUtils30X - }, - parameterSource = 'REQUEST', - schemaResolutionCache = {}, - resolvedSchema = deref.resolveRefs(schema, parameterSource, componentsAndPaths, schemaResolutionCache); - expect(_.get(schemaResolutionCache, ['#/components/schema/request', 'schema'])).to.deep.equal(resolvedSchema); - expect(resolvedSchema).to.deep.equal({ - type: 'object', - properties: { - name: { - type: 'string', - example: 'example name', - default: '' - } - } - }); + output_additionalPropsOverride = deref.resolveRefs(schemaWithAdditionPropRef, parameterSource, + _.cloneDeep(componentsAndPaths), {}, 'VALIDATION'); + + // override should not affect newly resolved schema + expect(output_additionalPropsOverride).to.deep.include( + componentsAndPaths.components.schemas.schemaAdditionalProps); done(); }); @@ -274,90 +256,6 @@ describe('DEREF FUNCTION TESTS ', function() { done(); }); - it('should correctly resolve schema from schemaResoltionCache based on schema resolution level', function (done) { - let schema = { - $ref: '#/components/schemas/schemaUsed' - }, - consumerSchema = { - type: 'object', - properties: { level2: { - type: 'object', - properties: { level3: { - type: 'object', - properties: { level4: { - type: 'object', - properties: { level5: { - type: 'object', - properties: { level6: { - type: 'object', - properties: { level7: { - type: 'object', - properties: { level8: { - type: 'object', - properties: { level9: { $ref: '#/components/schemas/schemaUsed' } } - } } - } } - } } - } } - } } - } } - } } - }, - componentsAndPaths = { - components: { - schemas: { - schemaUsed: { - 'type': 'object', - 'required': [ - 'id', - 'name' - ], - 'properties': { - 'id': { - 'type': 'integer', - 'format': 'int64' - }, - 'name': { - 'type': 'string' - }, - 'tag': { - 'type': 'string' - } - } - } - } - }, - concreteUtils: schemaUtils30X - }, - parameterSource = 'REQUEST', - schemaResoltionCache = {}, - resolvedConsumerSchema, - resolvedSchema; - - resolvedConsumerSchema = deref.resolveRefs(consumerSchema, parameterSource, componentsAndPaths, - schemaResoltionCache); - - // Consumer schema contains schema at nesting level 9, which results in impartial resolution of schema - expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'resLevel'])).to.eql(9); - expect(_.get(resolvedConsumerSchema, _.join(_.map(_.range(1, 10), (ele) => { - return `properties.level${ele}`; - }), '.'))).to.not.deep.equal(componentsAndPaths.components.schemas.schemaUsed); - expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'schema'])).to.not.deep - .equal(componentsAndPaths.components.schemas.schemaUsed); - resolvedSchema = deref.resolveRefs(schema, parameterSource, componentsAndPaths, schemaResoltionCache); - // Restoring the original format as it is deleted if not supported by json-schema-faker and ajv - resolvedSchema.properties.id.format = 'int64'; - - /** - * Even though schema cache contains schemaUsed as impartially cached,resolution were it's used again will - * depend on ongoing resolution level and schema is cached again if it's updated. - */ - expect(resolvedSchema).to.deep.equal(componentsAndPaths.components.schemas.schemaUsed); - expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'schema'])).to.deep - .equal(componentsAndPaths.components.schemas.schemaUsed); - done(); - }); - it('should not contain readOnly properties in resolved schema if they are not contained' + ' in resolved schema', function(done) { var schema = {