From 0d0d2ea11c1441ea5e808c9f5b49c3fdeb1dfc83 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Sun, 20 Jan 2019 22:23:34 +0100 Subject: [PATCH] Shouldn't always parse each object without this change, when a body is parsed and it contains a JSON in a JSON, then we parse it. That's because our logic hasn't actually ever considered that one can have a valid JSON string in a JSON. with this change we're introducing a parsing closure. When we know that the body is a Map and we know that we're parsing a JSON, then we don't want to parse any Strings inside that map. We assume that the users know what they do, so if an element in the map is a String, then we just pass it. fixes gh-652 --- .../JsonBodyVerificationBuilder.groovy | 31 +++---- .../builder/MessagingMethodBodyBuilder.groovy | 24 +++--- .../verifier/builder/MethodBodyBuilder.groovy | 37 +++++---- .../RequestProcessingMethodBodyBuilder.groovy | 28 ++++--- .../verifier/util/ContentUtils.groovy | 28 ++++--- .../util/JsonToJsonPathsConverter.groovy | 80 ++++++++++--------- .../verifier/util/MapConverter.groovy | 70 +++++++++------- .../builder/MethodBodyBuilderSpec.groovy | 64 ++++++++++++--- 8 files changed, 219 insertions(+), 143 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy index 3e8d2b1ed6..fe811e010d 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy @@ -1,22 +1,22 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.builder - import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException @@ -38,7 +38,6 @@ import org.springframework.cloud.contract.verifier.template.TemplateProcessor import org.springframework.cloud.contract.verifier.util.JsonPaths import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import org.springframework.cloud.contract.verifier.util.MapConverter - /** * @author Marcin Grzejszczak * @author Olga Maciaszek-Sharma @@ -83,9 +82,11 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier boolean shouldCommentOutBDDBlocks) { appendJsonPath(bb, responseString) DocumentContext parsedRequestBody + boolean dontParseStrings = convertedResponseBody instanceof Map + Closure parsingClosure = dontParseStrings ? Closure.IDENTITY : MapConverter.JSON_PARSING_CLOSURE if (contract.request?.body) { def testSideRequestBody = MapConverter - .getTestSideValues(contract.request.body) + .getTestSideValues(contract.request.body, parsingClosure) parsedRequestBody = JsonPath.parse(testSideRequestBody) if (convertedResponseBody instanceof String && ! textContainsJsonPathTemplate(convertedResponseBody)) { @@ -100,9 +101,9 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier TestSideRequestTemplateModel templateModel = contract.request?.body ? TestSideRequestTemplateModel.from(contract.request) : null convertedResponseBody = MapConverter.transformValues(convertedResponseBody, - returnReferencedEntries(templateModel)) + returnReferencedEntries(templateModel), parsingClosure) JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties). - transformToJsonPathWithTestsSideValues(convertedResponseBody) + transformToJsonPathWithTestsSideValues(convertedResponseBody, parsingClosure) jsonPaths.each { String method = it.method() method = processIfTemplateIsPresent(method, parsedRequestBody) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy index 65bb68dfb7..66d6839599 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.builder @@ -116,7 +117,8 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { } protected String getBodyAsString() { - Object bodyValue = extractServerValueFromBody(inputMessage.messageBody.serverValue) + ContentType contentType = contentType() + Object bodyValue = extractServerValueFromBody(contentType, inputMessage.messageBody.serverValue) if (bodyValue instanceof FromFileProperty) { FromFileProperty fileProperty = (FromFileProperty) bodyValue return fileProperty.isByte() ? diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index 7f17587774..9825ab2989 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.builder @@ -30,6 +31,7 @@ import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.Headers import org.springframework.cloud.contract.spec.internal.MatchingStrategy import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.OptionalProperty @@ -418,7 +420,9 @@ abstract class MethodBodyBuilder implements ClassVerifier { convertedResponseBody = extractValue(convertedResponseBody as GString, contentType, { Object o -> o instanceof DslProperty ? o.serverValue : o }) } if (TEXT != contentType && FORM != contentType) { - convertedResponseBody = MapConverter.getTestSideValues(convertedResponseBody) + boolean dontParseStrings = contentType == JSON && convertedResponseBody instanceof Map + Closure parsingClosure = dontParseStrings ? Closure.IDENTITY : MapConverter.JSON_PARSING_CLOSURE + convertedResponseBody = MapConverter.getTestSideValues(convertedResponseBody, parsingClosure) } else { convertedResponseBody = StringEscapeUtils.escapeJava(convertedResponseBody.toString()) } @@ -510,15 +514,18 @@ abstract class MethodBodyBuilder implements ClassVerifier { * Converts the passed body into ints server side representation. All {@link DslProperty} * will return their server side values */ - protected Object extractServerValueFromBody(bodyValue) { + protected Object extractServerValueFromBody(ContentType contentType, Object bodyValue) { if (bodyValue instanceof GString) { - return extractValue(bodyValue, contentType(), GET_SERVER_VALUE) + return extractValue(bodyValue, contentType, GET_SERVER_VALUE) } - return MapConverter.transformValues(bodyValue, GET_SERVER_VALUE) + boolean dontParseStrings = contentType == JSON && bodyValue instanceof Map + Closure parsingClosure = dontParseStrings ? Closure.IDENTITY : MapConverter.JSON_PARSING_CLOSURE + return MapConverter.transformValues(bodyValue, GET_SERVER_VALUE, parsingClosure) } protected ContentType contentType() { - return ContentUtils.recognizeContentTypeFromTestHeader(this.contract.request?.headers) + Headers headers = this.contract.request?.headers ?: this.contract.input?.messageHeaders + return ContentUtils.recognizeContentTypeFromTestHeader(headers) } /** diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index dd5772937d..995e7d9f89 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.builder @@ -133,7 +134,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { case ExecutionProperty: case FromFileProperty: body = request.body?.serverValue - break; + break default: body = getBodyAsString() } @@ -243,8 +244,9 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { @Override protected String getBodyAsString() { - Object bodyValue = extractServerValueFromBody(request.body.serverValue) - if (contentType() == ContentType.FORM) { + ContentType contentType = contentType() + Object bodyValue = extractServerValueFromBody(contentType, request.body.serverValue) + if (contentType == ContentType.FORM) { if (bodyValue instanceof Map) { // [a:3, b:4] == "a=3&b=4" return ((Map) bodyValue).collect { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index c523012e5b..1ad1f67918 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.util @@ -178,7 +179,7 @@ class ContentUtils { if (contentType == UNKNOWN) { return getClientContentType(bodyAsValue) } - return contentType; + return contentType } static ContentType getClientContentType(Map bodyAsValue) { @@ -277,10 +278,11 @@ class ContentUtils { return transformXMLStringValue(valueProvider(dslProperty), valueProvider) } - protected static Object convertDslPropsToTemporaryRegexPatterns(parsedJson) { + protected static Object convertDslPropsToTemporaryRegexPatterns(Object parsedJson, + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { MapConverter.transformValues(parsedJson, { Object value -> return transformJSONStringValue(value, GET_TEST_SIDE) - }) + }, parsingClosure) } private static Object convertAllTemporaryRegexPlaceholdersBackToPatterns(parsedJson) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index ba6b032c9e..659aad8f23 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.util @@ -23,7 +24,6 @@ import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException import com.toomuchcoding.jsonassert.JsonAssertion import groovy.json.JsonOutput -import groovy.json.JsonSlurper import groovy.transform.CompileStatic import groovy.util.logging.Commons @@ -233,56 +233,62 @@ class JsonToJsonPathsConverter { } } - JsonPaths transformToJsonPathWithTestsSideValues(def json) { - return transformToJsonPathWithValues(json, SERVER_SIDE) + JsonPaths transformToJsonPathWithTestsSideValues(def json, + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { + return transformToJsonPathWithValues(json, SERVER_SIDE, parsingClosure) } - JsonPaths transformToJsonPathWithStubsSideValues(def json) { - return transformToJsonPathWithValues(json, CLIENT_SIDE) + JsonPaths transformToJsonPathWithStubsSideValues(def json, + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { + return transformToJsonPathWithValues(json, CLIENT_SIDE, parsingClosure) } - static JsonPaths transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(def json) { + static JsonPaths transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(def json, + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { return new JsonToJsonPathsConverter() - .transformToJsonPathWithValues(json, CLIENT_SIDE) + .transformToJsonPathWithValues(json, CLIENT_SIDE, parsingClosure) } - private JsonPaths transformToJsonPathWithValues(def json, boolean clientSide) { + private JsonPaths transformToJsonPathWithValues(def json, boolean clientSide, + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { if(!json) { return new JsonPaths() } JsonPaths pathsAndValues = [] as Set - Object convertedJson = MapConverter.getClientOrServerSideValues(json, clientSide) - Object jsonWithPatterns = ContentUtils.convertDslPropsToTemporaryRegexPatterns(convertedJson) + Object convertedJson = MapConverter.getClientOrServerSideValues(json, clientSide, parsingClosure) + Object jsonWithPatterns = ContentUtils. + convertDslPropsToTemporaryRegexPatterns(convertedJson, parsingClosure) MethodBufferingJsonVerifiable methodBufferingJsonPathVerifiable = new DelegatingJsonVerifiable(JsonAssertion.assertThat(JsonOutput.toJson(jsonWithPatterns)) .withoutThrowingException()) - traverseRecursivelyForKey(jsonWithPatterns, methodBufferingJsonPathVerifiable) + traverseRecursivelyForKey(jsonWithPatterns, methodBufferingJsonPathVerifiable, { MethodBufferingJsonVerifiable key, Object value -> if (value instanceof ExecutionProperty || !(key instanceof FinishedDelegatingJsonVerifiable)) { return } pathsAndValues.add(key) - } + }, parsingClosure) return pathsAndValues } - protected def traverseRecursively(Class parentType, MethodBufferingJsonVerifiable key, def value, Closure closure) { + protected def traverseRecursively(Class parentType, MethodBufferingJsonVerifiable key, def value, + Closure closure, Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { value = ContentUtils.returnParsedObject(value) if (value instanceof String && value) { try { - def json = new JsonSlurper().parseText(value) + def json = parsingClosure(value) if (json instanceof Map) { - return convertWithKey(parentType, key, json, closure) + return convertWithKey(parentType, key, json, closure, parsingClosure) } } catch (Exception ignore) { return runClosure(closure, key, value) } } else if (isAnEntryWithNonCollectionLikeValue(value)) { - return convertWithKey(List, key, value as Map, closure) + return convertWithKey(List, key, value as Map, closure, parsingClosure) } else if (isAnEntryWithoutNestedStructures(value)) { - return convertWithKey(List, key, value as Map, closure) + return convertWithKey(List, key, value as Map, closure, parsingClosure) } else if (value instanceof Map && !value.isEmpty()) { - return convertWithKey(Map, key, value as Map, closure) + return convertWithKey(Map, key, value as Map, closure, parsingClosure) } else if (value instanceof Map && value.isEmpty()) { return runClosure(closure, key.isEmpty(), value) // JSON with a list of primitives ["a", "b", "c"] in root issue #266 @@ -290,28 +296,28 @@ class JsonToJsonPathsConverter { addSizeVerificationForListWithPrimitives(key, closure, value) value.each { traverseRecursively(Object, key.arrayField().contains(ContentUtils.returnParsedObject(it)), - ContentUtils.returnParsedObject(it), closure) + ContentUtils.returnParsedObject(it), closure, parsingClosure) } // JSON containing list of primitives { "partners":[ { "role":"AGENT", "payment_methods":[ "BANK", "CASH" ] } ] } else if (value instanceof List && listContainsOnlyPrimitives(value)) { addSizeVerificationForListWithPrimitives(key, closure, value) value.each { traverseRecursively(Object, valueToAsserter(key.arrayField(), ContentUtils.returnParsedObject(it)), - ContentUtils.returnParsedObject(it), closure) + ContentUtils.returnParsedObject(it), closure, parsingClosure) } } else if (value instanceof List && !value.empty) { MethodBufferingJsonVerifiable jsonPathVerifiable = createAsserterFromList(key, value) addSizeVerificationForListWithPrimitives(key, closure, value) value.each { def element -> traverseRecursively(List, createAsserterFromListElement(jsonPathVerifiable, ContentUtils.returnParsedObject(element)), - ContentUtils.returnParsedObject(element), closure) + ContentUtils.returnParsedObject(element), closure, parsingClosure) } return value } else if (value instanceof List && value.empty) { return runClosure(closure, key, value) } else if (key.isIteratingOverArray()) { traverseRecursively(Object, key.arrayField().contains(ContentUtils.returnParsedObject(value)), - ContentUtils.returnParsedObject(value), closure) + ContentUtils.returnParsedObject(value), closure, parsingClosure) } try { return runClosure(closure, key, value) @@ -435,7 +441,8 @@ class JsonToJsonPathsConverter { } } - private Map convertWithKey(Class parentType, MethodBufferingJsonVerifiable parentKey, Map map, Closure closureToExecute) { + private Map convertWithKey(Class parentType, MethodBufferingJsonVerifiable parentKey, Map map, + Closure closureToExecute, Closure parsingClosure) { return map.collectEntries { Object entrykey, value -> def convertedValue = ContentUtils.returnParsedObject(value) @@ -443,7 +450,7 @@ class JsonToJsonPathsConverter { convertedValue instanceof List ? list(convertedValue, entrykey, parentKey) : convertedValue instanceof Map ? parentKey.field(new ShouldTraverse(entrykey)) : valueToAsserter(parentKey.field(entrykey), convertedValue) - , convertedValue, closureToExecute)] + , convertedValue, closureToExecute, parsingClosure)] } } @@ -456,8 +463,9 @@ class JsonToJsonPathsConverter { parentKey.array(entrykey) } - private void traverseRecursivelyForKey(def json, MethodBufferingJsonVerifiable rootKey, Closure closure) { - traverseRecursively(Map, rootKey, json, closure) + private void traverseRecursivelyForKey(def json, MethodBufferingJsonVerifiable rootKey, + Closure closure, Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { + traverseRecursively(Map, rootKey, json, closure, parsingClosure) } protected MethodBufferingJsonVerifiable valueToAsserter(MethodBufferingJsonVerifiable key, Object value) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy index eb2b2e4135..ada07be543 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy @@ -1,26 +1,29 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.util import groovy.json.JsonSlurper + import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor import org.springframework.cloud.contract.verifier.template.TemplateProcessor + /** * Converts an object into either client or server side representation. * Iterates over the structure of an object (depending on whether it's an @@ -35,6 +38,9 @@ class MapConverter { public static final boolean STUB_SIDE = true public static final boolean TEST_SIDE = false + public static final Closure JSON_PARSING_CLOSURE = { String value -> + new JsonSlurper().parseText(value) + } private final TemplateProcessor templateProcessor @@ -61,35 +67,36 @@ class MapConverter { * * Returns the transformed structure */ - static def transformValues(def value, Closure closure) { + static def transformValues(def value, Closure closure, + Closure parsingClosure = JSON_PARSING_CLOSURE) { if (value instanceof String && value) { try { - def json = new JsonSlurper().parseText(value) + def json = parsingClosure(value) if (json instanceof Map) { - return convert(json, closure) + return convert(json, closure, parsingClosure) } else if (json instanceof List) { - return transformValues(json, closure) + return transformValues(json, closure, parsingClosure) } } catch (Exception ignore) { } return extractValue(value, closure) } else if (value instanceof Map) { - return convert(value as Map, closure) + return convert(value as Map, closure, parsingClosure) } else if (value instanceof List) { - return value.collect({ transformValues(it, closure) }) + return value.collect({ transformValues(it, closure, parsingClosure) }) } - return transformValue(closure, value) + return transformValue(closure, value, parsingClosure) } /** * Transforms a value with the given closure. Needs to be protected, otherwise * method access exception will occur at runtime. */ - protected static Object transformValue(Closure closure, Object value) { + protected static Object transformValue(Closure closure, Object value, Closure parsingClosure) { return extractValue(value, { Object val-> Object newValue = closure(val) if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && value) { - return transformValues(newValue, closure) + return transformValues(newValue, closure, parsingClosure) } return newValue }) @@ -103,10 +110,10 @@ class MapConverter { } } - private static Map convert(Map map, Closure closure) { + private static Map convert(Map map, Closure closure, Closure parsingClosure) { return map.collectEntries { key, value -> - [key, transformValues(value, closure)] + [key, transformValues(value, closure, parsingClosure)] } } @@ -114,12 +121,14 @@ class MapConverter { * If {@code clientSide} is {@code true} returns the client side value for the * provided object */ - static Object getClientOrServerSideValues(json, boolean clientSide) { - return transformValues(json) { + static Object getClientOrServerSideValues(json, boolean clientSide, + Closure parsingClosure = JSON_PARSING_CLOSURE) { + return transformValues(json, { if (it instanceof DslProperty) { DslProperty dslProperty = ((DslProperty) it) return clientSide ? - getClientOrServerSideValues(dslProperty.clientValue, clientSide) : getClientOrServerSideValues(dslProperty.serverValue, clientSide) + getClientOrServerSideValues(dslProperty.clientValue, clientSide, parsingClosure) : + getClientOrServerSideValues(dslProperty.serverValue, clientSide, parsingClosure) } else if (it instanceof GString) { ContentType type = new MapConverter().templateProcessor.containsJsonPathTemplateEntry( ContentUtils.extractValueForGString(it, ContentUtils.GET_TEST_SIDE).toString() @@ -127,7 +136,8 @@ class MapConverter { return ContentUtils.extractValue(it , type, { if (it instanceof DslProperty) { return clientSide ? - getClientOrServerSideValues((it as DslProperty).clientValue, clientSide) : getClientOrServerSideValues((it as DslProperty).serverValue, clientSide) + getClientOrServerSideValues((it as DslProperty).clientValue, clientSide, parsingClosure) : + getClientOrServerSideValues((it as DslProperty).serverValue, clientSide, parsingClosure) } return it }) @@ -135,14 +145,14 @@ class MapConverter { return it.isByte() ? it.asBytes() : it.asString() } return it - } + }, parsingClosure) } - static Object getStubSideValues(json) { - return getClientOrServerSideValues(json, STUB_SIDE) + static Object getStubSideValues(json, Closure parsingClosure = JSON_PARSING_CLOSURE) { + return getClientOrServerSideValues(json, STUB_SIDE, parsingClosure) } - static Object getTestSideValues(json) { - return getClientOrServerSideValues(json, TEST_SIDE) + static Object getTestSideValues(json, Closure parsingClosure = JSON_PARSING_CLOSURE) { + return getClientOrServerSideValues(json, TEST_SIDE, parsingClosure) } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 8bbc00e2fd..a04608c9b7 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -1,17 +1,18 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2018 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.cloud.contract.verifier.builder @@ -1130,4 +1131,47 @@ DocumentContext parsedJson = JsonPath.parse(json); "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } } + @Issue("#652") + def "should not parse json in a json [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + name("insertSomething_ShouldReturnHttp200") + description("POST should do sth") + request { + method 'POST' + url "/foo" + body( + value: "{}" + ) + headers { + contentType(applicationJson()) + } + } + response { + status 200 + headers { contentType(applicationJson()) } + body( + value: "{}" + ) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + String test = blockBuilder.toString() + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + !test.contains(''':{}}''') + test.contains("""assertThatJson(parsedJson).field("['value']").isEqualTo("{}")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + } + }