diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index fe34399319..3d98145cae 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -136,9 +136,12 @@ Besides status response may contain **headers** and **body**, which are specifie The contract can contain some dynamic properties - timestamps / ids etc. You don't want to enforce the consumers to stub their clocks to always return the same value of time so that it gets matched by the stub. That's why we allow you to provide the dynamic -parts in your contracts in the following way +parts in your contracts in two ways. One is to pass them directly in the +body and one to set them in a separate section called `testMatchers` and `stubMatchers`. -either via the `value` method +===== Dynamic properties inside the body + +You can set the properties inside the body either via the `value` method [source,groovy,indent=0] ---- @@ -161,7 +164,7 @@ $(client(...), server(...)) All of the aforementioned approaches are equal. That means that `stub` and `client` methods are aliases over the `consumer` method. Let's take a closer look at what we can do with those values in the subsequent sections. -==== Regular expressions +====== Regular expressions You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both @@ -198,8 +201,7 @@ so in your contract you can use it like this include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=contract_with_regex,indent=0] ---- - -==== Passing optional parameters +====== Passing optional parameters It is possible to provide optional parameters in your contract. It's only possible to have optional parameter for the: @@ -229,11 +231,116 @@ and the following stub: include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[tags=wiremock,indent=0] ---- -==== Executing custom methods on server side +====== Executing custom methods on server side It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" in the configuration. Please see the examples below: +===== Dynamic properties in matchers sections + +If you've been working with https://docs.pact.io/[Pact] this might seem familiar. Quite a few users +are used to having a separation between the body and setting dynamic parts of your contract. + +That's why you can profit from two separate sections. One is called `stubMatchers` where you can +define the dynamic values that should end up in a stub. You can set it in the `request` or `inputMessage` +part of your contract. The other is called `testMatchers` which is present in the `response` or +`outputMessage` side of the contract. + +Currently we support only JSON Path based matchers with the following matching possibilities. +For `stubMatchers`: + +- `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 +match the regex for ISO Date +- `byTimestamp()` - the value taken from the response via the provided JSON Path needs +to match the regex for ISO DateTime +- `byTime()` - the value taken from the response via the provided JSON Path needs to +match the regex for ISO Time + +For `testMatchers`: + +- `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 +match the regex for ISO Date +- `byTimestamp()` - the value taken from the response via the provided JSON Path needs +to match the regex for ISO DateTime +- `byTime()` - the value taken from the response via the provided JSON Path needs to +match the regex for ISO Time +- `byType()` - the value taken from the response via the provided JSON Path needs to +be of the same type as the type defined in the body of the response in the contract. +`byType` can take a closure where you can set `minOccurrence` and `maxOccurrence`. +That way you can assert on the size of the collection. + +Let's take a look at the following example: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy[tags=matchers,indent=0] +---- + +In this example we're providing the dynamic portions of the contract in the matchers sections. + For the request part you can see that for all fields but `valueWithoutAMatcher` we're setting + explicitly the values of regular expressions we'd like the stub to contain. For the `valueWithoutAMatcher` + the verification will take place in the same way as without the usage of matchers - the test + will perform an equality check in this case. + +For the response side in the `testMatchers` section we're defining all the dynamic parts + in a similar manner. The only difference is that we have the `byType` matchers too. In that + case we're checking 4 fields in the way that we're verifying whether the response from the test + has a value whose JSON path matching the given field is of the same type as the one defined in the response body and: + + - for `$.valueWithTypeMatch` - we're just checking the whether the type is the same + - for `$.valueWithMin` - we're checking the type and assert if the size is greater or equal to the min occurrence + - for `$.valueWithMax` - we're checking the type and assert if the size is smaller or equal to the max occurrence + - for `$.valueWithMinMax` - we're checking the type and assert if the size is between the min and max occurrence + +The resulting test would look more or less like this (note that we're separating the autogenerated +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\"}"); + + // 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); +---- + +and the WireMock stub like this: + +[source,json,indent=0] +---- +include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[tags=matchers,indent=0] +---- + + ===== Contract DSL [source,groovy,indent=0] 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 new file mode 100644 index 0000000000..85aae742ce --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy @@ -0,0 +1,39 @@ +package org.springframework.cloud.contract.spec.internal + +/** + * A jsonPathMatchers for the given path. + * + * @author Marcin Grzejszczak + * @since 1.0.3 + */ +interface BodyMatcher { + + /** + * What kind of matching are we dealing with + */ + MatchingType matchingType() + + /** + * Path to the path. Example for JSON it will be JSON Path + */ + String path() + + /** + * Optional value that the given path should be checked against. + * If there is no value then presence will be checked only together with + * type check. Example if we expect a JSON Path path {@code $.a} to be matched + * by type, the defined response body contained an integer but the actual one + * contained a string then the assertion should fail + */ + String value() + + /** + * Min no of occurrence when matching by type. In all other cases it will be ignored + */ + Integer minTypeOccurrence() + + /** + * Max no of occurrence when matching by type. In all other cases it will be ignored + */ + Integer maxTypeOccurrence() +} \ No newline at end of file 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 new file mode 100644 index 0000000000..fb8a9d3248 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -0,0 +1,103 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.Canonical +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +/** + * Matching strategy of dynamic parts of the body. + * + * @author Marcin Grzejszczak + * @since 1.0.3 + */ +@CompileStatic +class BodyMatchers { + private final RegexPatterns regexPatterns = new RegexPatterns() + private final List jsonPathRegexMatchers = [] + + void jsonPath(String path, MatchingTypeValue matchingType) { + this.jsonPathRegexMatchers << new JsonPathBodyMatcher(path, matchingType) + } + + boolean hasMatchers() { + return !this.jsonPathRegexMatchers.empty + } + + List jsonPathMatchers() { + return this.jsonPathRegexMatchers + } + + MatchingTypeValue byDate() { + return new MatchingTypeValue(MatchingType.DATE, this.regexPatterns.isoDate()) + } + + MatchingTypeValue byTime() { + return new MatchingTypeValue(MatchingType.TIME, this.regexPatterns.isoTime()) + } + + MatchingTypeValue byTimestamp() { + return new MatchingTypeValue(MatchingType.TIMESTAMP, this.regexPatterns.isoDateTime()) + } + + MatchingTypeValue byRegex(String regex) { + assert regex + return new MatchingTypeValue(MatchingType.REGEX, regex) + } +} + +@ToString +@EqualsAndHashCode +@Canonical +@CompileStatic +class JsonPathBodyMatcher implements BodyMatcher { + String jsonPath + MatchingTypeValue matchingTypeValue + + @Override + MatchingType matchingType() { + return this.matchingTypeValue.type + } + + @Override + String path() { + return this.jsonPath + } + + @Override + String value() { + return this.matchingTypeValue.value + } + + @Override + Integer minTypeOccurrence() { + return this.matchingTypeValue.minTypeOccurrence + } + + @Override + Integer maxTypeOccurrence() { + return this.matchingTypeValue.maxTypeOccurrence + } +} + +/** + * Matching type with corresponding values + */ +@Canonical +class MatchingTypeValue { + MatchingType type + + /** + * Value of regular expression + */ + String value + + /** + * Min occurrence when matching by type + */ + Integer minTypeOccurrence + + /** + * Max occurrence when matching by type + */ + Integer maxTypeOccurrence +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy index c536e8c64c..fe5b0691d1 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy @@ -40,6 +40,7 @@ class Input extends Common { Headers messageHeaders BodyType messageBody ExecutionProperty assertThat + BodyMatchers matchers Input() {} @@ -104,6 +105,12 @@ class Input extends Common { void assertThat(String assertThat) { this.assertThat = new ExecutionProperty(assertThat) } + + void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) { + this.matchers = new BodyMatchers() + closure.delegate = this.matchers + closure() + } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy new file mode 100644 index 0000000000..1b2a513362 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy @@ -0,0 +1,15 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic + +/** + * Represents the type of matching the should be done against + * the body of the request or response. + * + * @author Marcin Grzejszczak + * @since 1.0.3 + */ +@CompileStatic +enum MatchingType { + EQUALITY, TYPE, DATE, TIME, TIMESTAMP, REGEX +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy index decfeebd2f..f1672a5ae0 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy @@ -39,9 +39,8 @@ class OptionalProperty { return "($value)?" } - @Override - public String toString() { + String toString() { return optionalPattern() } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy index 8f5290eed2..4b32ecc908 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy @@ -33,6 +33,7 @@ class OutputMessage extends Common { Headers headers DslProperty body ExecutionProperty assertThat + ResponseBodyMatchers matchers OutputMessage() {} @@ -75,6 +76,12 @@ class OutputMessage extends Common { } return new DslProperty(value, server.serverValue) } + + void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) { + this.matchers = new ResponseBodyMatchers() + closure.delegate = this.matchers + closure() + } } @CompileStatic @@ -94,3 +101,4 @@ class ClientOutputMessage extends OutputMessage { super(request) } } + diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy index 7578f1f656..818e40f72f 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy @@ -43,6 +43,7 @@ class Request extends Common { RequestHeaders headers Body body Multipart multipart + BodyMatchers matchers Request() { } @@ -204,6 +205,12 @@ class Request extends Common { return value(client) } + void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) { + this.matchers = new BodyMatchers() + closure.delegate = this.matchers + closure() + } + @Override DslProperty value(ClientDslProperty client, ServerDslProperty server) { if (server.clientValue instanceof Pattern) { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy index a459219b04..c9ca105382 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -41,6 +41,7 @@ class Response extends Common { ResponseHeaders headers Body body boolean async + ResponseBodyMatchers matchers Response() { } @@ -111,6 +112,12 @@ class Response extends Common { return value(server) } + void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) { + this.matchers = new ResponseBodyMatchers() + closure.delegate = this.matchers + closure() + } + @Override DslProperty value(ClientDslProperty client, ServerDslProperty server) { if (client.clientValue instanceof Pattern) { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy new file mode 100644 index 0000000000..ef9eaf9ab2 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy @@ -0,0 +1,42 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Body matchers for the response side (output message, REST response) + * + * @author Marcin Grzejszczak + * @since 1.0.3 + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ResponseBodyMatchers extends BodyMatchers { + + MatchingTypeValue byType() { + return new MatchingTypeValue(MatchingType.TYPE) + } + + MatchingTypeValue byType(@DelegatesTo(MatchingTypeValueHolder) Closure closure) { + MatchingTypeValueHolder matchingTypeValue = new MatchingTypeValueHolder() + closure.delegate = matchingTypeValue + return closure() as MatchingTypeValue + } +} + +@CompileStatic +class MatchingTypeValueHolder { + MatchingTypeValue matchingTypeValue = new MatchingTypeValue() + + MatchingTypeValue minOccurrence(int number) { + this.matchingTypeValue.minTypeOccurrence = number + return this.matchingTypeValue + } + + MatchingTypeValue maxOccurrence(int number) { + this.matchingTypeValue.maxTypeOccurrence = number + return this.matchingTypeValue + } +} 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 a66e0cc766..f993d092dc 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 @@ -22,17 +22,19 @@ import java.util.regex.Pattern; import org.apache.camel.Exchange; import org.apache.camel.Predicate; import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.BodyMatcher; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import com.fasterxml.jackson.core.JsonProcessingException; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.toomuchcoding.jsonassert.JsonAssertion; -import com.toomuchcoding.jsonassert.JsonVerifiable; /** * Passes through a message that matches the one defined in the DSL @@ -54,9 +56,13 @@ class StubRunnerCamelPredicate implements Predicate { return false; } Object inputMessage = exchange.getIn().getBody(); + BodyMatchers matchers = this.groovyDsl.getInput().getMatchers(); + Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody()); + Object matchingInputMessage = JsonToJsonPathsConverter + .removeMatchingJsonPaths(dslBody, matchers); JsonPaths jsonPaths = JsonToJsonPathsConverter .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( - this.groovyDsl.getInput().getMessageBody()); + matchingInputMessage); DocumentContext parsedJson; try { parsedJson = JsonPath @@ -67,14 +73,20 @@ class StubRunnerCamelPredicate implements Predicate { } boolean matches = true; for (MethodBufferingJsonVerifiable path : jsonPaths) { - matches &= matchesJsonPath(parsedJson, path); + matches &= matchesJsonPath(parsedJson, path.jsonPath()); + } + if (matchers != null && matchers.hasMatchers()) { + for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + matches &= matchesJsonPath(parsedJson, jsonPath); + } } return matches; } - private boolean matchesJsonPath(DocumentContext parsedJson, JsonVerifiable jsonVerifiable) { + private boolean matchesJsonPath(DocumentContext parsedJson, String jsonPath) { try { - JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonVerifiable.jsonPath()); + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath); return true; } catch (Exception e) { return false; 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 9df770bf3e..4a746fcb6d 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 @@ -20,10 +20,13 @@ import java.util.Map; import java.util.regex.Pattern; import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.BodyMatcher; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import org.springframework.integration.core.MessageSelector; import org.springframework.messaging.Message; @@ -32,7 +35,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.toomuchcoding.jsonassert.JsonAssertion; -import com.toomuchcoding.jsonassert.JsonVerifiable; /** * Passes through a message that matches the one defined in the DSL @@ -54,9 +56,13 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector { return false; } Object inputMessage = message.getPayload(); + BodyMatchers matchers = this.groovyDsl.getInput().getMatchers(); + Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody()); + Object matchingInputMessage = JsonToJsonPathsConverter + .removeMatchingJsonPaths(dslBody, matchers); JsonPaths jsonPaths = JsonToJsonPathsConverter .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( - this.groovyDsl.getInput().getMessageBody()); + matchingInputMessage); DocumentContext parsedJson; try { parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage)); @@ -66,16 +72,21 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector { } boolean matches = true; for (MethodBufferingJsonVerifiable path : jsonPaths) { - matches &= matchesJsonPath(parsedJson, path); + matches &= matchesJsonPath(parsedJson, path.jsonPath()); + } + if (matchers != null && matchers.hasMatchers()) { + for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + matches &= matchesJsonPath(parsedJson, jsonPath); + } } return matches; } - private boolean matchesJsonPath(DocumentContext parsedJson, - JsonVerifiable jsonVerifiable) { + private boolean matchesJsonPath(DocumentContext parsedJson, String jsonPath) { try { JsonAssertion.assertThat(parsedJson) - .matchesJsonPath(jsonVerifiable.jsonPath()); + .matchesJsonPath(jsonPath); return true; } catch (Exception e) { 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 496c62333b..471bcb21c2 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 @@ -20,10 +20,13 @@ import java.util.Map; import java.util.regex.Pattern; import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.BodyMatcher; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import org.springframework.integration.core.MessageSelector; import org.springframework.messaging.Message; @@ -32,7 +35,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.toomuchcoding.jsonassert.JsonAssertion; -import com.toomuchcoding.jsonassert.JsonVerifiable; /** * Passes through a message that matches the one defined in the DSL @@ -50,31 +52,40 @@ class StubRunnerStreamMessageSelector implements MessageSelector { @Override public boolean accept(Message message) { - if(!headersMatch(message)){ + if (!headersMatch(message)) { return false; } Object inputMessage = message.getPayload(); + BodyMatchers matchers = this.groovyDsl.getInput().getMatchers(); + Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody()); + Object matchingInputMessage = JsonToJsonPathsConverter + .removeMatchingJsonPaths(dslBody, matchers); JsonPaths jsonPaths = JsonToJsonPathsConverter .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( - this.groovyDsl.getInput().getMessageBody()); + matchingInputMessage); DocumentContext parsedJson; try { parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage)); - for (MethodBufferingJsonVerifiable it : jsonPaths) { - if (!matchesJsonPath(parsedJson, it)) { - return false; - } - } } catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot parse JSON", e); + throw new IllegalStateException("Cannot serialize to JSON", e); } - return true; + boolean matches = true; + for (MethodBufferingJsonVerifiable path : jsonPaths) { + matches &= matchesJsonPath(parsedJson, path.jsonPath()); + } + if (matchers != null && matchers.hasMatchers()) { + for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher.path(), matcher.value()); + matches &= matchesJsonPath(parsedJson, jsonPath); + } + } + return matches; } - private boolean matchesJsonPath(DocumentContext parsedJson, JsonVerifiable jsonVerifiable) { + private boolean matchesJsonPath(DocumentContext parsedJson, String jsonPath) { try { - JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonVerifiable.jsonPath()); + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath); return true; } catch (Exception e) { return false; diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy new file mode 100644 index 0000000000..ba71c68044 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy @@ -0,0 +1,137 @@ +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import org.apache.camel.Exchange +import org.apache.camel.Message +import org.springframework.cloud.contract.spec.Contract +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerCamelPredicateSpec extends Specification { + Exchange exchange = Stub(Exchange) + Message message = Stub(Message) + + def "should return false if headers don't match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageBody(foo: "bar") + messageHeaders { + header("foo", $(c(regex("[0-9]{3}")), p(123))) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + + def "should return false if headers match and body doesn't"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + + def "should return false if headers match and body doesn't when it's using matchers"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: "non matching stuff") + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + def "should return true if headers and body match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: 123 + ] + expect: + predicate.matches(exchange) + + } + def "should return true if headers and body using matchers match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: 123) + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: 123 + ] + expect: + predicate.matches(exchange) + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy new file mode 100644 index 0000000000..62cc6f3bae --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy @@ -0,0 +1,130 @@ +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.messaging.Message +import spock.lang.Specification +/** + * @author Marcin Grzejszczak + */ +class StubRunnerIntegrationMessageSelectorSpec extends Specification { + Message message = Mock(Message) + + def "should return false if headers don't match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageBody(foo: "bar") + messageHeaders { + header("foo", $(c(regex("[0-9]{3}")), p(123))) + } + } + } + and: + StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl) + message.headers >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return false if headers match and body doesn't"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + } + } + and: + StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return false if headers match and body doesn't when it's using matchers"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: "non matching stuff") + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return true if headers and body match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + + } + } + and: + StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: 123 + ] + expect: + predicate.accept(message) + } + + def "should return true if headers and body using matchers match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: 123) + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: 123 + ] + expect: + predicate.accept(message) + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy new file mode 100644 index 0000000000..3f85df542f --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy @@ -0,0 +1,131 @@ +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.messaging.Message +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerStreamMessageSelectorSpec extends Specification { + Message message = Mock(Message) + + def "should return false if headers don't match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageBody(foo: "bar") + messageHeaders { + header("foo", $(c(regex("[0-9]{3}")), p(123))) + } + } + } + and: + StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl) + message.headers >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return false if headers match and body doesn't"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + } + } + and: + StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return false if headers match and body doesn't when it's using matchers"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: "non matching stuff") + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: "non matching stuff" + ] + expect: + !predicate.accept(message) + } + + def "should return true if headers and body match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + + } + } + and: + StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: 123 + ] + expect: + predicate.accept(message) + } + + def "should return true if headers and body using matchers match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: 123) + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl) + message.headers >> [ + foo: 123 + ] + message.payload >> [ + foo: 123 + ] + expect: + predicate.accept(message) + } +} 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 9406083d08..a9fe20ddf8 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 @@ -374,4 +374,235 @@ class DslToWireMockClientConverterSpec extends Specification { , json, false) } + def 'should convert dsl to wiremock with stub matchers'() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl_from_docs.groovy") + file.write(''' + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath '/get' + 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", + list: [ + some: [ + nested: [ + json: "with value", + anothervalue: 4 + ] + ], + someother: [ + nested: [ + json: "with value", + anothervalue: 4 + ] + ] + ] + ]) + stubMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath('$.list.some.nested.json', byRegex(".*")) + } + headers { + contentType(applicationJson()) + } + } + response { + status 200 + 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", + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 + ], + ]) + testMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.number', byRegex(number())) + 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) + }) + } + headers { + contentType(applicationJson()) + } + } + } + ''') + when: + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals(//tag::matchers[] + ''' +{ + "request" : { + "urlPath" : "/get", + "method" : "GET", + "headers" : { + "Content-Type" : { + "matches" : "application/json.*" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.valueWithoutAMatcher == 'foo')]" + }, { + "matchesJsonPath" : "$[?(@.valueWithTypeMatch == 'string')]" + }, { + "matchesJsonPath" : "$.list.some.nested[?(@.anothervalue == 4)]" + }, { + "matchesJsonPath" : "$.list.someother.nested[?(@.anothervalue == 4)]" + }, { + "matchesJsonPath" : "$.list.someother.nested[?(@.json == 'with value')]" + }, { + "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]" + }, { + "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]" + }, { + "matchesJsonPath" : "$[?(@.number =~ /(-?\\\\d*(\\\\.\\\\d+)?)/)]" + }, { + "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]" + }, { + "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]" + }, { + "matchesJsonPath" : "$[?(@.dateTime =~ /(([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]))/)]" + }, { + "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]" + }, { + "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]" + } ] + }, + "response" : { + "status" : 200, + "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\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3]}", + "headers" : { + "Content-Type" : "application/json" + } + } +} +''' +//end::matchers[] + , json, false) + } + + def 'should convert dsl to wiremock with stub matchers with docs example'() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl_from_docs.groovy") + file.write(''' + org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: 'abc@abc.com', + callback_url: 'http://partners.com' + ) + stubMatchers { + jsonPath('$.email', byRegex(email())) + jsonPath('$.callback_url', byRegex(hostname())) + } + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: "123123", + message: "User not found by email == [not.existing@user.com]" + ) + testMatchers { + jsonPath('$.code', byRegex("123123")) + jsonPath('$.message', byRegex("User not found by email == ${email()}")) + } + } + } + ''') + when: + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals( + ''' + { + "request" : { + "url" : "/users/password", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})/)]" + }, { + "matchesJsonPath" : "$[?(@.callback_url =~ /(((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?)/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 404, + "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } + }, + "priority" : 1 + } + ''' + , json, false) + } + } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy index 97283c9596..56e68bcda8 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy @@ -148,4 +148,8 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ blockBuilder.addLine("response.getHeaderString('$property') ${convertHeaderComparison(value)}") } + @Override + protected String postProcessJsonPathCall(String jsonPath) { + return jsonPath.replace('$', '\\$') + } } 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 88366ce7b8..1eb49b73d0 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 @@ -82,7 +82,7 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { if (outputMessage.headers) { bb.addLine(addCommentSignIfRequired('and:')).startBlock() } - validateResponseBodyBlock(bb, outputMessage.body.serverValue) + validateResponseBodyBlock(bb, outputMessage.matchers, outputMessage.body.serverValue) } if (outputMessage.assertThat) { bb.addLine(outputMessage.assertThat.executionCommand) 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 4157874499..985eaef508 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.contract.verifier.builder +import com.jayway.jsonpath.JsonPath +import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.apache.commons.lang3.StringEscapeUtils @@ -29,7 +31,6 @@ import org.springframework.cloud.contract.verifier.util.MapConverter import java.util.regex.Pattern import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue - /** * Main class for building method body. * @@ -109,17 +110,17 @@ abstract class MethodBodyBuilder { protected abstract void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) /** - * Appends to the {@link BlockBuilder} the assertion for the given header element + * Appends to the {@link BlockBuilder} the assertion for the given header path */ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern pattern) /** - * Appends to the {@link BlockBuilder} the assertion for the given header element + * Appends to the {@link BlockBuilder} the assertion for the given header path */ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) /** - * Appends to the {@link BlockBuilder} the assertion for the given header element + * Appends to the {@link BlockBuilder} the assertion for the given header path */ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, String value) @@ -258,12 +259,22 @@ abstract class MethodBodyBuilder { /** * Builds the response body verification part. The code will differ depending on the * ContentType, type of response etc. The result will be appended to {@link BlockBuilder} + * @deprecated - use {@link MethodBodyBuilder#validateResponseBodyBlock(org.springframework.cloud.contract.verifier.builder.BlockBuilder, org.springframework.cloud.contract.spec.internal.BodyMatchers, java.lang.Object)} */ + @Deprecated protected void validateResponseBodyBlock(BlockBuilder bb, Object responseBody) { + validateResponseBodyBlock(bb, null, responseBody) + } + + /** + * Builds the response body verification part. The code will differ depending on the + * ContentType, type of response etc. The result will be appended to {@link BlockBuilder} + */ + protected void validateResponseBodyBlock(BlockBuilder bb, BodyMatchers bodyMatchers, Object responseBody) { ContentType contentType = getResponseContentType() Object convertedResponseBody = responseBody if (convertedResponseBody instanceof GString) { - convertedResponseBody = extractValue(convertedResponseBody, contentType, { Object o -> o instanceof DslProperty ? o.serverValue : o }) + convertedResponseBody = extractValue(convertedResponseBody as GString, contentType, { Object o -> o instanceof DslProperty ? o.serverValue : o }) } if (contentType != ContentType.TEXT) { convertedResponseBody = MapConverter.getTestSideValues(convertedResponseBody) @@ -271,15 +282,7 @@ abstract class MethodBodyBuilder { convertedResponseBody = StringEscapeUtils.escapeJava(convertedResponseBody.toString()) } if (contentType == ContentType.JSON) { - appendJsonPath(bb, getResponseAsString()) - JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(convertedResponseBody) - jsonPaths.each { - String method = it.method() - String postProcessedMethod = postProcessJsonPathCall(method) - bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) - addColonIfRequired(bb) - } - processBodyElement(bb, "", convertedResponseBody) + addJsonResponseBodyCheck(bb, convertedResponseBody, bodyMatchers) } else if (contentType == ContentType.XML) { bb.addLine(getParsedXmlResponseBodyString(getResponseAsString())) addColonIfRequired(bb) @@ -291,6 +294,80 @@ abstract class MethodBodyBuilder { } } + private void addJsonResponseBodyCheck(BlockBuilder bb, convertedResponseBody, BodyMatchers bodyMatchers) { + appendJsonPath(bb, getResponseAsString()) + Object copiedBody = convertedResponseBody.clone() + convertedResponseBody = JsonToJsonPathsConverter.removeMatchingJsonPaths(convertedResponseBody, bodyMatchers) + JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(convertedResponseBody) + jsonPaths.each { + String method = it.method() + String postProcessedMethod = postProcessJsonPathCall(method) + bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) + addColonIfRequired(bb) + bb.endBlock() + } + if (bodyMatchers?.hasMatchers()) { + bb.addLine(addCommentSignIfRequired('and:')) + bb.startBlock() + // 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())})" + bb.addLine(postProcessJsonPathCall(method)) + addColonIfRequired(bb) + } else { + Object elementFromBody = JsonPath.parse(copiedBody).read(it.path()) + if (!elementFromBody) { + throw new IllegalStateException("Entry for the provided JSON path [${it.path()}] doesn't exist in the body [${JsonOutput.toJson(copiedBody)}]") + } + if (it.minTypeOccurrence() || it.maxTypeOccurrence()) { + checkType(bb, it, elementFromBody) + String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class).size()).${sizeCheckMethod(it)}" + bb.addLine(postProcessJsonPathCall(method)) + addColonIfRequired(bb) + } else { + checkType(bb, it, elementFromBody) + } + } + } + } + processBodyElement(bb, "", convertedResponseBody) + } + + protected void checkType(BlockBuilder bb, BodyMatcher it, Object elementFromBody) { + String method = "assertThat((Object) parsedJson.read(${quotedAndEscaped(it.path())})).isInstanceOf(${classToCheck(elementFromBody).name}.class)" + bb.addLine(postProcessJsonPathCall(method)) + addColonIfRequired(bb) + } + + // we want to make the type more generic (e.g. not ArrayList but List) + protected Class classToCheck(Object elementFromBody) { + switch (elementFromBody.class) { + case List: + return List + case Set: + return Set + case Map: + return Map + default: + return elementFromBody.class + } + } + + protected String sizeCheckMethod(BodyMatcher bodyMatcher) { + if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) { + return "isStrictlyBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" + } else if (bodyMatcher.minTypeOccurrence() != null ) { + return "isLessThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" + } else if (bodyMatcher.maxTypeOccurrence() != null) { + return "isGreaterThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" + } + } + + protected String quotedAndEscaped(String string) { + return '"' + StringEscapeUtils.escapeJava(string) + '"' + } + /** * Post processing of each JSON path entry */ @@ -324,7 +401,7 @@ abstract class MethodBodyBuilder { } /** - * Appends to the {@link BlockBuilder} the assertion for the given header element + * Appends to the {@link BlockBuilder} the assertion for the given header path */ protected void processHeaderElement(BlockBuilder blockBuilder, String property, Object value) { } 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 b8be7e54e9..1ade040def 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 @@ -145,7 +145,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { if (response.body) { bb.endBlock() bb.addLine(addCommentSignIfRequired('and:')).startBlock() - validateResponseBodyBlock(bb, response.body.serverValue) + validateResponseBodyBlock(bb, response.matchers, response.body.serverValue) } } 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 2725e8c158..d2500dff8f 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 @@ -75,15 +75,22 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } ContentType contentType = tryToGetContentType(request.body.clientValue, request.headers) if (contentType == ContentType.JSON) { - JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( - getMatchingStrategyFromBody(request.body)?.clientValue) - if (values.empty) { + def body = getMatchingStrategyFromBody(request.body)?.clientValue + body = JsonToJsonPathsConverter.removeMatchingJsonPaths(body, request.matchers) + JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body) + if (values.empty && !request.matchers?.hasMatchers()) { requestPattern.withRequestBody(WireMock.equalToJson(JsonOutput.toJson(getMatchingStrategy(request.body.clientValue).clientValue), false, false)) } else { values.findAll{ !it.assertsSize() }.each { requestPattern.withRequestBody(WireMock.matchingJsonPath(it.jsonPath().replace("\\\\", "\\"))) } } + if (request.matchers?.hasMatchers()) { + request.matchers.jsonPathMatchers().each { + String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it.path(), it.value()) + requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\"))) + } + } } else if (contentType == ContentType.XML) { requestPattern.withRequestBody(WireMock.equalToXml(getMatchingStrategy(request.body.clientValue).clientValue.toString())) } else if (containsPattern(request?.body)) { 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 d7e07dc413..3876b53483 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 @@ -16,11 +16,15 @@ package org.springframework.cloud.contract.verifier.util +import com.jayway.jsonpath.JsonPath 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.verifier.config.ContractVerifierConfigProperties @@ -58,15 +62,52 @@ class JsonToJsonPathsConverter { } } - public JsonPaths transformToJsonPathWithTestsSideValues(def json) { + /** + * Removes from the parsed json any JSON path matching entries. + * That way we remain with values that should be checked in the auto-generated + * fashion. + * + * @param json - parsed JSON + * @param bodyMatchers - the part of request / response that contains matchers + * @return json with removed entries + */ + 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 -> + JsonPath.parse(json).delete(matcher.path()) + } + } + return json + } + + /** + * For the given JSON path and regex pattern converts it into a JSON path + * that checks the Pattern + * + * @param path - JSON path + * @param pattern - pattern to check for the last element of JSON path + * @return JSON path that checks the regex for its last element + */ + static String convertJsonPathAndRegexToAJsonPath(String path, String pattern) { + if (!pattern) { + return path + } + int lastIndexOfDot = path.lastIndexOf(".") + String toLastDot = path.substring(0, lastIndexOfDot) + String fromLastDot = path.substring(lastIndexOfDot + 1) + return "${toLastDot}[?(@.${fromLastDot} =~ /(${pattern})/)]" + } + + JsonPaths transformToJsonPathWithTestsSideValues(def json) { return transformToJsonPathWithValues(json, SERVER_SIDE) } - public JsonPaths transformToJsonPathWithStubsSideValues(def json) { + JsonPaths transformToJsonPathWithStubsSideValues(def json) { return transformToJsonPathWithValues(json, CLIENT_SIDE) } - public static JsonPaths transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(def json) { + static JsonPaths transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(def json) { return new JsonToJsonPathsConverter() .transformToJsonPathWithValues(json, CLIENT_SIDE) } 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 new file mode 100644 index 0000000000..a3f5fdb29e --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2016 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 + * + * 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. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.util.SyntaxChecker +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements WireMockStubVerifier { + + @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties( + assertJsonSize: true + ) + + @Issue('#185') + def "should allow to set dynamic values via stub / test matchers for [#methodBuilderName]"() { + given: + //tag::matchers[] + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + 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" + ]) + stubMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + } + headers { + contentType(applicationJson()) + } + } + response { + status 200 + 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", + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 + ], + ]) + testMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.number', byRegex(number())) + 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) + }) + } + headers { + contentType(applicationJson()) + } + } + } + //end::matchers[] + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", String.class)).matches("[0-9]{3}")') + test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).matches("[\\\\p{L}]*")') + 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])")') + test.contains('assertThat(parsedJson.read("' + rootElement + '.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])")') + test.contains('assertThat(parsedJson.read("' + rootElement + '.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")') + test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithTypeMatch")).isInstanceOf(java.lang.String.class)') + test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMin")).isInstanceOf(java.util.List.class)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1)') + test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMax")).isInstanceOf(java.util.List.class)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3)') + test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3)') + !test.contains('cursor') + and: + SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString()) + where: + methodBuilderName | methodBuilder | rootElement + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$' + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' + } + +} 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 fb2e4152b3..91f38a7676 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 @@ -753,6 +753,21 @@ class JsonToJsonPathsConverterSpec extends Specification { } } + def "should convert a json path with regex to a regex checking json path"() { + given: + String jsonPath = '$.a.b.c.d' + String regexPattern = ".*" + expect: + '$.a.b.c[?(@.d =~ /(.*)/)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(jsonPath, regexPattern) + } + + def "should return the path if no regex pattern is provided"() { + given: + String jsonPath = '$.a.b.c.d' + expect: + '$.a.b.c.d' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(jsonPath, null) + } + private void assertThatJsonPathsInMapAreValid(String json, JsonPaths pathAndValues) { DocumentContext parsedJson = JsonPath.using(Configuration.builder().options(Option.ALWAYS_RETURN_LIST).build()).parse(json); pathAndValues.each {