From d4673414271f3b17870f0b214371b008c4105f0d Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 12 Apr 2018 15:49:18 +0200 Subject: [PATCH] Ensured that stub part of the body gets returned form WIreMock response, fixes gh-611 --- .../wiremock/BaseWireMockStubStrategy.groovy | 8 +- .../WireMockResponseStubStrategy.groovy | 3 +- .../verifier/util/ContentUtils.groovy | 4 +- .../util/DelegatingJsonVerifiable.java | 14 +-- .../util/JsonToJsonPathsConverter.groovy | 7 +- .../MockMvcMethodBodyBuilderSpec.groovy | 8 +- .../verifier/dsl/WireMockGroovyDslSpec.groovy | 78 +++++++++++++++++ .../util/JsonToJsonPathsConverterSpec.groovy | 85 +++++++++++-------- 8 files changed, 148 insertions(+), 59 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy index fff0773842..8c94880436 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -33,7 +33,7 @@ import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.MapConverter -import static ContentUtils.extractValue +import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue import static org.springframework.cloud.contract.verifier.util.MapConverter.transformValues /** * Common abstraction over WireMock Request / Response conversion implementations @@ -53,12 +53,12 @@ abstract class BaseWireMockStubStrategy { protected final Contract contract protected BaseWireMockStubStrategy(Contract contract) { - this.processor = processor() + this.processor = templateProcessor() this.template = contractTemplate() this.contract = contract } - private TemplateProcessor processor() { + private TemplateProcessor templateProcessor() { return new HandlebarsTemplateProcessor() } @@ -145,7 +145,7 @@ abstract class BaseWireMockStubStrategy { * For the given {@link ContentType} returns the String version of the body */ String parseBody(GString value, ContentType contentType) { - Object processedValue = extractValue(value, contentType, { Object o -> o instanceof DslProperty ? o.clientValue : o }) + Object processedValue = extractValue(value, contentType, { Object o -> o instanceof DslProperty ? o.clientValue : o }) if (processedValue instanceof GString) { return parseBody(processedValue.toString(), contentType) } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy index 14f4254afe..606d36a4fe 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy @@ -28,6 +28,7 @@ import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.Request import org.springframework.cloud.contract.spec.internal.Response import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.core.io.support.SpringFactoriesLoader @@ -84,7 +85,7 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { private void appendBody(ResponseDefinitionBuilder builder) { if (response.body) { - Object body = response.body.clientValue + Object body = MapConverter.getStubSideValues(response.body) ContentType contentType = recognizeContentTypeFromHeader(response.headers) if (contentType == ContentType.UNKNOWN) { contentType = recognizeContentTypeFromContent(body) 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 ea2e38098b..140dced5b3 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 @@ -21,6 +21,7 @@ import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.transform.TypeChecked import groovy.util.logging.Slf4j +import org.apache.commons.lang3.StringEscapeUtils import org.codehaus.groovy.runtime.GStringImpl import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.ExecutionProperty @@ -35,6 +36,7 @@ import java.util.regex.Pattern import static org.apache.commons.text.StringEscapeUtils.escapeJava import static org.apache.commons.text.StringEscapeUtils.escapeJson import static org.apache.commons.text.StringEscapeUtils.escapeXml11 +import static org.apache.commons.text.StringEscapeUtils.unescapeXml /** * A utility class that can operate on a message body basing on the provided Content Type. @@ -208,7 +210,7 @@ class ContentUtils { } private static String transformXMLStringValue(Object obj, Closure valueProvider) { - return escapeXml11(obj.toString()) + return escapeXml11(unescapeXml(obj.toString())) } private static String transformXMLStringValue(DslProperty dslProperty, Closure valueProvider) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java index 965f5921b5..74573419d1 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java @@ -340,23 +340,13 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { return true; if (o == null || getClass() != o.getClass()) return false; - DelegatingJsonVerifiable that = (DelegatingJsonVerifiable) o; - - if (this.delegate != null ? !this.delegate.equals(that.delegate) : that.delegate != null) - return false; - if (this.delegate == null) { - return false; - } - if (this.delegate.jsonPath() == null && that.delegate.jsonPath() == null) - return true; - return this.delegate.jsonPath().equals(that.delegate.jsonPath()); - + return this.methodsBuffer.toString().equals(that.methodsBuffer.toString()); } @Override public int hashCode() { - int result = this.delegate != null ? this.delegate.jsonPath().hashCode() : 0; + int result = this.methodsBuffer.toString().hashCode(); return 31 * result; } 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 1935c7835b..b868ab161a 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 @@ -274,7 +274,7 @@ class JsonToJsonPathsConverter { String systemPropValue = System.getProperty(SIZE_ASSERTION_SYSTEM_PROP) Boolean configPropValue = configProperties.assertJsonSize if ((systemPropValue != null && Boolean.parseBoolean(systemPropValue)) || - configPropValue) { + configPropValue && listContainsOnlyPrimitives(value)) { addArraySizeCheck(key, value, closure) } else { if (log.isDebugEnabled()) { @@ -323,6 +323,10 @@ class JsonToJsonPathsConverter { return jsonPathVerifiable.matches((object as Pattern).pattern()) } return jsonPathVerifiable.contains(object) + } else if (element instanceof List) { + if (listContainsOnlyPrimitives(element)) { + return jsonPathVerifiable.array() + } } return jsonPathVerifiable } @@ -367,6 +371,7 @@ class JsonToJsonPathsConverter { } } } + private boolean isAnEntryWithLists(def value) { if (!(value instanceof Iterable)) { return false diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index d6fce5b5da..cff968bd6a 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -1597,10 +1597,10 @@ World.'''""" builder.appendTo(blockBuilder) def test = blockBuilder.toString() then: - test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Programming").value()') - test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Java").value()') - test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Spring").value()') - test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Boot").value()') + test.contains('assertThatJson(parsedJson).array().array().arrayField().isEqualTo("Programming").value()') + test.contains('assertThatJson(parsedJson).array().array().arrayField().isEqualTo("Java").value()') + test.contains('assertThatJson(parsedJson).array().array().arrayField().isEqualTo("Spring").value()') + test.contains('assertThatJson(parsedJson).array().array().arrayField().isEqualTo("Boot").value()') and: SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString()) where: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy index 991131503b..fabc159728 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy @@ -20,6 +20,7 @@ import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.core.WireMockConfiguration import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer import groovy.json.JsonBuilder +import groovy.json.JsonOutput import groovy.json.JsonSlurper import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.cloud.contract.spec.Contract @@ -1842,6 +1843,76 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie server?.shutdown() } + @Issue('#266') + def "should work with array of arrays"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = Contract.make { + request { + method 'POST' + urlPath '/api/categories' + body([["Programming", "Java"], ["Programming", "Java", "Spring", "Boot"]]) + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + } + response { + status 200 + body([["Programming", "Java"], ["Programming", "Java", "Spring", "Boot"]]) + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "urlPath" : "/api/categories", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json;charset=UTF-8" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[*][?(@ == 'Spring')]" + }, { + "matchesJsonPath" : "$[*][?(@ == 'Boot')]" + }, { + "matchesJsonPath" : "$[*][?(@ == 'Programming')]" + }, { + "matchesJsonPath" : "$[*][?(@ == 'Java')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "[\\"[\\\\\\"Programming\\\\\\",\\\\\\"Java\\\\\\"]\\",\\"[\\\\\\"Programming\\\\\\",\\\\\\"Java\\\\\\",\\\\\\"Spring\\\\\\",\\\\\\"Boot\\\\\\"]\\"]", + "headers" : { + "Content-Type" : "application/json;charset=UTF-8" + }, + "transformers" : [ "response-template", "foo-transformer" ] + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + and: + int port = SocketUtils.findAvailableTcpPort() + WireMockServer server = new WireMockServer(config().port(port)) + server.start() + server.addStubMapping(WireMockStubMapping.buildFrom(json)) + then: + String entity = callApiCategories(port) + and: + AssertionUtil.assertThatJsonsAreEqual((''' + ["[\\"Programming\\",\\"Java\\"]","[\\"Programming\\",\\"Java\\",\\"Spring\\",\\"Boot\\"]"] + '''), entity) + cleanup: + server?.shutdown() + } + @Issue('#269') def "should create a stub for dot separated keys"() { given: @@ -2028,4 +2099,11 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie .header("Cookie", "foo=bar") .body("{\"foo\":\"bar\",\"baz\":5}"), String.class) } + + String callApiCategories(int port) { + return new TestRestTemplate().exchange( + RequestEntity.post(URI.create("http://localhost:" + port + "/api/categories")) + .header("Content-Type", "application/json;charset=UTF-8") + .body(JsonOutput.toJson([["Programming", "Java"], ["Programming", "Java", "Spring", "Boot"]])), String.class).body + } } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy index 81594524cb..06cc215abc 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy @@ -673,20 +673,25 @@ class JsonToJsonPathsConverterSpec extends Specification { when: JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) then: + DocumentContext context = JsonPath.parse(json) + pathAndValues.each { + assert context.read(it.jsonPath(), JSONArray) + } + and: pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(38.995548)""" && + it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(38.995548)""" && it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.995548)]""" } pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(-77.119759)""" && + it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(-77.119759)""" && it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -77.119759)]""" } pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(-76.909393)""" && + it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(-76.909393)""" && it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -76.909393)]""" } pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(38.791645)""" && + it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(38.791645)""" && it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.791645)]""" } and: @@ -700,8 +705,8 @@ class JsonToJsonPathsConverterSpec extends Specification { @RestoreSystemProperties def "should manage to parse a double array with array size check"() { given: - System.setProperty('spring.cloud.contract.verifier.assert.size', 'true') - String json = ''' + System.setProperty('spring.cloud.contract.verifier.assert.size', 'true') + String json = ''' [{ "place": { @@ -717,38 +722,46 @@ class JsonToJsonPathsConverterSpec extends Specification { }] ''' when: - JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) then: - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(38.995548)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.995548)]""" - } - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(-77.119759)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -77.119759)]""" - } - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(-76.909393)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -76.909393)]""" - } - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().arrayField().isEqualTo(38.791645)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.791645)]""" - } - pathAndValues.find { - it.method()== """.hasSize(1)""" && - it.jsonPath() == """\$""" - } - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().hasSize(2)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*]""" - } - pathAndValues.find { - it.method()== """.array().field("['place']").field("['bounding_box']").array("['coordinates']").hasSize(1)""" && - it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*]""" - } + DocumentContext context = JsonPath.parse(json) + pathAndValues.each { + assert context.read(it.jsonPath(), JSONArray) + } + pathAndValues.find { + it.method() == """.hasSize(1)""" && + it.jsonPath() == """\$""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(-77.119759)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -77.119759)]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(38.995548)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.995548)]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").hasSize(1)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().hasSize(2)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(38.791645)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == 38.791645)]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().hasSize(2)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*]""" + } + pathAndValues.find { + it.method() == """.array().field("['place']").field("['bounding_box']").array("['coordinates']").array().array().arrayField().isEqualTo(-76.909393)""" && + it.jsonPath() == """\$[*].['place'].['bounding_box'].['coordinates'][*][*][?(@ == -76.909393)]""" + } and: - pathAndValues.size() == 7 + pathAndValues.size() == 8 and: pathAndValues.each { JsonAssertion.assertThat(json).matchesJsonPath(it.jsonPath())