Ensures reversed order of body matchers

without this change if in json paths we have .a[0].b[0] and .a[0].b[1] when the first entry is removed, then there is no longer any .a[0].b[1] since it becomes .a[0].b[0].
with this change we're first deleting enties without removing the trailing containers, then we're reversing the order of jsonpaths so that we delete starting from .a[0].b[1] and only then .a[0].b and finally we're removing the containers

fixes gh-1125
This commit is contained in:
Marcin Grzejszczak
2020-01-02 10:03:28 +01:00
parent e3d5f9bfff
commit 50f04284cb
2 changed files with 106 additions and 34 deletions

View File

@@ -85,20 +85,26 @@ class JsonToJsonPathsConverter {
def jsonCopy = cloneBody(json)
DocumentContext context = JsonPath.parse(jsonCopy)
if (bodyMatchers?.hasMatchers()) {
bodyMatchers.matchers().each { BodyMatcher matcher ->
List<String> pathsToDelete = []
List<String> paths = bodyMatchers.matchers().collect { it.path() }
paths.each { String path ->
try {
def entry = entry(context, matcher.path())
def entry = entry(context, path)
if (entry != null) {
context.delete(matcher.path())
removeTrailingContainers(matcher.path(), context)
context.delete(path)
pathsToDelete.add(path)
}
}
catch (RuntimeException e) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to delete path [${matcher.path()}]", e)
log.debug("Exception occurred while trying to delete path [${path}]", e)
}
}
}
pathsToDelete.sort(Collections.reverseOrder())
pathsToDelete.each {
removeTrailingContainers(it, context)
}
}
return jsonCopy
}
@@ -137,38 +143,46 @@ class JsonToJsonPathsConverter {
* defining body...
*/
private static boolean removeTrailingContainers(String matcherPath, DocumentContext context) {
Matcher matcher = ANY_ARRAY_NOTATION_IN_JSONPATH.matcher(matcherPath)
boolean containsArray = matcher.find()
String pathWithoutAnyArray = containsArray ? matcherPath.
substring(0, matcherPath.lastIndexOf(lastMatch(matcher))) : matcherPath
def object = entry(context, pathWithoutAnyArray)
// object got removed and it was the only element
// let's get its parent and see if it contains an empty element
if (isIterable(object)
&&
containsOnlyEmptyElements(object)
&& isNotRootArray(matcherPath)) {
String pathToDelete = pathToDelete(pathWithoutAnyArray)
context.delete(pathToDelete)
return pathToDelete.contains(DESCENDANT_OPERATOR) ? false :
removeTrailingContainers(pathToDelete, context)
}
else {
int lastIndexOfDot = matcherPath.lastIndexOf(".")
if (lastIndexOfDot == -1) {
return false
}
String lastParent = matcherPath.substring(0, lastIndexOfDot)
def lastParentObject = context.read(lastParent)
if (isIterable(lastParentObject)
try {
Matcher matcher = ANY_ARRAY_NOTATION_IN_JSONPATH.matcher(matcherPath)
boolean containsArray = matcher.find()
String pathWithoutAnyArray = containsArray ? matcherPath.
substring(0, matcherPath.lastIndexOf(lastMatch(matcher))) : matcherPath
def object = entry(context, pathWithoutAnyArray)
// object got removed and it was the only element
// let's get its parent and see if it contains an empty element
if (isIterable(object)
&&
containsOnlyEmptyElements(lastParentObject)
&& isNotRoot(lastParent)) {
context.delete(lastParent)
return removeTrailingContainers(lastParent, context)
containsOnlyEmptyElements(object)
&& isNotRootArray(matcherPath)) {
String pathToDelete = pathToDelete(pathWithoutAnyArray)
context.delete(pathToDelete)
return pathToDelete.contains(DESCENDANT_OPERATOR) ? false :
removeTrailingContainers(pathToDelete, context)
}
else {
int lastIndexOfDot = matcherPath.lastIndexOf(".")
if (lastIndexOfDot == -1) {
return false
}
String lastParent = matcherPath.substring(0, lastIndexOfDot)
def lastParentObject = context.read(lastParent)
if (isIterable(lastParentObject)
&&
containsOnlyEmptyElements(lastParentObject)
&& isNotRoot(lastParent)) {
context.delete(lastParent)
return removeTrailingContainers(lastParent, context)
}
}
return false
}
catch (RuntimeException e) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to delete path [${matcherPath}]", e)
}
return false
}
return false
}
private static String lastMatch(Matcher matcher) {

View File

@@ -2753,6 +2753,64 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
}
@Issue("#1125")
def "should not generate assertions for [*] when all manual entries were passed"() {
given:
Contract contractDsl = Contract.make {
request {
method(GET())
url '/operations'
body(
channedlId: "UC",
operations: [
[
parameters : [
[
name : "#POID",
value: '70000269814',
type : "DECIMAL"
],
[
name : "#OVID",
value: "3",
type : "DECIMAL"
],
[
name : "#CAMC",
value: "CC2PAY",
type : "CHAR"
]
]
]
]
)
bodyMatchers {
jsonPath('$.operations[0].parameters[0].name', byEquality())
jsonPath('$.operations[0].parameters[0].value', byRegex('[0-9]{11}'))
jsonPath('$.operations[0].parameters[0].type', byEquality())
jsonPath('$.operations[0].parameters[1].name', byEquality())
jsonPath('$.operations[0].parameters[1].value', byEquality())
jsonPath('$.operations[0].parameters[1].type', byEquality())
jsonPath('$.operations[0].parameters[2].name', byEquality())
jsonPath('$.operations[0].parameters[2].value', byEquality())
jsonPath('$.operations[0].parameters[2].type', byEquality())
}
}
response {
status 200
}
}
when:
String wireMockStub = new WireMockStubStrategy("Test",
new ContractMetadata(null, false, 0, null, contractDsl), contractDsl)
.toWireMockClientStub()
then:
!wireMockStub.contains('''$.['operations'][*]''')
stubMappingIsValidWireMockStub(wireMockStub)
}
@Issue("#1257")
def "should work with null request element on the client side and optional stub entry"() {
given: