diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index 55a18fcc9b..920283e798 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -255,6 +255,8 @@ part of your contract. The other is called `testMatchers` which is present in th Currently we support only JSON Path based matchers with the following matching possibilities. For `stubMatchers`: +- `byValue(...)` - the value taken from the response via the provided JSON Path needs +to be equal to the provided value - `byRegex(...)` - the value taken from the response via the provided JSON Path needs to match the regex - `byDate()` - the value taken from the response via the provided JSON Path needs to @@ -266,6 +268,8 @@ match the regex for ISO Time For `testMatchers`: +- `byValue(...)` - the value taken from the response via the provided JSON Path needs +to be equal to the provided value - `byRegex(...)` - the value taken from the response via the provided JSON Path needs to match the regex - `byDate()` - the value taken from the response via the provided JSON Path needs to @@ -307,36 +311,38 @@ assertions and the one from matchers with an `and` section): [source,java,indent=0] ---- - // given: - MockMvcRequestSpecification request = given() - .header("Content-Type", "application/json") - .body("{\"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\"}"); + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/json") + .body("{\"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\"}"); - // when: - ResponseOptions response = given().spec(request) - .get("/get"); + // when: + ResponseOptions response = given().spec(request) + .get("/get"); - // then: - assertThat(response.statusCode()).isEqualTo(200); - assertThat(response.header("Content-Type")).matches("application/json.*"); - // and: - DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); - assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo"); - // and: - assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}"); - assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*"); - assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?"); - assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)"); - assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"); - assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); - assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); - assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(class java.lang.String.class); - assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class); - assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1); - assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class); - assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3); - assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class); - assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3); + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo"); + // and: + assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}"); + assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123); + assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*"); + assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc"); + assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?"); + assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)"); + assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"); + assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class); + assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class); + assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1); + assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class); + assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3); + assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class); + assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3); ---- and the WireMock stub like this: diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy index 85aae742ce..b835d8cb12 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy @@ -25,7 +25,7 @@ interface BodyMatcher { * by type, the defined response body contained an integer but the actual one * contained a string then the assertion should fail */ - String value() + Object value() /** * Min no of occurrence when matching by type. In all other cases it will be ignored diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy index ef25c44d67..adbf00b873 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -46,6 +46,11 @@ class BodyMatchers { return new MatchingTypeValue(MatchingType.REGEX, regex) } + MatchingTypeValue byValue(Object value) { + assert value + return new MatchingTypeValue(MatchingType.EQUALITY, value) + } + boolean equals(o) { if (this.is(o)) return true if (this.getClass() != o.class) return false @@ -81,7 +86,7 @@ class JsonPathBodyMatcher implements BodyMatcher { } @Override - String value() { + Object value() { return this.matchingTypeValue.value } @@ -106,9 +111,9 @@ class MatchingTypeValue { MatchingType type /** - * Value of regular expression + * Value to check */ - String value + Object value /** * Min occurrence when matching by type diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java index f993d092dc..d710e38536 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java @@ -77,7 +77,7 @@ class StubRunnerCamelPredicate implements Predicate { } if (matchers != null && matchers.hasMatchers()) { for (BodyMatcher matcher : matchers.jsonPathMatchers()) { - String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher); matches &= matchesJsonPath(parsedJson, jsonPath); } } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java index 4a746fcb6d..f5103bba68 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java @@ -76,7 +76,7 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector { } if (matchers != null && matchers.hasMatchers()) { for (BodyMatcher matcher : matchers.jsonPathMatchers()) { - String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher); matches &= matchesJsonPath(parsedJson, jsonPath); } } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java index 471bcb21c2..c6259190f2 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java @@ -76,7 +76,7 @@ class StubRunnerStreamMessageSelector implements MessageSelector { } if (matchers != null && matchers.hasMatchers()) { for (BodyMatcher matcher : matchers.jsonPathMatchers()) { - String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher); matches &= matchesJsonPath(parsedJson, jsonPath); } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy index e657c65e57..8b67006e3b 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -16,6 +16,7 @@ package org.springframework.cloud.contract.verifier.wiremock +import com.github.tomakehurst.wiremock.matching.RegexPattern import com.github.tomakehurst.wiremock.stubbing.StubMapping import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -24,6 +25,8 @@ import org.springframework.cloud.contract.verifier.file.ContractMetadata import spock.lang.Issue import spock.lang.Specification +import java.util.regex.Pattern + class DslToWireMockClientConverterSpec extends Specification { @Rule @@ -51,6 +54,8 @@ class DslToWireMockClientConverterSpec extends Specification { JSONAssert.assertEquals(''' {"request":{"method":"PUT","urlPattern":"/[0-9]{2}"},"response":{"status":200}} ''', json, false) + and: + stubMappingIsValidWireMockStub(json) } @Issue("196") @@ -82,6 +87,8 @@ class DslToWireMockClientConverterSpec extends Specification { "status":200,"fixedDelayMilliseconds":1000 }} ''', json, false) + and: + stubMappingIsValidWireMockStub(json) } def "should convert DSL file with a nested list to WireMock JSON"() { @@ -186,6 +193,8 @@ class DslToWireMockClientConverterSpec extends Specification { } } ''', json, false) + and: + stubMappingIsValidWireMockStub(json) } @@ -222,6 +231,8 @@ class DslToWireMockClientConverterSpec extends Specification { JSONAssert.assertEquals(''' {"request":{"urlPath":"/foos","method":"GET"},"response":{"body":"[{\\"id\\":\\"123\\"},{\\"id\\":\\"567\\"}]"}} ''', json, false) + and: + stubMappingIsValidWireMockStub(json) } @@ -248,11 +259,12 @@ class DslToWireMockClientConverterSpec extends Specification { """) when: String json = converter.convertContent("test", new ContractMetadata(file.toPath(), false, 0, null)) - StubMapping.buildFrom(json) then: noExceptionThrown() and: !json.contains('cursor') + and: + stubMappingIsValidWireMockStub(json) } def 'should convert dsl to wiremock to show it in the docs'() { @@ -287,9 +299,9 @@ class DslToWireMockClientConverterSpec extends Specification { } ''') when: - String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) then: - JSONAssert.assertEquals( // tag::wiremock[] + JSONAssert.assertEquals( // tag::wiremock[] ''' { "request" : { @@ -318,14 +330,16 @@ class DslToWireMockClientConverterSpec extends Specification { ''' // end::wiremock[] , json, false) + and: + stubMappingIsValidWireMockStub(json) } def 'should convert dsl to wiremock with stub matchers'() { given: - def converter = new DslToWireMockClientConverter() + def converter = new DslToWireMockClientConverter() and: - File file = tmpFolder.newFile("dsl_from_docs.groovy") - file.write(''' + File file = tmpFolder.newFile("dsl_from_docs.groovy") + file.write(''' org.springframework.cloud.contract.spec.Contract.make { request { method 'GET' @@ -357,7 +371,9 @@ class DslToWireMockClientConverterSpec extends Specification { ]) stubMatchers { jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byValue(123)) jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byValue("abc")) jsonPath('$.number', byRegex(number())) jsonPath('$.aBoolean', byRegex(anyBoolean())) jsonPath('$.date', byDate()) @@ -394,8 +410,10 @@ class DslToWireMockClientConverterSpec extends Specification { testMatchers { // asserts the jsonpath value against manual regex jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byValue(123)) // asserts the jsonpath value against some default regex jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byValue("abc")) jsonPath('$.number', byRegex(number())) jsonPath('$.aBoolean', byRegex(anyBoolean())) // asserts vs inbuilt time related regex @@ -425,9 +443,9 @@ class DslToWireMockClientConverterSpec extends Specification { } ''') when: - String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) then: - JSONAssert.assertEquals(//tag::matchers[] + JSONAssert.assertEquals(//tag::matchers[] ''' { "request" : { @@ -450,8 +468,12 @@ class DslToWireMockClientConverterSpec extends Specification { "matchesJsonPath" : "$.list.someother.nested[?(@.json == 'with value')]" }, { "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]" + }, { + "matchesJsonPath" : "$[?(@.duck == 123)]" }, { "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]" + }, { + "matchesJsonPath" : "$[?(@.alpha == 'abc')]" }, { "matchesJsonPath" : "$[?(@.number =~ /(-?\\\\d*(\\\\.\\\\d+)?)/)]" }, { @@ -477,6 +499,8 @@ class DslToWireMockClientConverterSpec extends Specification { ''' //end::matchers[] , json, false) + and: + stubMappingIsValidWireMockStub(json) } def 'should convert dsl to wiremock with stub matchers with docs example'() { @@ -549,6 +573,16 @@ class DslToWireMockClientConverterSpec extends Specification { } ''' , json, false) + and: + stubMappingIsValidWireMockStub(json) + } + + void stubMappingIsValidWireMockStub(String mappingDefinition) { + StubMapping stubMapping = StubMapping.buildFrom(mappingDefinition) + stubMapping.request.bodyPatterns.findAll { it.isPresent() && it instanceof RegexPattern }.every { + Pattern.compile(it.getValue()) + } + assert !mappingDefinition.contains('org.springframework.cloud.contract.spec.internal') } } 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 985eaef508..e9cf97a10b 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 @@ -312,7 +312,11 @@ abstract class MethodBodyBuilder { // for the rest we'll do JsonPath matching in brute force bodyMatchers.jsonPathMatchers().each { if (it.value()) { - String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, String.class)).matches(${quotedAndEscaped(it.value())})" + String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches" + String valueAsParam = it.value() instanceof String ? quotedAndEscaped(it.value().toString()) : it.value().toString() + String classToCastTo = "${it.value().class.simpleName}.class" + String path = quotedAndEscaped(it.path()) + String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})" bb.addLine(postProcessJsonPathCall(method)) addColonIfRequired(bb) } else { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index d2500dff8f..debe1fc4f5 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -87,7 +87,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } if (request.matchers?.hasMatchers()) { request.matchers.jsonPathMatchers().each { - String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it.path(), it.value()) + String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it) requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\"))) } } 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 3876b53483..962a9f62b7 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 @@ -21,15 +21,14 @@ import com.toomuchcoding.jsonassert.JsonAssertion import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.util.logging.Slf4j -import org.springframework.cloud.contract.spec.internal.OptionalProperty import org.springframework.cloud.contract.spec.internal.BodyMatcher import org.springframework.cloud.contract.spec.internal.BodyMatchers -import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.ExecutionProperty +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 java.util.regex.Pattern - /** * I would like to apologize to anyone who is reading this class. Since JSON is a hectic structure * this class is also hectic. The idea is to traverse the JSON structure and build a set of @@ -73,8 +72,7 @@ class JsonToJsonPathsConverter { */ static def removeMatchingJsonPaths(def json, BodyMatchers bodyMatchers) { if (bodyMatchers?.hasMatchers()) { - // remove all jsonpaths from the body - for those that remain we continue as usual - bodyMatchers.jsonPathMatchers().findAll { it.matchingType() != MatchingType.EQUALITY }.each { BodyMatcher matcher -> + bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher -> JsonPath.parse(json).delete(matcher.path()) } } @@ -82,21 +80,32 @@ class JsonToJsonPathsConverter { } /** - * For the given JSON path and regex pattern converts it into a JSON path - * that checks the Pattern + * For the given matcher converts it into a JSON path + * that checks the regex pattern or equality * - * @param path - JSON path - * @param pattern - pattern to check for the last element of JSON path + * @param bodyMatcher * @return JSON path that checks the regex for its last element */ - static String convertJsonPathAndRegexToAJsonPath(String path, String pattern) { - if (!pattern) { + static String convertJsonPathAndRegexToAJsonPath(BodyMatcher bodyMatcher) { + String path = bodyMatcher.path() + Object value = bodyMatcher.value() + if (!value) { return path } int lastIndexOfDot = path.lastIndexOf(".") String toLastDot = path.substring(0, lastIndexOfDot) String fromLastDot = path.substring(lastIndexOfDot + 1) - return "${toLastDot}[?(@.${fromLastDot} =~ /(${pattern})/)]" + String comparison = createComparison(bodyMatcher, value) + return "${toLastDot}[?(@.${fromLastDot} ${comparison})]" + } + + private static String createComparison(BodyMatcher bodyMatcher, Object value) { + if (bodyMatcher.matchingType() == MatchingType.EQUALITY) { + String wrappedValue = value instanceof Number ? value : "'${value.toString()}'" + return "== ${wrappedValue}" + } else { + return "=~ /(${value})/" + } } JsonPaths transformToJsonPathWithTestsSideValues(def json) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy index a3f5fdb29e..762788be9a 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -51,7 +51,9 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements ]) stubMatchers { jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byValue(123)) jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byValue("abc")) jsonPath('$.number', byRegex(number())) jsonPath('$.aBoolean', byRegex(anyBoolean())) jsonPath('$.date', byDate()) @@ -87,8 +89,11 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements testMatchers { // asserts the jsonpath value against manual regex jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against the provided value + jsonPath('$.duck', byValue(123)) // asserts the jsonpath value against some default regex jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byValue("abc")) jsonPath('$.number', byRegex(number())) jsonPath('$.aBoolean', byRegex(anyBoolean())) // asserts vs inbuilt time related regex @@ -124,7 +129,9 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements def test = blockBuilder.toString() then: test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", String.class)).matches("[0-9]{3}")') + test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", Integer.class)).isEqualTo(123)') test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).matches("[\\\\p{L}]*")') + test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).isEqualTo("abc")') test.contains('assertThat(parsedJson.read("' + rootElement + '.number", String.class)).matches("-?\\\\d*(\\\\.\\\\d+)?")') test.contains('assertThat(parsedJson.read("' + rootElement + '.aBoolean", String.class)).matches("(true|false)")') test.contains('assertThat(parsedJson.read("' + rootElement + '.date", String.class)).matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")') 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 91f38a7676..e2f74b3bb5 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 @@ -24,6 +24,8 @@ import com.toomuchcoding.jsonassert.JsonAssertion import groovy.json.JsonOutput import groovy.json.JsonSlurper import net.minidev.json.JSONArray +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.MatchingType import spock.lang.Specification import spock.util.environment.RestoreSystemProperties @@ -758,14 +760,59 @@ class JsonToJsonPathsConverterSpec extends Specification { String jsonPath = '$.a.b.c.d' String regexPattern = ".*" expect: - '$.a.b.c[?(@.d =~ /(.*)/)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(jsonPath, regexPattern) + '$.a.b.c[?(@.d =~ /(.*)/)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.REGEX, jsonPath, regexPattern)) } - def "should return the path if no regex pattern is provided"() { + def "should convert a json path with value to a equality checking json path without quotes for numbers"() { + given: + String jsonPath = '$.a.b.c.d' + Integer value = 1234 + expect: + '$.a.b.c[?(@.d == 1234)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, value)) + } + + def "should convert a json path with value to a equality checking json path with quotes for strings"() { + given: + String jsonPath = '$.a.b.c.d' + String value = "foo" + expect: + '$.a.b.c[?(@.d == \'foo\')]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, value)) + } + + def "should return the path if no value is provided"() { given: String jsonPath = '$.a.b.c.d' expect: - '$.a.b.c.d' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(jsonPath, null) + '$.a.b.c.d' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.REGEX, jsonPath, null)) + } + + private BodyMatcher matcher(final MatchingType matchingType, final String jsonPath, final Object value) { + return new BodyMatcher() { + @Override + MatchingType matchingType() { + return matchingType + } + + @Override + String path() { + return jsonPath + } + + @Override + Object value() { + return value + } + + @Override + Integer minTypeOccurrence() { + return null + } + + @Override + Integer maxTypeOccurrence() { + return null + } + } } private void assertThatJsonPathsInMapAreValid(String json, JsonPaths pathAndValues) {