Merge branch '1.0.x'

Stub / Test Matchers (#186)
Without this change we're forcing users to embed their dynamic properties inside the body. For some this is natural and acceptable, but especially for the users coming from the Pact world this sounds bizarre. Also some other people have a problem with remembering who the consumer / producer is etc.

With this change we're introducing the stubMatchers and testMatchers section. Thanks to this one can separate the body from defining the dynamic properties. Especially for Pact users this is more natural. Speaking of which this is a prerequisite for #96

fixes #185
This commit is contained in:
Marcin Grzejszczak
2017-01-10 17:21:38 +01:00
25 changed files with 1346 additions and 54 deletions

View File

@@ -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]

View File

@@ -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()
}

View File

@@ -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<BodyMatcher> jsonPathRegexMatchers = []
void jsonPath(String path, MatchingTypeValue matchingType) {
this.jsonPathRegexMatchers << new JsonPathBodyMatcher(path, matchingType)
}
boolean hasMatchers() {
return !this.jsonPathRegexMatchers.empty
}
List<BodyMatcher> 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
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -39,9 +39,8 @@ class OptionalProperty {
return "($value)?"
}
@Override
public String toString() {
String toString() {
return optionalPattern()
}
}

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -148,4 +148,8 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ
blockBuilder.addLine("response.getHeaderString('$property') ${convertHeaderComparison(value)}")
}
@Override
protected String postProcessJsonPathCall(String jsonPath) {
return jsonPath.replace('$', '\\$')
}
}

View File

@@ -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)

View File

@@ -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) {
}

View File

@@ -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)
}
}

View File

@@ -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)) {

View File

@@ -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)
}

View File

@@ -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) } | '$'
}
}

View File

@@ -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 {