Skip to content

Commit 132987e

Browse files
authored
Merge branch 'develop' into fix/formatingBodyRawOutputWhenIsNotAnObject
2 parents cba96d4 + 808eefc commit 132987e

28 files changed

+2768
-87
lines changed

assets/json-schema-faker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23782,7 +23782,7 @@ function extend() {
2378223782
var min = Math.max(params.minimum || 0, 0);
2378323783
var max = Math.min(params.maximum || Infinity, Infinity);
2378423784
min = handleExclusiveMinimum(schema, min);
23785-
max = handleExclusiveMaximum(schema, min);
23785+
max = handleExclusiveMaximum(schema, max);
2378623786
// discard out-of-bounds enumerations
2378723787
schema.enum = schema.enum.filter(function (x) {
2378823788
if (x >= min && x <= max) {

lib/bundle.js

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let path = require('path'),
2222
{ DFS } = require('./dfs'),
2323
deref = require('./deref.js'),
2424
{ isSwagger, getBundleRulesDataByVersion } = require('./common/versionUtils'),
25-
CIRCULAR_REF_EXT_PROP = 'x-circularRef';
25+
CIRCULAR_OR_REF_EXT_PROP = 'x-orRef';
2626

2727

2828
/**
@@ -481,6 +481,34 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac
481481
return relatedRef;
482482
}
483483

484+
/**
485+
* Verifies if a node has same content as one of the parents so it is a circular ref
486+
* @param {function} traverseContext - The context of the traverse function
487+
* @param {object} contentFromTrace - The resolved content of the node to deref
488+
* @returns {boolean} whether is circular reference or not.
489+
*/
490+
function isCircularReference(traverseContext, contentFromTrace) {
491+
return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined;
492+
}
493+
494+
/**
495+
* Modifies content of a node if it is circular reference.
496+
*
497+
* @param {function} traverseContext - The context of the traverse function
498+
* @param {object} documentContext The document context from root
499+
* @returns {undefined} nothing
500+
*/
501+
function handleCircularReference(traverseContext, documentContext) {
502+
let relatedRef = '';
503+
if (traverseContext.circular) {
504+
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, traverseContext.circular.key);
505+
traverseContext.update({ $ref: relatedRef });
506+
}
507+
if (traverseContext.keys && traverseContext.keys.includes(CIRCULAR_OR_REF_EXT_PROP)) {
508+
traverseContext.update({ $ref: traverseContext.node[CIRCULAR_OR_REF_EXT_PROP] });
509+
}
510+
}
511+
484512
/**
485513
* Generates the components object from the documentContext data
486514
* @param {object} documentContext The document context from root
@@ -492,8 +520,9 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac
492520
*/
493521
function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) {
494522
let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => {
495-
return value.keyInComponents.length !== 0;
496-
});
523+
return value.keyInComponents.length !== 0;
524+
}),
525+
circularRefsSet = new Set();
497526
const { COMPONENTS_KEYS } = getBundleRulesDataByVersion(version);
498527
notInLine.forEach(([key, value]) => {
499528
let [, partial] = key.split(localPointer);
@@ -541,6 +570,17 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
541570
if (!contentFromTrace) {
542571
refData.nodeContent = { $ref: `${localPointer + local}` };
543572
}
573+
else if (isCircularReference(this, contentFromTrace)) {
574+
if (refData.inline) {
575+
refData.nodeContent = { [CIRCULAR_OR_REF_EXT_PROP]: tempRef };
576+
circularRefsSet.add(tempRef);
577+
}
578+
else {
579+
refData.node = { [CIRCULAR_OR_REF_EXT_PROP]: refData.reference };
580+
refData.nodeContent = contentFromTrace;
581+
circularRefsSet.add(refData.reference);
582+
}
583+
}
544584
else {
545585
refData.nodeContent = contentFromTrace;
546586
}
@@ -551,39 +591,30 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
551591
refData.node = hasSiblings ?
552592
_.merge(referenceSiblings, refData.nodeContent) :
553593
refData.nodeContent;
554-
documentContext.globalReferences[property.$ref].reference =
594+
documentContext.globalReferences[tempRef].reference =
555595
resolveJsonPointerInlineNodes(this.parents, this.key);
556596
}
557-
this.update(refData.node);
558-
if (!refData.inline) {
559-
if (documentContext.globalReferences[tempRef].refHasContent) {
560-
setValueInComponents(
561-
refData.keyInComponents,
562-
components,
563-
refData.nodeContent,
564-
COMPONENTS_KEYS
565-
);
566-
}
597+
else if (refData.refHasContent) {
598+
setValueInComponents(
599+
refData.keyInComponents,
600+
components,
601+
refData.nodeContent,
602+
COMPONENTS_KEYS
603+
);
567604
}
605+
this.update(refData.node);
568606
}
569607
}
570608
});
571609
});
572610
return {
573611
resRoot: traverseUtility(rootContent).map(function () {
574-
let relatedRef = '';
575-
if (this.circular) {
576-
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
577-
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
578-
}
612+
handleCircularReference(this, documentContext);
579613
}),
580614
newComponents: traverseUtility(components).map(function () {
581-
let relatedRef = '';
582-
if (this.circular) {
583-
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
584-
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
585-
}
586-
})
615+
handleCircularReference(this, documentContext);
616+
}),
617+
circularRefs: [...circularRefsSet]
587618
};
588619
}
589620

