Support for Pact v3 (#569)

* Upgraded pact-jvm-model to 3.5.13
* Enhanced the conversion of Spring Cloud contracts to Pact contracts using the v3 spec.
Introduces breaking change returning a list of `Pact`s instead of one.
* Enhanced the conversion of Pact contracts using the v3 spec tp Spring Cloud contracts.
* Implemented conversion of Pact v3 messaging to/from Spring Cloud contracts
* Updated code based on comments in the PR
* Added null matcher
* Added missing null matcher conversion from SCC to Pact
* Added number, integer and decimal matchers
* Added support for multiple matchers for the same json path.
Currently only the AND rule logic is supported.
* Added value generator functionality
* Refactored `stubMatchers` and `testMatchers` to support multiple types of "matcher groups", eg. header, path & query
* Refactored e37a8c5d82f96da6e1a698331afc62e6cf747bd6 in favor of a less invasive change using deprecation
* Added header matcher conversion
* Add/updated the ASF license header
* Fixed an issue with the header matchers while updating the documentation
* Updated the documentation
* Made some last minute changes to the `bodyMatchers`: `byNull()` isn't supported by WireMock

Closes #583
Fixes #595
This commit is contained in:
Tim Ysewyn
2018-04-02 22:06:51 +02:00
committed by Marcin Grzejszczak
parent ff8fd0a16c
commit de529e222e
67 changed files with 3685 additions and 561 deletions

View File

@@ -371,8 +371,11 @@ want to force the consumers to stub their clocks to always return the same value
so that it gets matched by the stub.
For Groovy DSL you can provide the dynamic parts in your contracts
in two ways: pass them directly in the body or set them in separate sections called
`testMatchers` and `stubMatchers`.
in two ways: pass them directly in the body or set them in a separate section called
`bodyMatchers`.
NOTE: Before 2.0.0 these were set using `testMatchers` and `stubMatchers`,
check out the https://github.com/spring-cloud/spring-cloud-contract/wiki/Spring-Cloud-Contract-2.0-Migration-Guide[migration guide] for more information.
For YAML you can only use the `matchers` section.
@@ -732,11 +735,12 @@ If you work with https://docs.pact.io/[Pact], the following discussion may seem
Quite a few users are used to having a separation between the body and setting the
dynamic parts of a contract.
You can use two separate sections:
You can use the `bodyMatchers` section for two reasons:
* `stubMatchers`, which lets you define the dynamic values that should end up in a stub.
* Define the dynamic values that should end up in a stub.
You can set it in the `request` or `inputMessage` part of your contract.
* `testMatchers`, which is present in the `response` or `outputMessage` side of the
* Verify the result of your test.
This section is present in the `response` or `outputMessage` side of the
contract.
Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the
@@ -744,7 +748,7 @@ following matching possibilities:
.Groovy DSL
* For `stubMatchers`:
* For the stubs:
** `byEquality()`: The value taken from the response via the provided JSON Path must be
equal to the value provided in the contract.
** `byRegex(...)`: The value taken from the response via the provided JSON Path must
@@ -755,7 +759,7 @@ match the regex for an ISO Date value.
match the regex for an ISO DateTime value.
** `byTime()`: The value taken from the response via the provided JSON Path must
match the regex for an ISO Time value.
* For `testMatchers`:
* For the verification:
** `byEquality()`: The value taken from the response via the provided JSON Path must be
equal to the provided value in the contract.
** `byRegex(...)`: The value taken from the response via the provided JSON Path must
@@ -781,6 +785,7 @@ following, depending on the JSON path:
*** `Map`: If you point to a `Map`.
*** `Number`: If you point to `Integer`, `Double`, or other kind of number.
*** `Boolean`: If you point to a `Boolean`.
** `byNull()`: The value taken from the response via the provided JSON Path must be null
.YAML
@@ -824,6 +829,7 @@ Below you can find the allowed list of `type`s.
** `by_type`
*** there are 2 additional fields accepted: `minOccurrence` and `maxOccurrence`.
** `by_command`
** `by_null`
Consider the following example:
@@ -846,7 +852,7 @@ contain are explicitly set. For the `valueWithoutAMatcher`, the verification tak
in the same way as without the use of matchers. In that case, the test performs an
equality check.
For the response side in the `testMatchers` section, we define the dynamic parts in a
For the response side in the `bodyMatchers` section, we define the dynamic parts in a
similar manner. The only difference is that the `byType` matchers are also present. The
verifier engine checks four fields to verify whether the response from the test
has a value for which the JSON path matches the given field, is of the same type as the one
@@ -868,7 +874,7 @@ separates the autogenerated assertions and the assertion from matchers):
// 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\"}");
.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\",\"key\":{\"complex.key\":\"foo\"}}");
// when:
ResponseOptions response = given().spec(request)
@@ -879,29 +885,30 @@ separates the autogenerated assertions and the assertion from matchers):
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo");
assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo");
// and:
assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?");
assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(1);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).hasSizeLessThanOrEqualTo(3);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).hasSizeBetween(1, 3);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(0);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).hasSizeLessThanOrEqualTo(0);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
assertThatValueIsANumber(parsedJson.read("$.duck"));
assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
----
IMPORTANT: Notice that, for the `byCommand` method, the example calls the
@@ -917,7 +924,7 @@ The resulting WireMock stub is in the following example:
include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[tags=matchers,indent=0]
----
IMPORTANT: If you use a `matcher`, then the part of the request aned response that the
IMPORTANT: If you use a `matcher`, then the part of the request and response that the
`matcher` addresses with the JSON Path gets removed from the assertion. In the case of
verifying a collection, you must create matchers for *all* the elements of the
collection.
@@ -944,7 +951,7 @@ Contract.make {
]
]
)
testMatchers {
bodyMatchers {
jsonPath('$.events[0].operation', byRegex('.+'))
jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
jsonPath('$.events[0].status', byRegex('.+'))
@@ -999,7 +1006,7 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
If you're using asynchronous communication on the server side (your controllers are
returning `Callable`, `DeferredResult`, and so on), then, inside your contract, you must
provide a `sync()` method in the `response` section. The following code shows an example:
provide an `async()` method in the `response` section. The following code shows an example:
.Groovy DSL
[source,groovy,indent=0]
@@ -1378,8 +1385,15 @@ org.springframework.cloud.contract.verifier.converter.YamlContractConverter
==== Pact Converter
Spring Cloud Contract includes support for https://docs.pact.io/[Pact] representation of
contracts. Instead of using the Groovy DSL, you can use Pact files. In this section, we
present how to add Pact support for your project.
contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we
present how to add Pact support for your project. Note however that not all functionality is supported.
Starting with v3 you can combine multiple matcher for the same element;
you can use matchers for the body, headers, request and path; and you can use value generators.
Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic.
Next to that the request and path matchers are skipped during the conversion.
When using a date, time or datetime value generator with a given format,
the given format will be skipped and the ISO format will be used.
==== Pact Contract
@@ -1395,7 +1409,7 @@ The remainder of this section about using Pact refers to the preceding file.
==== Pact for Producers
On the producer side, you mustadd two additional dependencies to your plugin
On the producer side, you must add two additional dependencies to your plugin
configuration. One is the Spring Cloud Contract Pact support, and the other represents
the current Pact version that you use.
@@ -1429,10 +1443,10 @@ test might be as follows:
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8");
assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high");
assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
// and:
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
}
@@ -1443,17 +1457,18 @@ The corresponding generated stub might be as follows:
[source,javascript,indent=0]
----
{
"id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"request" : {
"url" : "/fraudcheck",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"equalTo" : "application/vnd.fraud.v1+json"
"matches" : "application/vnd\\.fraud\\.v1\\+json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.loanAmount == 99999)]"
"matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
}, {
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
} ]
@@ -1463,8 +1478,9 @@ The corresponding generated stub might be as follows:
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
}
}
},
"transformers" : [ "response-template" ]
},
}
----