From edb3d97f3f678ac7207bae8ebb68a8fa82bcabe1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 23 Oct 2018 22:13:26 +0200 Subject: [PATCH] Fixed invalid cloning of a json; fixes gh-763 --- .../util/JsonToJsonPathsConverter.groovy | 31 ++-- .../verifier/util/MapConverter.groovy | 3 - .../WireMockResponseStubStrategySpec.groovy | 156 ++++++++++++++++++ 3 files changed, 176 insertions(+), 14 deletions(-) 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 d849c888e5..1a80dd840e 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 @@ -22,7 +22,9 @@ 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 +import org.apache.commons.lang3.StringEscapeUtils import org.springframework.cloud.contract.spec.internal.BodyMatcher import org.springframework.cloud.contract.spec.internal.BodyMatchers import org.springframework.cloud.contract.spec.internal.ExecutionProperty @@ -30,6 +32,7 @@ import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.OptionalProperty import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.util.SerializationUtils +import repackaged.nl.flotsam.xeger.Xeger import java.util.regex.Pattern /** @@ -125,15 +128,8 @@ class JsonToJsonPathsConverter { // Doing a clone doesn't work for nested lists... private static Object cloneBody(Object object) { - if (object instanceof List) { - byte[] serializedObject = SerializationUtils.serialize(object) - return SerializationUtils.deserialize(serializedObject) - } - try { - return object.clone() - } catch (CloneNotSupportedException e) { - return object - } + byte[] serializedObject = SerializationUtils.serialize(object) + return SerializationUtils.deserialize(serializedObject) } /** @@ -168,17 +164,30 @@ class JsonToJsonPathsConverter { return path.contains("['") } + + @CompileStatic + static Object generatedValueIfNeeded(Object value) { + if (value instanceof Pattern) { + return StringEscapeUtils.escapeJava(new Xeger(((Pattern) value).pattern()).generate()) + } + return value + } + private static String createComparison(BodyMatcher bodyMatcher, Object value, def body) { if (bodyMatcher.matchingType() == MatchingType.EQUALITY) { + Object convertedBody = body if (!body) { throw new IllegalStateException("Body hasn't been passed") } try { - Object retrievedValue = JsonPath.parse(body).read(bodyMatcher.path()) + convertedBody = MapConverter.transformValues(body) { + return generatedValueIfNeeded(it) + } + Object retrievedValue = JsonPath.parse(convertedBody).read(bodyMatcher.path()) String wrappedValue = retrievedValue instanceof Number ? retrievedValue : "'${retrievedValue.toString()}'" return "== ${wrappedValue}" } catch (PathNotFoundException e) { - throw new IllegalStateException("Value [${bodyMatcher.path()}] not found in JSON [${JsonOutput.toJson(body)}]", e) + throw new IllegalStateException("Value [${bodyMatcher.path()}] not found in JSON [${JsonOutput.toJson(convertedBody)}]", e) } } else { String convertedValue = value.toString().replace('/', '\\\\/') 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 1bea23558a..6b9c045806 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 @@ -20,9 +20,6 @@ import groovy.json.JsonSlurper import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor import org.springframework.cloud.contract.verifier.template.TemplateProcessor - -import static org.springframework.cloud.contract.verifier.util.ContentUtils.getClientContentType - /** * Converts an object into either client or server side representation. * Iterates over the structure of an object (depending on whether it's an diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy index 61a94cdfd7..d5070c821f 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy @@ -54,4 +54,160 @@ class WireMockResponseStubStrategySpec extends Specification { assert body.get("positiveInt") instanceof Integer assert body.get("double") instanceof BigDecimal } + + def "should convert patterns to proper value"() { + given: + def contract = Contract.make { + request { + method(GET()) + urlPath(value(regex("/info/[0-9]"))) { + queryParameters { + parameter 'limit': $(consumer(equalTo('20')), producer(equalTo('10'))) + parameter 'offset': $(consumer(containing("20")), producer(equalTo('20'))) + parameter 'filter': 'email' + parameter 'sort': equalTo("name") + parameter 'age': $(consumer(notMatching("^\\w*\$")), producer('99')) + parameter 'name': $(consumer(matching('John.*')), producer('John.Doe')) + parameter 'email': 'bob@email.com' + parameter 'hello': $(consumer(matching('John.*')), producer(absent())) + parameter 'hello2': absent() + } + } + headers { + contentType(applicationJson()) + header([ + second: "value", third: $(anyAlphaNumeric()) + ]) + } + body([ + foo1 : $(c(regex("[0-9]")), p(1)), + foo2 : $(c(regex("[0-9]"))), + foo3 : $(anyAlphaNumeric()), + foo4 : value(regex(aDouble())), + foo5 : value(anyDouble()), + foo6 : "concrete", + duck : 123, + alpha : 'abc', + number : 123, + aBoolean : true, + date : '2017-01-01', + dateTime : '2017-01-01T01:23:45', + time : '01:02:34', + valueWithoutAMatcher: 'foo', + valueWithTypeMatch : 'string', + key : [ + 'complex.key': 'foo' + ] + ]) + bodyMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byEquality()) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath("\$.['key'].['complex.key']", byEquality()) + } + } + response { + status(OK()) + headers { + contentType(applicationJson()) + header([ + second: "value", third: $(anyAlphaNumeric()) + ]) + } + body([ + foo1 : $(p(regex("[0-9]")), c(1)), + foo2 : $(p(regex("[0-9]"))), + foo3 : $(anyAlphaNumeric()), + foo4 : value(regex(aDouble())), + foo5 : value(anyDouble()), + foo6 : "concrete", + duck : 123, + alpha : 'abc', + number : 123, + positiveInteger : 1234567890, + negativeInteger : -1234567890, + positiveDecimalNumber: 123.4567890, + negativeDecimalNumber: -123.4567890, + aBoolean : true, + date : '2017-01-01', + dateTime : '2017-01-01T01:23:45', + time : "01:02:34", + valueWithoutAMatcher : 'foo', + valueWithTypeMatch : 'string', + valueWithMin : [ + 1, 2, 3 + ], + valueWithMax : [ + 1, 2, 3 + ], + valueWithMinMax : [ + 1, 2, 3 + ], + valueWithMinEmpty : [], + valueWithMaxEmpty : [], + key : [ + 'complex.key': 'foo' + ], + nullValue : null + ]) + bodyMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against the provided value + jsonPath('$.duck', byEquality()) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.positiveInteger', byRegex(anInteger())) + jsonPath('$.negativeInteger', byRegex(anInteger())) + jsonPath('$.positiveDecimalNumber', byRegex(aDouble())) + jsonPath('$.negativeDecimalNumber', byRegex(aDouble())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + // asserts vs inbuilt time related regex + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + // asserts that the resulting type is the same as in response body + jsonPath('$.valueWithTypeMatch', byType()) + jsonPath('$.valueWithMin', byType { + // results in verification of size of array (min 1) + minOccurrence(1) + }) + jsonPath('$.valueWithMax', byType { + // results in verification of size of array (max 3) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinMax', byType { + // results in verification of size of array (min 1 & max 3) + minOccurrence(1) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinEmpty', byType { + // results in verification of size of array (min 0) + minOccurrence(0) + }) + jsonPath('$.valueWithMaxEmpty', byType { + // results in verification of size of array (max 0) + maxOccurrence(0) + }) + // will execute a method `assertThatValueIsANumber` + jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)')) + jsonPath("\$.['key'].['complex.key']", byEquality()) + jsonPath('$.nullValue', byNull()) + } + } + } + when: + def subject = new WireMockRequestStubStrategy(contract) + subject.buildClientRequestContent() + then: + noExceptionThrown() + } }