@@ -618,7 +649,7 @@ function generateComponentsWrapper(parsedOasObject, version, nodesContent = {})
618649
}
619650

620651
/**
621-
* Generates a map of generated refernce to the original reference
652+
* Generates a map of generated reference to the original reference
622653
*
623654
* @param {object} globalReferences - Global references present at each root file context
624655
* @returns {object} reference map
@@ -712,7 +743,8 @@ module.exports = {
712743
fileContent: finalElements.resRoot,
713744
components: finalElements.newComponents,
714745
fileName: specRoot.fileName,
715-
referenceMap: getReferenceMap(rootContextData.globalReferences)
746+
referenceMap: getReferenceMap(rootContextData.globalReferences),
747+
circularRefs: finalElements.circularRefs
716748
};
717749
},
718750
getReferences,

lib/common/schemaUtilsCommon.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,5 +345,16 @@ module.exports = {
345345

346346
isKnownType: function(schema) {
347347
return typeof schemaTypeToJsValidator[schema.type] === 'function';
348+
},
349+
350+
getServersPathVars: function(servers) {
351+
return servers.reduce((acc, current) => {
352+
const newVarNames = current.hasOwnProperty('variables') ?
353+
Object.keys(current.variables).filter((varName) => {
354+
return !acc.includes(varName);
355+
}) :
356+
[];
357+
return [...acc, ...newVarNames];
358+
}, []);
348359
}
349360
};

lib/schemaUtils.js

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,6 +1828,10 @@ module.exports = {
18281828
bodyData = this.convertToPmBodyData(contentObj[FORM_DATA], requestType, FORM_DATA,
18291829
PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
18301830
encoding = contentObj[FORM_DATA].encoding ? contentObj[FORM_DATA].encoding : {};
1831+
1832+
if (contentObj[FORM_DATA].hasOwnProperty('schema') && contentObj[FORM_DATA].schema.hasOwnProperty('$ref')) {
1833+
contentObj[FORM_DATA].schema = this.getRefObject(contentObj[FORM_DATA].schema.$ref, components, options);
1834+
}
18311835
// create the form parameters and add it to the request body object
18321836
_.forOwn(bodyData, (value, key) => {
18331837

@@ -2626,11 +2630,15 @@ module.exports = {
26262630
matchedPath,
26272631
matchedPathJsonPath,
26282632
schemaPathItems = schema.paths,
2633+
pathToMatchServer,
26292634
filteredPathItemsArray = [];
26302635

26312636
// Return no matches for invalid url (if unable to decode parsed url)
26322637
try {
26332638
pathToMatch = decodeURI(parsedUrl.pathname);
2639+
if (!_.isNil(parsedUrl.hash)) {
2640+
pathToMatch += parsedUrl.hash;
2641+
}
26342642
}
26352643
catch (e) {
26362644
console.warn(
@@ -2669,9 +2677,16 @@ module.exports = {
26692677
}
26702678
return accumulator;
26712679
}, []);
2672-
2680+
let schemaMatchResult = { match: false };
26732681
// check if path and pathToMatch match (non-null)
2674-
let schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatch, path, options);
2682+
// check in explicit (local defined) servers
2683+
if (pathItemObject[method.toLowerCase()].servers) {
2684+
pathToMatchServer = this.handleExplicitServersPathToMatch(pathToMatch, path);
2685+
schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatchServer, path, options);
2686+
}
2687+
else {
2688+
schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatch, path, options);
2689+
}
26752690
if (!schemaMatchResult.match) {
26762691
// there was no reasonable match b/w the postman path and this schema path
26772692
return true;
@@ -3337,7 +3352,7 @@ module.exports = {
33373352

33383353
/**
33393354
*
3340-
* @param {*} determinedPathVariables the key/determined-value pairs of the path variables (from Postman)
3355+
* @param {*} matchedPathData the matchedPath data
33413356
* @param {*} transactionPathPrefix the jsonpath for this validation (will be prepended to all identified mismatches)
33423357
* @param {*} schemaPath the applicable pathItem defined at the schema level
33433358
* @param {*} components the components + paths from the OAS spec that need to be used to resolve $refs
@@ -3348,7 +3363,7 @@ module.exports = {
33483363
* @returns {array} mismatches (in the callback)
33493364
*/
33503365
checkPathVariables: function (
3351-
determinedPathVariables,
3366+
matchedPathData,
33523367
transactionPathPrefix,
33533368
schemaPath,
33543369
components,
@@ -3362,7 +3377,9 @@ module.exports = {
33623377
var mismatchProperty = 'PATHVARIABLE',
33633378
// all path variables defined in this path. acc. to the spec, all path params are required
33643379
schemaPathVariables,
3365-
pmPathVariables;
3380+
pmPathVariables,
3381+
determinedPathVariables = matchedPathData.pathVariables,
3382+
unmatchedVariablesFromTransaction = matchedPathData.unmatchedVariablesFromTransaction;
33663383

33673384
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
33683385
return callback(null, []);
@@ -3428,17 +3445,41 @@ module.exports = {
34283445
}, (err, res) => {
34293446
let mismatches = [],
34303447
mismatchObj;
3448+
const unmatchedSchemaVariableNames = determinedPathVariables.filter((pathVariable) => {
3449+
return !pathVariable._varMatched;
3450+
}).map((schemaPathVar) => {
3451+
return schemaPathVar.key;
3452+
});
34313453

34323454
if (err) {
34333455
return callback(err);
34343456
}
34353457

34363458
// go through required schemaPathVariables, and params that aren't found in the given transaction are errors
3437-
_.each(schemaPathVariables, (pathVar) => {
3459+
_.each(schemaPathVariables, (pathVar, index) => {
34383460
if (!_.find(determinedPathVariables, (param) => {
34393461
// only consider variable matching if url path variables is not allowed
34403462
return param.key === pathVar.name && (options.allowUrlPathVarMatching || param._varMatched);
34413463
})) {
3464+
let reasonCode = 'MISSING_IN_REQUEST',
3465+
reason,
3466+
actualValue,
3467+
currentUnmatchedVariableInTransaction = unmatchedVariablesFromTransaction[index],
3468+
isInvalidValue = currentUnmatchedVariableInTransaction !== undefined;
3469+
3470+
if (unmatchedSchemaVariableNames.length > 0 && isInvalidValue) {
3471+
reason = `The ${currentUnmatchedVariableInTransaction.key} path variable does not match with ` +
3472+
`path variable expected (${unmatchedSchemaVariableNames[index]}) in the schema at this position`;
3473+
actualValue = {
3474+
key: currentUnmatchedVariableInTransaction.key,
3475+
description: this.getParameterDescription(currentUnmatchedVariableInTransaction),
3476+
value: currentUnmatchedVariableInTransaction.value
3477+
};
3478+
}
3479+
else {
3480+
reason = `The required path variable "${pathVar.name}" was not found in the transaction`;
3481+
actualValue = null;
3482+
}
34423483

34433484
// assign parameter example(s) as schema examples;
34443485
this.assignParameterExamples(pathVar);
@@ -3447,14 +3488,14 @@ module.exports = {
34473488
property: mismatchProperty,
34483489
transactionJsonPath: transactionPathPrefix,
34493490
schemaJsonPath: pathVar.pathPrefix,
3450-
reasonCode: 'MISSING_IN_REQUEST',
3451-
reason: `The required path variable "${pathVar.name}" was not found in the transaction`
3491+
reasonCode,
3492+
reason
34523493
};
34533494

34543495
if (options.suggestAvailableFixes) {
34553496
mismatchObj.suggestedFix = {
34563497
key: pathVar.name,
3457-
actualValue: null,
3498+
actualValue,
34583499
suggestedValue: {
34593500
key: pathVar.name,
34603501
value: safeSchemaFaker(pathVar.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
@@ -4454,6 +4495,32 @@ module.exports = {
44544495
});
44554496
},
44564497

4498+
/**
4499+
* Takes in the postman path and the schema path
4500+
* takes from the path the number of segments present in the schema path
4501+
* and returns the last segments from the path to match in an string format
4502+
*
4503+
* @param {string} pathToMatch - parsed path (exclude host and params) from the Postman request
4504+
* @param {string} schemaPath - schema path from the OAS spec (exclude servers object)
4505+
* @returns {string} only the selected segments from the pathToMatch
4506+
*/
4507+
handleExplicitServersPathToMatch: function (pathToMatch, schemaPath) {
4508+
let pathTMatchSlice,
4509+
schemaPathArr = _.reject(schemaPath.split('/'), (segment) => {
4510+
return segment === '';
4511+
}),
4512+
schemaPathSegments = schemaPathArr.length,
4513+
pathToMatchArr = _.reject(pathToMatch.split('/'), (segment) => {
4514+
return segment === '';
4515+
}),
4516+
pathToMatchSegments = pathToMatchArr.length;
4517+
if (pathToMatchSegments < schemaPathSegments) {
4518+
return pathToMatch;
4519+
}
4520+
pathTMatchSlice = pathToMatchArr.slice(pathToMatchArr.length - schemaPathSegments, pathToMatchArr.length);
4521+
return pathTMatchSlice.join('/');
4522+
},
4523+
44574524
/**
44584525
* @param {string} postmanPath - parsed path (exclude host and params) from the Postman request
44594526
* @param {string} schemaPath - schema path from the OAS spec (exclude servers object)

lib/schemapack.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
43
// This is the default collection name if one can't be inferred from the OpenAPI spec
54
const COLLECTION_NAME = 'Imported from OpenAPI 3.0',
65
{ getConcreteSchemaUtils } = require('./common/versionUtils.js'),
@@ -26,7 +25,8 @@ const COLLECTION_NAME = 'Imported from OpenAPI 3.0',
2625
// This provides the base class for
2726
// errors with the input OpenAPI spec
2827
OpenApiErr = require('./error.js'),
29-
schemaUtils = require('./schemaUtils');
28+
schemaUtils = require('./schemaUtils'),
29+
{ getServersPathVars } = require('./common/schemaUtilsCommon.js');
3030

3131
let path = require('path'),
3232
concreteUtils,
@@ -495,12 +495,29 @@ class SchemaPack {
495495
return setTimeout(() => {
496496
// 2. perform validation for each identified matchedPath (schema endpoint)
497497
return async.map(matchedPaths, (matchedPath, pathsCallback) => {
498+
const transactionPathVariables = _.get(transaction, 'request.url.variable', []),
499+
localServers = matchedPath.path.hasOwnProperty('servers') ?
500+
matchedPath.path.servers :
501+
[],
502+
serversPathVars = [...getServersPathVars(localServers), ...getServersPathVars(schema.servers)],
503+
isNotAServerPathVar = (pathVarName) => {
504+
return !serversPathVars.includes(pathVarName);
505+
};
498506

507+
matchedPath.unmatchedVariablesFromTransaction = [];
499508
// override path variable value with actual value present in transaction
500509
// as matched pathvariable contains key as value, as it is generated from url only
501510
_.forEach(matchedPath.pathVariables, (pathVar) => {
502-
let mappedPathVar = _.find(_.get(transaction, 'request.url.variable', []), (transactionPathVar) => {
503-
return transactionPathVar.key === pathVar.key;
511+
const mappedPathVar = _.find(transactionPathVariables, (transactionPathVar) => {
512+
let matched = transactionPathVar.key === pathVar.key;
513+
if (
514+
!matched &&
515+
isNotAServerPathVar(transactionPathVar.key) &&
516+
!matchedPath.unmatchedVariablesFromTransaction.includes(transactionPathVar)
517+
) {
518+
matchedPath.unmatchedVariablesFromTransaction.push(transactionPathVar);
519+
}
520+
return matched;
504521
});
505522
pathVar.value = _.get(mappedPathVar, 'value', pathVar.value);
506523
// set _varMatched flag which represents if variable was found in transaction or not
@@ -522,7 +539,7 @@ class SchemaPack {
522539
schemaUtils.checkMetadata(transaction, '$', matchedPath.path, matchedPath.name, options, cb);
523540
},
524541
path: function(cb) {
525-
schemaUtils.checkPathVariables(matchedPath.pathVariables, '$.request.url.variable', matchedPath.path,
542+
schemaUtils.checkPathVariables(matchedPath, '$.request.url.variable', matchedPath.path,
526543
componentsAndPaths, options, schemaCache, jsonSchemaDialect, cb);
527544
},
528545
queryparams: function(cb) {

0 commit comments

Comments
 (0)