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" ]
},
}
----

20
pom.xml
View File

@@ -24,6 +24,7 @@
<properties>
<checkstyle.version>2.17</checkstyle.version>
<pact.version>3.5.13</pact.version>
<jsch-agent.version>0.0.9</jsch-agent.version>
<spring-cloud-build.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-build.version>
<spring-cloud-zookeeper.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-zookeeper.version>
@@ -111,10 +112,27 @@
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-java8_2.12</artifactId>
<version>${pact.version}</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
<version>${pact.version}</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>

View File

@@ -40,7 +40,7 @@ dependencies {
testCompile "org.springframework.cloud:spring-cloud-starter-contract-stub-runner"
//tag::pact_dependency[]
testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact"
testCompile 'au.com.dius:pact-jvm-model:2.4.18'
testCompile 'au.com.dius:pact-jvm-model:3.5.13'
//end::pact_dependency[]
}

View File

@@ -52,7 +52,7 @@
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
<version>3.5.13</version>
<scope>test</scope>
</dependency>
<!-- end::pact_dependency[] -->

View File

@@ -11,7 +11,7 @@ buildscript {
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}"
//tag::pact_dependency[]
classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}"
classpath 'au.com.dius:pact-jvm-model:2.4.18'
classpath 'au.com.dius:pact-jvm-model:3.5.13'
//end::pact_dependency[]
}
}

View File

@@ -83,7 +83,7 @@
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
<version>3.5.13</version>
</dependency>
</dependencies>
</plugin>

View File

@@ -18,10 +18,36 @@
"clientId": "1234567890",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"$.body.clientId": {
"match": "regex",
"regex": "[0-9]{10}"
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body" : {
"$.clientId": {
"matchers": [
{
"match": "regex",
"regex": "[0-9]{10}"
}
],
"combine": "AND"
}
}
}
},
@@ -35,9 +61,27 @@
"rejectionReason": "Amount too high"
},
"matchingRules": {
"$.body.fraudCheckStatus": {
"match": "regex",
"regex": "FRAUD"
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.fraudCheckStatus": {
"matchers": [
{
"match": "regex",
"regex": "FRAUD"
}
],
"combine": "AND"
}
}
}
}
@@ -45,10 +89,10 @@
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
"version": "3.0.0"
},
"pact-jvm": {
"version": "2.4.18"
"version": "3.5.13"
}
}
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.spec.internal
import java.util.regex.Pattern

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -16,6 +16,8 @@
package org.springframework.cloud.contract.spec.internal
import groovy.util.logging.Slf4j
import java.util.regex.Pattern
import groovy.transform.CompileStatic
@@ -27,8 +29,11 @@ import repackaged.nl.flotsam.xeger.Xeger
* Represents an input for messaging. The input can be a message or some
* action inside the application.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@Slf4j
@TypeChecked
@EqualsAndHashCode
@ToString(includePackage = false, includeNames = true)
@@ -41,7 +46,7 @@ class Input extends Common {
Headers messageHeaders = new Headers()
BodyType messageBody
ExecutionProperty assertThat
BodyMatchers matchers
BodyMatchers bodyMatchers
Input() {}
@@ -107,9 +112,18 @@ class Input extends Common {
this.assertThat = new ExecutionProperty(assertThat)
}
/**
* @deprecated Deprecated in favor of bodyMatchers to support other future bodyMatchers too
*/
@Deprecated
void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.matchers = new BodyMatchers()
closure.delegate = this.matchers
log.warn("stubMatchers method is deprecated. Please use bodyMatchers instead")
bodyMatchers(closure)
}
void bodyMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.bodyMatchers = new BodyMatchers()
closure.delegate = this.bodyMatchers
closure()
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.spec.internal
import groovy.transform.CompileStatic
@@ -7,6 +22,7 @@ import groovy.transform.CompileStatic
* the body of the request or response.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.3
*/
@CompileStatic
@@ -42,10 +58,14 @@ enum MatchingType {
/**
* The user can provide custom command to execute
*/
COMMAND
COMMAND,
/**
* Verification if the value for the given path is null
*/
NULL
static boolean regexRelated(MatchingType type) {
if (type == EQUALITY || type == TYPE || type == COMMAND ) {
if (type == EQUALITY || type == TYPE || type == COMMAND || type == NULL ) {
return false
}
return true

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -20,11 +20,20 @@ import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.transform.TypeChecked
import groovy.util.logging.Slf4j
import org.apache.commons.lang3.StringEscapeUtils
import repackaged.nl.flotsam.xeger.Xeger
import java.util.regex.Pattern
/**
* Represents an output for messaging. Used for verifying
* the body and headers that are sent.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@Slf4j
@TypeChecked
@EqualsAndHashCode
@ToString(includePackage = false, includeNames = true)
@@ -36,7 +45,7 @@ class OutputMessage extends Common {
Headers headers
DslProperty body
ExecutionProperty assertThat
ResponseBodyMatchers matchers
ResponseBodyMatchers bodyMatchers
OutputMessage() {}
@@ -80,9 +89,18 @@ class OutputMessage extends Common {
return new DslProperty(value, server.serverValue)
}
/**
* @deprecated Deprecated in favor of bodyMatchers to support other future bodyMatchers too
*/
@Deprecated
void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.matchers = new ResponseBodyMatchers()
closure.delegate = this.matchers
log.warn("testMatchers method is deprecated. Please use bodyMatchers instead")
bodyMatchers(closure)
}
void bodyMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.bodyMatchers = new ResponseBodyMatchers()
closure.delegate = this.bodyMatchers
closure()
}

View File

@@ -1,11 +1,28 @@
/*
* Copyright 2013-2018 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.spec.internal
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.apache.commons.lang3.RandomStringUtils
import java.util.regex.Pattern
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
@PackageScope
@CompileStatic
@@ -40,10 +57,19 @@ abstract class PatternValueDslProperty<T extends DslProperty> {
RandomStringGenerator.randomString(20))
}
T anyAlphaNumeric() {
return createAndValidateProperty(RegexPatterns.ALPHA_NUMERIC,
RandomStringUtils.randomAlphanumeric(20))
}
T anyNumber() {
return createAndValidateProperty(RegexPatterns.NUMBER, this.random.nextInt())
}
T anyInteger() {
return createAndValidateProperty(RegexPatterns.INTEGER, this.random.nextInt())
}
T anyPositiveInt() {
return createAndValidateProperty(RegexPatterns.POSITIVE_INT, Math.abs(this.random.nextInt() + 1))
}
@@ -52,6 +78,11 @@ abstract class PatternValueDslProperty<T extends DslProperty> {
return createAndValidateProperty(RegexPatterns.DOUBLE, this.random.nextInt(100) + this.random.nextDouble())
}
T anyHex() {
return createAndValidateProperty(RegexPatterns.HEX,
RandomStringUtils.random(10, "0123456789abcdef"))
}
T aBoolean() {
return createAndValidateProperty(RegexPatterns.TRUE_OR_FALSE)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -25,6 +25,8 @@ import java.util.regex.Pattern
/**
* Contains most common regular expression patterns
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@CompileStatic
@@ -34,10 +36,13 @@ class RegexPatterns {
// tag::regexps[]
protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
protected static final Pattern ALPHA_NUMERIC = Pattern.compile('[a-zA-Z0-9]+')
protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
protected static final Pattern NUMBER = Pattern.compile('-?(\\d*\\.\\d+|\\d+)')
protected static final Pattern INTEGER = Pattern.compile('-?(\\d+)')
protected static final Pattern POSITIVE_INT = Pattern.compile('([1-9]\\d*)')
protected static final Pattern DOUBLE = Pattern.compile('-?(\\d*\\.\\d+)')
protected static final Pattern HEX = Pattern.compile('[a-fA-F0-9]+')
protected static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])')
protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?')
protected static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}')
@@ -59,6 +64,10 @@ class RegexPatterns {
return ONLY_ALPHA_UNICODE
}
Pattern alphaNumeric() {
return ALPHA_NUMERIC
}
Pattern number() {
return NUMBER
}
@@ -71,6 +80,10 @@ class RegexPatterns {
return TRUE_OR_FALSE
}
Pattern anInteger() {
return INTEGER
}
Pattern aDouble() {
return DOUBLE
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -20,6 +20,7 @@ import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.transform.TypeChecked
import groovy.util.logging.Slf4j
import org.apache.commons.lang3.StringEscapeUtils
import org.springframework.cloud.contract.spec.util.RegexpUtils
@@ -29,8 +30,11 @@ import java.util.regex.Pattern
/**
* Represents the request side of the HTTP communication
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@Slf4j
@TypeChecked
@EqualsAndHashCode
@ToString(includePackage = false, includeNames = true)
@@ -45,7 +49,7 @@ class Request extends Common {
Headers headers
Body body
Multipart multipart
BodyMatchers matchers
BodyMatchers bodyMatchers
Request() {
}
@@ -207,9 +211,18 @@ class Request extends Common {
return value(client)
}
/**
* @deprecated Deprecated in favor of bodyMatchers to support other future bodyMatchers too
*/
@Deprecated
void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.matchers = new BodyMatchers()
closure.delegate = this.matchers
log.warn("stubMatchers method is deprecated. Please use bodyMatchers instead")
bodyMatchers(closure)
}
void bodyMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.bodyMatchers = new BodyMatchers()
closure.delegate = this.bodyMatchers
closure()
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -16,6 +16,8 @@
package org.springframework.cloud.contract.spec.internal
import groovy.util.logging.Slf4j
import java.util.regex.Pattern
import groovy.transform.CompileStatic
@@ -28,8 +30,11 @@ import org.springframework.cloud.contract.spec.util.RegexpUtils
/**
* Represents the response side of the HTTP communication
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@Slf4j
@TypeChecked
@EqualsAndHashCode
@ToString(includePackage = false, includeFields = true)
@@ -43,7 +48,7 @@ class Response extends Common {
Headers headers
Body body
boolean async
ResponseBodyMatchers matchers
ResponseBodyMatchers bodyMatchers
Response() {
}
@@ -114,9 +119,18 @@ class Response extends Common {
return value(server)
}
/**
* @deprecated Deprecated in favor of bodyMatchers to support other future bodyMatchers too
*/
@Deprecated
void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.matchers = new ResponseBodyMatchers()
closure.delegate = this.matchers
log.warn("testMatchers method is deprecated. Please use bodyMatchers instead")
bodyMatchers(closure)
}
void bodyMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.bodyMatchers = new ResponseBodyMatchers()
closure.delegate = this.bodyMatchers
closure()
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.spec.internal
import groovy.transform.CompileStatic
@@ -8,6 +23,7 @@ import groovy.transform.ToString
* Body matchers for the response side (output message, REST response)
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.3
*/
@CompileStatic
@@ -28,6 +44,10 @@ class ResponseBodyMatchers extends BodyMatchers {
closure()
return matchingTypeValue.matchingTypeValue
}
MatchingTypeValue byNull() {
return new MatchingTypeValue(MatchingType.NULL, null)
}
}
@CompileStatic

View File

@@ -2,6 +2,9 @@ package org.springframework.cloud.contract.spec.internal
import org.springframework.cloud.contract.spec.Contract
import spock.lang.Specification
import static org.assertj.core.api.Assertions.assertThat
/**
* @author Marcin Grzejszczak
*/
@@ -291,4 +294,38 @@ then:
}
a == b
}
def 'should support deprecated testMatchers and stubMatchers'() {
given:
def contract = Contract.make {
request {
method 'GET'
url '/path'
body(
id: [value: '132']
)
stubMatchers {
jsonPath('$.id.value', byRegex(anInteger()))
}
}
response {
status OK()
body(
id: [value: '132'],
surname: 'Kowalsky',
name: 'Jan',
created: '2014-02-02 12:23:43'
)
headers {
contentType(applicationJson())
}
testMatchers {
jsonPath('$.created', byTimestamp())
}
}
}
expect:
assertThat(contract.request.bodyMatchers.hasMatchers()).isTrue()
assertThat(contract.response.bodyMatchers.hasMatchers()).isTrue()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -40,6 +40,7 @@ import com.toomuchcoding.jsonassert.JsonAssertion;
* Passes through a message that matches the one defined in the DSL
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerIntegrationMessageSelector implements MessageSelector {
@@ -56,7 +57,7 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector {
return false;
}
Object inputMessage = message.getPayload();
BodyMatchers matchers = this.groovyDsl.getInput().getMatchers();
BodyMatchers matchers = this.groovyDsl.getInput().getBodyMatchers();
Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody());
Object matchingInputMessage = JsonToJsonPathsConverter
.removeMatchingJsonPaths(dslBody, matchers);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -40,6 +40,7 @@ import com.toomuchcoding.jsonassert.JsonAssertion;
* Passes through a message that matches the one defined in the DSL
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerStreamMessageSelector implements MessageSelector {
@@ -56,7 +57,7 @@ class StubRunnerStreamMessageSelector implements MessageSelector {
return false;
}
Object inputMessage = message.getPayload();
BodyMatchers matchers = this.groovyDsl.getInput().getMatchers();
BodyMatchers matchers = this.groovyDsl.getInput().getBodyMatchers();
Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody());
Object matchingInputMessage = JsonToJsonPathsConverter
.removeMatchingJsonPaths(dslBody, matchers);

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.stubrunner.messaging.integration
import org.springframework.cloud.contract.spec.Contract
@@ -61,7 +76,7 @@ class StubRunnerIntegrationMessageSelectorSpec extends Specification {
header("foo", 123)
}
messageBody(foo: "non matching stuff")
stubMatchers {
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
@@ -111,7 +126,7 @@ class StubRunnerIntegrationMessageSelectorSpec extends Specification {
header("foo", 123)
}
messageBody(foo: 123)
stubMatchers {
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.stubrunner.messaging.stream
import org.springframework.cloud.contract.spec.Contract
@@ -62,7 +77,7 @@ class StubRunnerStreamMessageSelectorSpec extends Specification {
header("foo", 123)
}
messageBody(foo: "non matching stuff")
stubMatchers {
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
@@ -112,7 +127,7 @@ class StubRunnerStreamMessageSelectorSpec extends Specification {
header("foo", 123)
}
messageBody(foo: 123)
stubMatchers {
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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
* 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,
@@ -583,7 +583,7 @@ class DslToWireMockClientConverterSpec extends Specification {
]
]
])
stubMatchers {
bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
@@ -621,7 +621,7 @@ class DslToWireMockClientConverterSpec extends Specification {
1,2,3
],
])
testMatchers {
bodyMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
@@ -789,7 +789,7 @@ class DslToWireMockClientConverterSpec extends Specification {
email: 'abc@abc.com',
callback_url: 'http://partners.com'
)
stubMatchers {
bodyMatchers {
jsonPath('$.[\\'email\\']', byRegex(email()))
jsonPath('$.[\\'callback_url\\']', byRegex(hostname()))
}
@@ -803,7 +803,7 @@ class DslToWireMockClientConverterSpec extends Specification {
code: "123123",
message: "User not found by email == [not.existing@user.com]"
)
testMatchers {
bodyMatchers {
jsonPath('$.code', byRegex("123123"))
jsonPath('$.message', byRegex("User not found by email == ${email()}"))
}
@@ -875,7 +875,7 @@ class DslToWireMockClientConverterSpec extends Specification {
email: 'abc@abc.com',
callback_url: 'http://partners.com'
)
stubMatchers {
bodyMatchers {
jsonPath('$.[\\'email\\']', byRegex(email()))
jsonPath('$.[\\'callback_url\\']', byRegex(hostname()))
}

View File

@@ -52,7 +52,7 @@
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
<version>3.5.13</version>
</dependency>
</dependencies>
</plugin>

View File

@@ -35,8 +35,7 @@
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<optional>true</optional>
<artifactId>pact-jvm-consumer-java8_2.12</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.consumer.dsl.DslPart
import au.com.dius.pact.consumer.dsl.PactDslJsonArray
import au.com.dius.pact.consumer.dsl.PactDslJsonBody
import au.com.dius.pact.model.OptionalBody
import au.com.dius.pact.model.Request
import au.com.dius.pact.model.Response
import au.com.dius.pact.model.generators.Generator
import au.com.dius.pact.model.v3.messaging.Message
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.internal.EvaluationContext
import com.jayway.jsonpath.internal.Path
import com.jayway.jsonpath.internal.PathRef
import com.jayway.jsonpath.internal.path.PathCompiler
import groovy.json.JsonException
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.internal.Body
import org.springframework.cloud.contract.spec.internal.ClientDslProperty
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.ServerDslProperty
import org.springframework.cloud.contract.verifier.util.ContentUtils
import java.util.regex.Pattern
/**
* @author Tim Ysewyn
* @Since 2.0.0
*/
@CompileStatic
@PackageScope
class BodyConverter {
private static final JsonSlurper jsonSlurper = new JsonSlurper()
static DslPart toPactBody(Body body, Closure dslPropertyValueExtractor) {
return traverse(body, null, dslPropertyValueExtractor)
}
static DslPart toPactBody(DslProperty dslProperty, Closure dslPropertyValueExtractor) {
return traverse(dslProperty, null, dslPropertyValueExtractor)
}
private static DslPart traverse(Object value, DslPart parent, Closure dslPropertyValueExtractor) {
boolean isRoot = parent == null
Object v = value
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor(v)
}
if (v instanceof GString) {
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
}
if (v instanceof String) {
v = v.trim()
if (v.startsWith("{") && v.endsWith("}")) {
try {
v = jsonSlurper.parseText(v as String)
} catch (JsonException ex) { /*it wasn't a JSON string after all...*/
}
}
}
DslPart p = isRoot ? createRootDslPart(v) : parent
if (v instanceof Map) {
if (!isRoot) {
p = p.object()
}
processMap(v as Map, p as PactDslJsonBody, dslPropertyValueExtractor)
if (!isRoot) {
p = p.closeObject()
}
} else if (v instanceof Collection) {
if (!isRoot) {
p = p.array()
}
processCollection(v as Collection, p as PactDslJsonArray, dslPropertyValueExtractor)
if (!isRoot) {
p = p.closeArray()
}
}
return p
}
private static DslPart createRootDslPart(Object value) {
return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody()
}
private static void processCollection(Collection values, PactDslJsonArray jsonArray, Closure dslPropertyValueExtractor) {
values.forEach({
Object v = it
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor(v)
}
if (v instanceof GString) {
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
}
if (v == null) {
jsonArray.nullValue()
} else if (v instanceof String) {
jsonArray.string(v)
} else if (v instanceof Number) {
jsonArray.number(v)
} else {
traverse(it, jsonArray, dslPropertyValueExtractor)
}
})
}
private static void processMap(Map<String, Object> values, PactDslJsonBody jsonObject, Closure dslPropertyValueExtractor) {
values.forEach({ String k, Object v ->
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor(v)
}
if (v instanceof GString) {
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
}
if (v == null) {
jsonObject.nullValue(k)
} else if (v instanceof String) {
jsonObject.stringType(k, v)
} else if (v instanceof Number) {
jsonObject.numberValue(k, v)
} else {
PactDslJsonBody current = jsonObject.object(k)
traverse(v, current, dslPropertyValueExtractor)
current.closeObject()
}
})
}
static def toSCCBody(Request request) {
def body = parseBody(request.body)
if (request.generators.isNotEmpty() && request.generators.categories.containsKey(au.com.dius.pact.model.generators.Category.BODY)) {
applyGenerators(body, request.generators.categories.get(au.com.dius.pact.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
return new DslProperty<Object>(new ClientDslProperty(pattern, generatedValue), currentValue)
}
}
return body
}
static def toSCCBody(Response response) {
def body = parseBody(response.body)
if (response.generators.isNotEmpty() && response.generators.categories.containsKey(au.com.dius.pact.model.generators.Category.BODY)) {
applyGenerators(body, response.generators.categories.get(au.com.dius.pact.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
return new DslProperty<Object>(currentValue, new ServerDslProperty(pattern, generatedValue))
}
}
return body
}
static def toSCCBody(Message message) {
def body = parseBody(message.contents)
if (message.generators.isNotEmpty() && message.generators.categories.containsKey(au.com.dius.pact.model.generators.Category.BODY)) {
applyGenerators(body, message.generators.categories.get(au.com.dius.pact.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
return new DslProperty<Object>(new ClientDslProperty(pattern, generatedValue), currentValue)
}
}
return body
}
private static def parseBody(OptionalBody optionalBody) {
if (optionalBody.present) {
return new JsonSlurper().parseText(optionalBody.value)
} else {
return optionalBody.value
}
}
private static void applyGenerators(def body, Map<String, Generator> generatorsPerPath, Closure<DslProperty> dslPropertyProvider) {
Configuration configuration = Configuration.defaultConfiguration()
generatorsPerPath.each { String path, Generator generator ->
Path compiledPath = PathCompiler.compile(path)
EvaluationContext evaluationContext = compiledPath.evaluate(body, body, configuration, true)
evaluationContext.updateOperations().each { PathRef pathRef ->
pathRef.convert({ Object currentValue, Configuration config ->
return ValueGeneratorConverter.convert(generator) { Pattern pattern, Object generatedValue ->
return dslPropertyProvider(currentValue, pattern, generatedValue)
}
}, configuration)
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.EqualsMatcher
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.model.matchingrules.TypeMatcher
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
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.RegexPatterns
/**
* @author Tim Ysewyn
* @Since 2.0.0
*/
@CompileStatic
@PackageScope
class MatchingRulesConverter {
private static final RegexPatterns regexPatterns = new RegexPatterns()
static Category matchingRulesForBody(BodyMatchers bodyMatchers) {
return matchingRulesFor("body", bodyMatchers)
}
private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) {
Category category = new Category(categoryName)
bodyMatchers.jsonPathMatchers().forEach({ BodyMatcher it ->
String key = getMatcherKey(it.path())
MatchingType matchingType = it.matchingType()
switch (matchingType) {
case MatchingType.NULL:
category.addRule(key, NullMatcher.INSTANCE)
break
case MatchingType.EQUALITY:
category.addRule(key, EqualsMatcher.INSTANCE)
break
case MatchingType.TYPE:
if (it.minTypeOccurrence() && it.maxTypeOccurrence()) {
category.addRule(key, new MinMaxTypeMatcher(it.minTypeOccurrence(), it.maxTypeOccurrence()))
} else if (it.minTypeOccurrence()) {
category.addRule(key, new MinTypeMatcher(it.minTypeOccurrence()))
} else if (it.maxTypeOccurrence()) {
category.addRule(key, new MaxTypeMatcher(it.maxTypeOccurrence()))
} else {
category.addRule(key, TypeMatcher.INSTANCE)
}
break
case MatchingType.DATE:
category.addRule(key, new DateMatcher())
break
case MatchingType.TIME:
category.addRule(key, new TimeMatcher())
break
case MatchingType.TIMESTAMP:
category.addRule(key, new TimestampMatcher())
break
case MatchingType.REGEX:
String pattern = it.value().toString()
if (pattern.equals(regexPatterns.number().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER))
} else if (pattern.equals(regexPatterns.anInteger().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))
} else if (pattern.equals(regexPatterns.aDouble().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL))
} else {
category.addRule(key, new RegexMatcher(pattern))
}
break
default:
break
}
})
return category
}
private static String getMatcherKey(String path) {
return "${path.startsWith('$') ? path.substring(1) : path}"
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.consumer.MessagePactBuilder
import au.com.dius.pact.consumer.dsl.DslPart
import au.com.dius.pact.model.v3.messaging.MessagePact
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.Header
import org.springframework.cloud.contract.spec.internal.Headers
import org.springframework.cloud.contract.spec.internal.Input
import org.springframework.cloud.contract.spec.internal.OutputMessage
import org.springframework.cloud.contract.verifier.util.ContentUtils
/**
* Creator of {@link MessagePact} instances
*
* @author Tim Ysewyn
* @since 2.0.0
*/
@CompileStatic
@PackageScope
class MessagePactCreator {
private static final Closure clientValueExtractor = { DslProperty property -> property.clientValue }
MessagePact createFromContract(Contract contract) {
MessagePactBuilder messagePactBuilder = MessagePactBuilder.consumer("Consumer")
.hasPactWith("Provider")
.given(getGiven(contract.input))
.expectsToReceive(getOutcome(contract))
if (contract.outputMessage) {
OutputMessage message = contract.outputMessage
if (message.body) {
DslPart pactResponseBody = BodyConverter.toPactBody(message.body, clientValueExtractor)
if (message.bodyMatchers) {
pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(message.bodyMatchers))
}
pactResponseBody.setGenerators(ValueGeneratorConverter.extract(message, { DslProperty dslProperty -> dslProperty.serverValue }))
messagePactBuilder = messagePactBuilder.withContent(pactResponseBody)
}
if (message.headers) {
messagePactBuilder = messagePactBuilder.withMetadata(getMetadata(message.headers))
}
}
return messagePactBuilder.toPact()
}
private String getGiven(Input input) {
if (input.triggeredBy) {
return input.triggeredBy.executionCommand
} else if (input.messageFrom) {
return "received message from " + clientValueExtractor.call(input.messageFrom)
} else {
return ""
}
}
private String getOutcome(Contract contract) {
if (contract.outputMessage) {
OutputMessage message = contract.outputMessage
return "message sent to " + clientValueExtractor.call(message.sentTo)
} else {
return "assert that " + contract.input.assertThat.executionCommand
}
}
private Map<String, String> getMetadata(Headers headers) {
return headers.entries.collectEntries({ Header header ->
return ["$header.name": extractValue(header)]
})
}
private String extractValue(Object value) {
Object v = value
if (v instanceof DslProperty) {
v = clientValueExtractor.call(v)
}
if (v instanceof GString) {
v = ContentUtils.extractValue(v, clientValueExtractor)
}
if (v instanceof String) {
return v
} else {
return v.toString()
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.MatchingRule
import au.com.dius.pact.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.RuleLogic
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.model.matchingrules.TypeMatcher
import au.com.dius.pact.model.v3.messaging.Message
import au.com.dius.pact.model.v3.messaging.MessagePact
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
/**
* Creator of {@link Contract} instances
*
* @author Tim Ysewyn
* @since 2.0.0
*/
@CompileStatic
@PackageScope
class MessagingSCContractCreator {
private static final String FULL_BODY = '$'
Collection<Contract> convertFrom(MessagePact pact) {
return pact.messages.collect({ Message message ->
Contract.make {
label("$message.description")
if (!message.providerStates.isEmpty()) {
input {
triggeredBy(getTriggeredBy(message))
}
}
outputMessage {
if (message.contents.present) {
body(BodyConverter.toSCCBody(message))
Category bodyRules = message.matchingRules.rulesForCategory('body')
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
bodyMatchers {
bodyRules.matchingRules.each { String key, MatchingRuleGroup ruleGroup ->
if (ruleGroup.ruleLogic != RuleLogic.AND) {
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
}
if (FULL_BODY.equals(key)) {
JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.contents.value)
jsonPaths.each {
jsonPath(it.keyBeforeChecking(), byType())
}
} else {
ruleGroup.rules.each { MatchingRule rule ->
if (rule instanceof NullMatcher) {
jsonPath(key, byNull())
} else if (rule instanceof RegexMatcher) {
jsonPath(key, byRegex(rule.regex))
} else if (rule instanceof DateMatcher) {
jsonPath(key, byDate())
} else if (rule instanceof TimeMatcher) {
jsonPath(key, byTime())
} else if (rule instanceof TimestampMatcher) {
jsonPath(key, byTimestamp())
} else if (rule instanceof MinTypeMatcher) {
jsonPath(key, byType() {
minOccurrence((rule as MinTypeMatcher).min)
})
} else if (rule instanceof MinMaxTypeMatcher) {
jsonPath(key, byType() {
minOccurrence((rule as MinMaxTypeMatcher).min)
maxOccurrence((rule as MinMaxTypeMatcher).max)
})
} else if (rule instanceof MaxTypeMatcher) {
jsonPath(key, byType() {
maxOccurrence((rule as MaxTypeMatcher).max)
})
} else if (rule instanceof TypeMatcher) {
jsonPath(key, byType())
} else if (rule instanceof NumberTypeMatcher) {
switch (rule.numberType) {
case NumberTypeMatcher.NumberType.NUMBER:
jsonPath(key, byRegex(number()))
break
case NumberTypeMatcher.NumberType.INTEGER:
jsonPath(key, byRegex(anInteger()))
break
case NumberTypeMatcher.NumberType.DECIMAL:
jsonPath(key, byRegex(aDouble()))
break
default:
throw new RuntimeException("Unsupported number type!")
}
}
}
}
}
}
}
}
if (!message.metaData.isEmpty()) {
headers {
message.metaData.each { String k, String v ->
if (k.equalsIgnoreCase("contentType")) {
messagingContentType(v)
} else {
header(k, v)
}
}
}
}
}
}
})
}
private String getTriggeredBy(Message message) {
return message.providerStates.first().name
.replace(':', ' ')
.replace(' ', '_')
.replace('(', '')
.replace(')', '')
.uncapitalize() + "()"
}
}

View File

@@ -1,43 +1,43 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.BasePact
import au.com.dius.pact.model.Consumer
import au.com.dius.pact.model.Interaction
import au.com.dius.pact.model.OptionalBody
import au.com.dius.pact.model.Pact
import au.com.dius.pact.model.PactReader
import au.com.dius.pact.model.Provider
import au.com.dius.pact.model.Request
import au.com.dius.pact.model.RequestResponseInteraction
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.Response
import groovy.json.JsonOutput
import au.com.dius.pact.model.v3.messaging.MessagePact
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import org.springframework.cloud.contract.spec.internal.BodyMatchers
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.Headers
import org.springframework.cloud.contract.spec.internal.MatchingType
import org.springframework.cloud.contract.spec.internal.QueryParameters
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
import org.springframework.cloud.contract.verifier.util.MapConverter
/**
* Converter of JSON PACT file
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.1.0
*/
@CompileStatic
class PactContractConverter implements ContractConverter<Pact> {
class PactContractConverter implements ContractConverter<Collection<Pact>> {
private static final String MATCH_KEY = "match"
private static final String REGEX_KEY = "regex"
private static final String MAX_KEY = "max"
private static final String MIN_KEY = "min"
private static final String FULL_BODY = '$.body'
private RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator()
private MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator()
private RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator()
private MessagePactCreator messagePactCreator = new MessagePactCreator()
@Override
boolean isAccepted(File file) {
@@ -52,284 +52,26 @@ class PactContractConverter implements ContractConverter<Pact> {
@Override
Collection<Contract> convertFrom(File file) {
Pact pact = PactReader.loadPact(file)
List<Interaction> interactions = pact.interactions
return interactions.collect { Interaction interaction ->
Contract.make {
if (interaction instanceof RequestResponseInteraction) {
RequestResponseInteraction requestResponseInteraction = (RequestResponseInteraction) interaction
description("$requestResponseInteraction.description${providerState(interaction)}")
request {
method(requestResponseInteraction.request.method)
if (requestResponseInteraction.request.query) {
url(requestResponseInteraction.request.path) {
queryParameters {
requestResponseInteraction.request.query.each { String key, List<String> value ->
value.each { String singleValue ->
parameter(key, singleValue)
}
}
}
}
} else {
url(requestResponseInteraction.request.path)
}
if (requestResponseInteraction.request.headers) {
headers {
requestResponseInteraction.request.headers.each { String key, String value ->
header(key, value)
}
}
}
if (requestResponseInteraction.request.body.state == OptionalBody.State.PRESENT) {
def parsedBody = BasePact.parseBody(requestResponseInteraction.request)
if (parsedBody instanceof Map) {
body(parsedBody as Map)
} else if (parsedBody instanceof List) {
body(parsedBody as List)
} else {
body(parsedBody.toString())
}
}
if (requestResponseInteraction.request?.matchingRules) {
stubMatchers {
Map<String, Map<String, Object>> rules = requestResponseInteraction.request.matchingRules
rules.each { String key, Map<String, Object> value ->
String keyFromBody = toKeyStartingFromBody(key)
if (value.containsKey(MATCH_KEY)) {
MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase())
switch (matchingType) {
case MatchingType.EQUALITY:
// equality is checked by default in the standard way
break
case MatchingType.DATE:
jsonPath(keyFromBody, byDate())
break
case MatchingType.TIME:
jsonPath(keyFromBody, byTime())
break
case MatchingType.TIMESTAMP:
jsonPath(keyFromBody, byTimestamp())
break
case MatchingType.REGEX:
jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String))
break
}
} else if (value.containsKey(REGEX_KEY)) {
jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String))
}
}
}
}
}
response {
status(requestResponseInteraction.response.status)
if (requestResponseInteraction.response.body.state == OptionalBody.State.PRESENT) {
def parsedBody = BasePact.parseBody(requestResponseInteraction.response)
if (parsedBody instanceof Map) {
body(parsedBody as Map)
} else if (parsedBody instanceof List) {
body(parsedBody as List)
} else {
body(parsedBody.toString())
}
}
if (requestResponseInteraction.response?.matchingRules) {
testMatchers {
Map<String, Map<String, Object>> rules = requestResponseInteraction.response.matchingRules
Map<String, Object> fullBodyCheck = rules.get(FULL_BODY)
if (fullBodyCheck != null) {
JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(requestResponseInteraction.response?.body?.value)
jsonPaths.each {
jsonPath(it.keyBeforeChecking(), byType())
}
}
rules.each { String key, Map<String, Object> value ->
String keyFromBody = toKeyStartingFromBody(key)
if (!keyFromBody) {
return
}
if (value.containsKey(MATCH_KEY)) {
MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase())
switch (matchingType) {
case MatchingType.EQUALITY:
// equality is checked by default in the standard way
break
case MatchingType.DATE:
jsonPath(keyFromBody, byDate())
break
case MatchingType.TIME:
jsonPath(keyFromBody, byTime())
break
case MatchingType.TIMESTAMP:
jsonPath(keyFromBody, byTimestamp())
break
case MatchingType.REGEX:
jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String))
break
case MatchingType.TYPE:
jsonPath(keyFromBody, byType() {
if (value.containsKey(MIN_KEY)) {
minOccurrence(value.get(MIN_KEY) as Integer)
}
if (value.containsKey(MAX_KEY)) {
maxOccurrence(value.get(MAX_KEY) as Integer)
}
})
break
}
} else if (value.containsKey(REGEX_KEY)) {
jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String))
}
}
}
}
requestResponseInteraction.response.headers?.each { String key, String value ->
headers {
header(key, value)
}
}
}
}
}
if (pact instanceof RequestResponsePact) {
return requestResponseSCContractCreator.convertFrom(pact as RequestResponsePact)
}
}
protected String providerState(Interaction interaction) {
return interaction.providerState ? " ${interaction.providerState}" : ""
}
protected String toKeyStartingFromBody(String key) {
if (key == FULL_BODY) {
return ""
if (pact instanceof MessagePact) {
return messagingSCContractCreator.convertFrom(pact as MessagePact)
}
return key.replace(FULL_BODY, '$')
throw new UnsupportedOperationException("We currently don't support pact contracts of type" + pact.class.simpleName)
}
@Override
Pact convertTo(Collection<Contract> contract) {
Provider provider = new Provider()
provider.name = "Provider"
Consumer consumer = new Consumer()
consumer.name = "Consumer"
List<RequestResponseInteraction> interactions = contract.find { it.request }.collect { Contract dsl ->
RequestResponseInteraction interaction = new RequestResponseInteraction()
interaction.description = dsl.description ?: ""
Request request = new Request().with {
method = dsl.request.method.serverValue.toString()
path = url(dsl)
QueryParameters params = queryParams(dsl)
if (params) {
query = params.parameters.collectEntries {
String name = it.name
String value = it.serverValue
return [(name) : [value]]
}
}
if (dsl.request.headers) {
headers = headers(dsl.request.headers, { DslProperty property -> property.serverValue })
}
if (dsl.request.body) {
assertInputContract(dsl.request.body.serverValue)
def json = MapConverter.getTestSideValues(dsl.request.body.serverValue)
String jsonBody = JsonOutput.toJson(json)
body = new OptionalBody(OptionalBody.State.PRESENT, jsonBody)
}
if (dsl.request.matchers && dsl.request.matchers.hasMatchers()) {
matchingRules = matchingRules(dsl.request.matchers)
}
return it
Collection<Pact> convertTo(Collection<Contract> contracts) {
List<Pact> pactContracts = new ArrayList<>()
contracts.collect({ Contract contract ->
if (contract.request) {
pactContracts.add(requestResponsePactCreator.createFromContract(contract))
}
Response response = new Response().with {
status = dsl.response.status.clientValue as Integer
if (dsl.response.headers) {
headers = headers(dsl.response.headers, { DslProperty property -> property.clientValue })
}
if (dsl.response.body) {
assertInputContract(dsl.response.body.clientValue)
def json = MapConverter.getStubSideValues(dsl.response.body.clientValue)
String jsonBody = JsonOutput.toJson(json)
body = new OptionalBody(OptionalBody.State.PRESENT, jsonBody)
}
if (dsl.response.matchers && dsl.response.matchers.hasMatchers()) {
matchingRules = matchingRules(dsl.response.matchers)
}
return it
if (contract.input) {
pactContracts.add(messagePactCreator.createFromContract(contract))
}
interaction.request = request
interaction.response = response
return interaction
}
return new RequestResponsePact(provider, consumer, interactions)
}
protected void assertInputContract(parsedJson) {
boolean hasExecutionProp = false
MapConverter.transformValues(parsedJson, {
if (it instanceof ExecutionProperty) {
hasExecutionProp = true
}
return it
})
if (hasExecutionProp) {
throw new UnsupportedOperationException("We can't convert a contract that has execution property")
}
}
protected Map<String, String> headers(Headers headers, Closure closure) {
return headers.entries.collectEntries {
String name = it.name
String value = closure(it)
return [(name) : value]
}
}
protected Map<String, Map<String, Object>> matchingRules(BodyMatchers bodyMatchers) {
return bodyMatchers.jsonPathMatchers().collectEntries {
MatchingType matchingType = it.matchingType()
String key = "\$.body${it.path().startsWith('$') ? it.path().substring(1) : it.path()}"
Object value = it.value()
Integer minTypeOccurrence = it.minTypeOccurrence()
Integer maxTypeOccurrence = it.maxTypeOccurrence()
Map<String, Object> matchingRule = [:]
switch (matchingType) {
case MatchingType.EQUALITY:
matchingRule << [(MATCH_KEY) : MatchingType.EQUALITY.toString().toLowerCase() as Object]
break
case MatchingType.TYPE:
Map<String, Object> map = [(MATCH_KEY) : MatchingType.TYPE.toString().toLowerCase() as Object]
if (minTypeOccurrence) map.put(MIN_KEY, minTypeOccurrence)
if (maxTypeOccurrence) map.put(MAX_KEY, maxTypeOccurrence)
matchingRule << map
break
case MatchingType.DATE:
case MatchingType.TIME:
case MatchingType.TIMESTAMP:
case MatchingType.REGEX:
matchingRule << [
(MATCH_KEY) : MatchingType.REGEX.toString().toLowerCase() as Object,
(REGEX_KEY) : value
]
break
}
return [(key) : matchingRule]
}
}
protected String url(Contract dsl) {
if (dsl.request.urlPath) {
return dsl.request.urlPath.serverValue.toString()
} else if (dsl.request.url) {
return dsl.request.url.serverValue.toString()
}
throw new IllegalStateException("No url provided")
}
protected QueryParameters queryParams(Contract dsl) {
if (dsl.request.urlPath) {
return dsl.request.urlPath.queryParameters
} else if (dsl.request.url) {
return dsl.request.url.queryParameters
}
throw new IllegalStateException("No url provided")
return pactContracts
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.consumer.ConsumerPactBuilder
import au.com.dius.pact.consumer.dsl.DslPart
import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath
import au.com.dius.pact.consumer.dsl.PactDslResponse
import au.com.dius.pact.consumer.dsl.PactDslWithProvider
import au.com.dius.pact.model.RequestResponsePact
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.Body
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.Header
import org.springframework.cloud.contract.spec.internal.QueryParameters
import org.springframework.cloud.contract.spec.internal.Request
import org.springframework.cloud.contract.spec.internal.Response
/**
* Creator of {@link RequestResponsePact} instances
*
* @author Tim Ysewyn
* @since 2.0.0
*/
@CompileStatic
@PackageScope
class RequestResponsePactCreator {
RequestResponsePact createFromContract(Contract contract) {
assertNoExecutionProperty(contract)
PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer("Consumer")
.hasPactWith("Provider")
PactDslRequestWithPath pactDslRequest = createPactDslRequestWithPath(contract, pactDslWithProvider)
PactDslResponse pactDslResponse = createPactDslResponse(contract, pactDslRequest)
return pactDslResponse.toPact()
}
private void assertNoExecutionProperty(Contract contract) {
assertNoExecutionPropertyInBody(contract.request.body, { DslProperty dslProperty -> dslProperty.serverValue })
assertNoExecutionPropertyInBody(contract.response.body, { DslProperty dslProperty -> dslProperty.clientValue })
}
private void assertNoExecutionPropertyInBody(Body body, Closure dslPropertyValueExtractor) {
traverseValues(body, dslPropertyValueExtractor, {
if (it instanceof ExecutionProperty) {
throw new UnsupportedOperationException("We can't convert a contract that has execution property")
}
})
}
private void traverseValues(def value, Closure dslPropertyValueExtractor, Closure closure) {
if (value instanceof DslProperty) {
traverseValues(dslPropertyValueExtractor(value), dslPropertyValueExtractor, closure)
} else if (value instanceof Map) {
value.values().forEach({traverseValues(it, dslPropertyValueExtractor, closure)})
} else if (value instanceof Collection) {
value.forEach({traverseValues(it, dslPropertyValueExtractor, closure)})
} else {
closure(value)
}
}
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslWithProvider pactDslWithProvider) {
Request request = contract.request
PactDslRequestWithPath pactDslRequest = pactDslWithProvider
.uponReceiving(contract.description ?: "")
.path(url(request))
.method(request.method.serverValue.toString())
String query = query(request)
if (query) {
pactDslRequest = pactDslRequest.encodedQuery(query)
}
if (request.headers) {
request.headers.entries.each { Header header ->
pactDslRequest = processHeader(pactDslRequest, header)
}
}
if (request.body) {
DslPart pactRequestBody = BodyConverter.toPactBody(request.body, { DslProperty property -> property.serverValue })
if (request.bodyMatchers) {
pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.bodyMatchers))
}
pactRequestBody.setGenerators(ValueGeneratorConverter.extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue }))
pactDslRequest = pactDslRequest.body(pactRequestBody)
}
return pactDslRequest
}
private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) {
Response response = contract.response
PactDslResponse pactDslResponse = pactDslRequest.willRespondWith()
.status(response.status.clientValue as Integer)
if (response.headers) {
response.headers.entries.each { Header header ->
pactDslResponse = processHeader(pactDslResponse, header)
}
}
if (response.body) {
DslPart pactResponseBody = BodyConverter.toPactBody(response.body, { DslProperty property -> property.clientValue })
if (response.bodyMatchers) {
pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(response.bodyMatchers))
}
pactResponseBody.setGenerators(ValueGeneratorConverter.extract(response.body, { DslProperty dslProperty -> dslProperty.serverValue }))
pactDslResponse = pactDslResponse.body(pactResponseBody)
}
return pactDslResponse
}
private String url(Request request) {
if (request.urlPath) {
return request.urlPath.serverValue.toString()
} else if (request.url) {
return request.url.serverValue.toString()
}
throw new IllegalStateException("No url provided")
}
private String query(Request request) {
String query = null
QueryParameters params = queryParams(request)
if (params) {
query = ""
params.parameters.eachWithIndex { param, index ->
query += param.name + '=' + param.serverValue
if (index + 1 < params.parameters.size()) {
query += '&'
}
}
}
return query
}
private QueryParameters queryParams(Request request) {
if (request.urlPath) {
return request.urlPath.queryParameters
} else if (request.url) {
return request.url.queryParameters
}
throw new IllegalStateException("No url provided")
}
private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) {
if (header.isSingleValue()) {
String value = getDslPropertyServerValue(header).toString()
return pactDslRequest.headers(header.name, value)
} else {
String regex = getDslPropertyClientValue(header).toString()
String example = getDslPropertyServerValue(header).toString()
return pactDslRequest.matchHeader(header.name, regex, example)
}
}
private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) {
if (header.isSingleValue()) {
String value = getDslPropertyClientValue(header).toString()
return pactDslResponse.headers([(header.name) : value])
} else {
String regex = getDslPropertyServerValue(header).toString()
String example = getDslPropertyClientValue(header).toString()
return pactDslResponse.matchHeader(header.name, regex, example)
}
}
private Object getDslPropertyClientValue(Object o) {
Object value = o
if (value instanceof DslProperty) {
value = getDslPropertyClientValue(value.getClientValue())
}
return value
}
private Object getDslPropertyServerValue(Object o) {
Object value = o
if (value instanceof DslProperty) {
value = getDslPropertyServerValue(value.getServerValue())
}
return value
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.OptionalBody
import au.com.dius.pact.model.ProviderState
import au.com.dius.pact.model.Request
import au.com.dius.pact.model.RequestResponseInteraction
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.Response
import au.com.dius.pact.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.MatchingRule
import au.com.dius.pact.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.RuleLogic
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.model.matchingrules.TypeMatcher
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.NotToEscapePattern
import org.springframework.cloud.contract.spec.internal.RegexPatterns
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
import java.util.regex.Pattern
/**
* Creator of {@link Contract} instances
*
* @author Tim Ysewyn
* @since 2.0.0
*/
@CompileStatic
@PackageScope
class RequestResponseSCContractCreator {
private static final String FULL_BODY = '$'
private static final RegexPatterns regexPatterns = new RegexPatterns()
Collection<Contract> convertFrom(RequestResponsePact pact) {
return pact.interactions.collect { RequestResponseInteraction interaction ->
Contract.make {
description(buildDescription(interaction))
request {
Request request = interaction.request
method(request.method)
if (request.query) {
url(request.path) {
queryParameters {
request.query.each { String key, List<String> value ->
value.each { String singleValue ->
parameter(key, singleValue)
}
}
}
}
} else {
url(request.path)
}
if (request.headers) {
Category headerRules = request.matchingRules.rulesForCategory('header')
headers {
request.headers.each { k, v ->
if (headerRules.matchingRules.containsKey(k)) {
MatchingRuleGroup ruleGroup = headerRules.matchingRules.get(k)
if (ruleGroup.rules.size() > 1) {
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
}
MatchingRule rule = ruleGroup.rules[0]
if (rule instanceof RegexMatcher) {
header(k, new DslProperty((Object)Pattern.compile(rule.getRegex()), (Object)v))
} else {
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
}
} else {
header(k, v)
}
}
}
}
if (request.body.state == OptionalBody.State.PRESENT) {
def parsedBody = BodyConverter.toSCCBody(request)
if (parsedBody instanceof Map) {
body(parsedBody as Map)
} else if (parsedBody instanceof List) {
body(parsedBody as List)
} else {
body(parsedBody.toString())
}
}
Category bodyRules = request.matchingRules.rulesForCategory('body')
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
bodyMatchers {
bodyRules.matchingRules.each { String key, MatchingRuleGroup ruleGroup ->
if (ruleGroup.ruleLogic != RuleLogic.AND) {
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
}
ruleGroup.rules.each { MatchingRule rule ->
if (rule instanceof RegexMatcher) {
jsonPath(key, byRegex(rule.regex))
} else if (rule instanceof DateMatcher) {
jsonPath(key, byDate())
} else if (rule instanceof TimeMatcher) {
jsonPath(key, byTime())
} else if (rule instanceof TimestampMatcher) {
jsonPath(key, byTimestamp())
} else if (rule instanceof NumberTypeMatcher) {
switch (rule.numberType) {
case NumberTypeMatcher.NumberType.NUMBER:
jsonPath(key, byRegex(regexPatterns.number()))
break
case NumberTypeMatcher.NumberType.INTEGER:
jsonPath(key, byRegex(regexPatterns.anInteger()))
break
case NumberTypeMatcher.NumberType.DECIMAL:
jsonPath(key, byRegex(regexPatterns.aDouble()))
break
default:
throw new RuntimeException("Unsupported number type!")
}
}
}
}
}
}
}
response {
Response response = interaction.response
status(response.status)
if (response.body.present) {
def parsedBody = BodyConverter.toSCCBody(response)
if (parsedBody instanceof Map) {
body(parsedBody as Map)
} else if (parsedBody instanceof List) {
body(parsedBody as List)
} else {
body(parsedBody.toString())
}
}
Category bodyRules = response.matchingRules.rulesForCategory('body')
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
bodyMatchers {
bodyRules.matchingRules.each { String key, MatchingRuleGroup ruleGroup ->
if (ruleGroup.ruleLogic != RuleLogic.AND) {
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
}
if (FULL_BODY.equals(key)) {
JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(response.body.value)
jsonPaths.each {
jsonPath(it.keyBeforeChecking(), byType())
}
} else {
ruleGroup.rules.each { MatchingRule rule ->
if (rule instanceof NullMatcher) {
jsonPath(key, byNull())
} else if (rule instanceof RegexMatcher) {
jsonPath(key, byRegex(rule.regex))
} else if (rule instanceof DateMatcher) {
jsonPath(key, byDate())
} else if (rule instanceof TimeMatcher) {
jsonPath(key, byTime())
} else if (rule instanceof TimestampMatcher) {
jsonPath(key, byTimestamp())
} else if (rule instanceof MinTypeMatcher) {
jsonPath(key, byType() {
minOccurrence((rule as MinTypeMatcher).min)
})
} else if (rule instanceof MinMaxTypeMatcher) {
jsonPath(key, byType() {
minOccurrence((rule as MinMaxTypeMatcher).min)
maxOccurrence((rule as MinMaxTypeMatcher).max)
})
} else if (rule instanceof MaxTypeMatcher) {
jsonPath(key, byType() {
maxOccurrence((rule as MaxTypeMatcher).max)
})
} else if (rule instanceof TypeMatcher) {
jsonPath(key, byType())
} else if (rule instanceof NumberTypeMatcher) {
switch (rule.numberType) {
case NumberTypeMatcher.NumberType.NUMBER:
jsonPath(key, byRegex(regexPatterns.number()))
break
case NumberTypeMatcher.NumberType.INTEGER:
jsonPath(key, byRegex(regexPatterns.anInteger()))
break
case NumberTypeMatcher.NumberType.DECIMAL:
jsonPath(key, byRegex(regexPatterns.aDouble()))
break
default:
throw new UnsupportedOperationException("Unsupported number type!")
}
}
}
}
}
}
}
if (response.headers) {
Category headerRules = response.matchingRules.rulesForCategory('header')
headers {
response.headers.forEach({ String k, String v ->
if (headerRules.matchingRules.containsKey(k)) {
MatchingRuleGroup ruleGroup = headerRules.matchingRules.get(k)
if (ruleGroup.rules.size() > 1) {
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
}
MatchingRule rule = ruleGroup.rules[0]
if (rule instanceof RegexMatcher) {
header(k, new DslProperty(new DslProperty(v), new NotToEscapePattern(Pattern.compile(rule.getRegex()))))
} else {
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
}
} else {
header(k, v)
}
})
}
}
}
}
}
}
private String buildDescription(RequestResponseInteraction interaction) {
String description = "$interaction.description"
interaction.providerStates.forEach({ ProviderState it ->
description += " $it.name"
if (!it.params.isEmpty()) {
Map<String, Object> params = it.params
description += "("
params.forEach({ String k, Object v ->
description += k + ": " + v.toString()
if (params.keySet().last() != k) {
description += ", "
}
})
description += ")"
}
})
return description
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.generators.Category
import au.com.dius.pact.model.generators.DateGenerator
import au.com.dius.pact.model.generators.DateTimeGenerator
import au.com.dius.pact.model.generators.Generator
import au.com.dius.pact.model.generators.Generators
import au.com.dius.pact.model.generators.RandomBooleanGenerator
import au.com.dius.pact.model.generators.RandomDecimalGenerator
import au.com.dius.pact.model.generators.RandomHexadecimalGenerator
import au.com.dius.pact.model.generators.RandomIntGenerator
import au.com.dius.pact.model.generators.RandomStringGenerator
import au.com.dius.pact.model.generators.RegexGenerator
import au.com.dius.pact.model.generators.TimeGenerator
import au.com.dius.pact.model.generators.UuidGenerator
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.internal.Body
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.OutputMessage
import org.springframework.cloud.contract.verifier.util.ContentUtils
import java.util.regex.Pattern
/**
* @author Tim Ysewyn
* @Since 2.0.0
*/
@CompileStatic
@PackageScope
class ValueGeneratorConverter {
private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN)
private static final String INTEGER_PATTERN = '-?(\\d+)'
private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN)
private static final String DECIMAL_PATTERN = '-?(\\d*\\.\\d+)'
private static final Pattern HEX = Pattern.compile(HEX_PATTERN)
private static final String HEX_PATTERN = '[a-fA-F0-9]+'
private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN)
private static final String ALPHA_NUMERIC_PATTERN = '[a-zA-Z0-9]+'
private static final Pattern UUID = Pattern.compile(UUID_PATTERN)
private static final String UUID_PATTERN = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN)
private static final String ANY_DATE_PATTERN = '(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])'
private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN)
private static final String ANY_TIME_PATTERN = '(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'
private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN)
private static final String ANY_DATE_TIME_PATTERN = '([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])'
private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN)
private static final String TRUE_OR_FALSE_PATTERN = /(true|false)/
static DslProperty convert(Generator generator, Closure<DslProperty> dslPropertyProvider) {
Pattern pattern
if (generator instanceof RandomIntGenerator) {
pattern = INTEGER
} else if (generator instanceof RandomDecimalGenerator) {
pattern = DECIMAL
} else if (generator instanceof RandomHexadecimalGenerator) {
pattern = HEX
} else if (generator instanceof RandomStringGenerator) {
pattern = ALPHA_NUMERIC
} else if (generator instanceof RegexGenerator) {
pattern = Pattern.compile(generator.regex)
} else if (generator instanceof UuidGenerator) {
pattern = UUID
} else if (generator instanceof DateGenerator) {
pattern = getDateTimePattern(generator.format, ANY_DATE)
} else if (generator instanceof TimeGenerator) {
pattern = getDateTimePattern(generator.format, ANY_TIME)
} else if (generator instanceof DateTimeGenerator) {
pattern = getDateTimePattern(generator.format, ANY_DATE_TIME)
} else if (generator instanceof RandomBooleanGenerator) {
pattern = TRUE_OR_FALSE
}
if (pattern == null) {
throw new UnsupportedOperationException("We currently don't support a generator of type " + generator.class.simpleName)
} else {
Object generatedValue = generator.generate(null)
return dslPropertyProvider(pattern, generatedValue)
}
}
private static Pattern getDateTimePattern(String format, Pattern defaultPattern) {
return format ? Pattern.compile(format) : defaultPattern
}
static Generators extract(Body body, Closure dslPropertyValueProvider) {
Generators generators = new Generators()
traverse(body, dslPropertyValueProvider, '', generators, Category.BODY)
return generators
}
static Generators extract(OutputMessage message, Closure dslPropertyValueProvider) {
Generators generators = new Generators()
traverse(message.body, dslPropertyValueProvider, '', generators, Category.BODY)
return generators
}
private static void traverse(Object value, Closure dslPropertyValueProvider, String path, Generators generators, Category category) {
Object v = value
if (v instanceof DslProperty) {
v = dslPropertyValueProvider(v)
}
if (v instanceof GString) {
v = ContentUtils.extractValue(v, dslPropertyValueProvider)
}
if (v instanceof Map) {
v.each { Map.Entry entry ->
traverse(entry.value, dslPropertyValueProvider, path + "." + entry.key, generators, category)
}
} else if (v instanceof Collection) {
v.eachWithIndex{ def entry, int index ->
traverse(entry, dslPropertyValueProvider, path + "[" + index + "]", generators, category)
}
} else if (v instanceof DslProperty) {
traverse(v, dslPropertyValueProvider, path, generators, category)
} else if (v instanceof Pattern) {
switch (v.pattern()) {
case INTEGER_PATTERN:
generators.addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE))
break
case DECIMAL_PATTERN:
generators.addGenerator(category, path, new RandomDecimalGenerator(10))
break
case HEX_PATTERN:
generators.addGenerator(category, path, new RandomHexadecimalGenerator(10))
break
case ALPHA_NUMERIC_PATTERN:
generators.addGenerator(category, path, new RandomStringGenerator(10))
break
case UUID_PATTERN:
generators.addGenerator(category, path, UuidGenerator.INSTANCE)
break
case ANY_DATE_PATTERN:
generators.addGenerator(category, path, new DateGenerator())
break
case ANY_TIME_PATTERN:
generators.addGenerator(category, path, new TimeGenerator())
break
case ANY_DATE_TIME_PATTERN:
generators.addGenerator(category, path, new DateTimeGenerator())
break
case TRUE_OR_FALSE_PATTERN:
generators.addGenerator(category, path, RandomBooleanGenerator.INSTANCE)
break
default:
generators.addGenerator(category, path, new RegexGenerator(v.pattern()))
break
}
}
}
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2013-2018 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.spec.pact
import au.com.dius.pact.model.Pact
@@ -11,13 +26,19 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import spock.lang.Issue
import spock.lang.Specification
import spock.lang.Subject
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class PactContractConverterSpec extends Specification {
File pactJson = new File(PactContractConverterSpec.getResource("/pact/pact.json").toURI())
File pact509Json = new File(PactContractConverterSpec.getResource("/pact/pact_509.json").toURI())
File pactv2Json = new File(PactContractConverterSpec.getResource("/pact/pact_v2.json").toURI())
File pactv3Json = new File(PactContractConverterSpec.getResource("/pact/pact_v3.json").toURI())
File pactv3MessagingJson = new File(PactContractConverterSpec.getResource("/pact/pact_v3_messaging.json").toURI())
File pactv3UnsupportedRuleLogicJson = new File(PactContractConverterSpec.getResource("/pact/pact_v3_unsupported_rule_logic.json").toURI())
@Subject PactContractConverter converter = new PactContractConverter()
def "should accept json files that are pact files"() {
@@ -48,7 +69,7 @@ class PactContractConverterSpec extends Specification {
contentType(applicationJson())
}
body(id: "123", method: "create")
stubMatchers {
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
}
}
@@ -62,7 +83,7 @@ class PactContractConverterSpec extends Specification {
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
testMatchers {
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
@@ -97,7 +118,7 @@ class PactContractConverterSpec extends Specification {
contentType(applicationJson())
}
body(id: 42, firstName: "Arthur", lastName: "Dent")
testMatchers {
bodyMatchers {
jsonPath('''$.['id']''', byType())
jsonPath('''$.['lastName']''', byType())
jsonPath('''$.['firstName']''', byType())
@@ -131,7 +152,7 @@ class PactContractConverterSpec extends Specification {
method: $(stub(regex("[0][1][2]"))),
something: "foo"
)
stubMatchers {
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
jsonPath('$.something', byEquality())
}
@@ -145,18 +166,29 @@ class PactContractConverterSpec extends Specification {
[email: "rddtGwwWMEhnkAPEmsyE",
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
number: $(producer(regex("[0-9]{3}")), consumer(923)),
positiveInteger: 1234567890,
negativeInteger: -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
something: "foo",
userName: "AJQrokEGPAVdOHprQpKP"]
userName: "AJQrokEGPAVdOHprQpKP",
nullValue: null]
]])
testMatchers {
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(1)
maxOccurrence(5)
})
jsonPath('$[0][*].number', byRegex(number()))
jsonPath('$[0][*].positiveInteger', byRegex(anInteger()))
jsonPath('$[0][*].negativeInteger', byRegex(anInteger()))
jsonPath('$[0][*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].negativeDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].userName', byType())
jsonPath('$[0][*].something', byEquality())
jsonPath('$[0][*].nullValue', byNull())
}
}
}
@@ -175,7 +207,10 @@ class PactContractConverterSpec extends Specification {
"request": {
"method": "GET",
"path": "\\/mallory",
"query": "name=ron&status=good",
"query": {
"name": ["ron"],
"status": ["good"]
},
"headers": {
"Content-Type": "application\\/json"
},
@@ -185,12 +220,18 @@ class PactContractConverterSpec extends Specification {
"something": "foo"
},
"matchingRules": {
"$.body.id": {
"match": "regex",
"regex": "[0-9]{3}"
},
"$.body.something": {
"match": "equality"
"body": {
"$.id": {
"matchers": [{
"match": "regex",
"regex": "[0-9]{3}"
}]
},
"$.something": {
"matchers": [{
"match": "equality"
}]
}
}
}
},
@@ -205,28 +246,76 @@ class PactContractConverterSpec extends Specification {
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"number": 923,
"userName": "AJQrokEGPAVdOHprQpKP"
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890,
"userName": "AJQrokEGPAVdOHprQpKP",
"something": "foo",
"nullValue": null
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"match": "regex",
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"match": "type",
"min": 1,
"max": 5
},
"$.body[0][*].userName": {
"match": "type"
},
"$.body[0][*].something": {
"match": "equality"
"body": {
"$[0][*].email": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].id": {
"matchers": [{
"match": "regex",
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}]
},
"$[0]": {
"matchers": [{
"match": "type",
"min": 1,
"max": 5
}]
},
"$[0][*].number": {
"matchers": [{
"match": "number"
}]
},
"$[0][*].positiveInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].negativeInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].positiveDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].negativeDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].userName": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].something": {
"matchers": [{
"match": "equality"
}]
},
"$[0][*].nullValue": {
"matchers": [{
"match": "null"
}]
}
}
}
}
@@ -234,18 +323,18 @@ class PactContractConverterSpec extends Specification {
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
"version": "3.0.0"
},
"pact-jvm": {
"version": "2.4.18"
"version": "3.5.13"
}
}
}
'''
when:
Pact pact = converter.convertTo(inputContracts)
Pact pact = converter.convertTo(inputContracts).get(0)
then:
String actual = JsonOutput.toJson(pact.toMap(PactSpecVersion.V2))
String actual = JsonOutput.toJson(pact.toMap(PactSpecVersion.V3))
JSONAssert.assertEquals(expectedJson, actual, false)
}
@@ -303,15 +392,267 @@ class PactContractConverterSpec extends Specification {
Map<String, Collection<Contract>> contracts = contractResources.collectEntries { [(it.filename) : ContractVerifierDslConverter.convertAsCollection(new File("/"), it.file)] }
Map<String, String> jsonPacts = pactResources.collectEntries { [(it.filename) : it.file.text] }
when:
Map<String, Pact> pacts = contracts.entrySet().collectEntries { [(it.key) : converter.convertTo(it.value)] }
Map<String, Collection<Pact>> pacts = contracts.entrySet().collectEntries { [(it.key) : converter.convertTo(it.value)] }
then:
pacts.entrySet().each {
String convertedPactAsText = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V2))
String convertedPactAsText = JsonOutput.toJson(it.value[0].toMap(PactSpecVersion.V3))
String pactFileName = it.key.replace("groovy", "json")
println "File name [${it.key}]"
JSONAssert.assertEquals(jsonPacts.get(pactFileName), convertedPactAsText, false)
JSONAssert.assertEquals(jsonPacts.get(pactFileName), convertedPactAsText, true)
}
}
def "should convert from pact v2 to two SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("get all users for max a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "rddtGwwWMEhnkAPEmsyE",
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
maxOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
},
Contract.make {
description("get all users for min a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "DPvAfkCZpOBZWzKYiDMC",
id: "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
userName: "GIUlVKoiLdHLYNKGbcSy"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv2Json)
then:
contracts == expectedContracts
}
def "should convert from pact v3 to three SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("java test interaction with a DSL array body")
request {
method(GET())
url("/")
headers {
contentType(applicationJsonUtf8())
header("Some-Header", $(c(regex("[a-zA-Z]{9}")), p("someValue")))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
header("Some-Header", $(c("someValue"), p(regex("[a-zA-Z]{9}"))))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
body([
[
"dob": "07/19/2016",
"id": 8958464620,
"name": "Rogger the Dogger",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
],
[
"dob": "07/19/2016",
"id": 4143398442,
"name": "Cat in the Hat",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
]
])
bodyMatchers {
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[*].nullValue', byNull())
jsonPath('$[*].aNumber', byRegex(number()))
jsonPath('$[*].positiveInteger', byRegex(anInteger()))
jsonPath('$[*].negativeInteger', byRegex(anInteger()))
jsonPath('$[*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[*].negativeDecimalNumber', byRegex(aDouble()))
}
}
},
Contract.make {
description("test interaction with a array body with templates")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
}
body([
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
]
])
bodyMatchers {
jsonPath('$[2].name', byType())
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[2].id', byType())
jsonPath('$[1].name', byType())
jsonPath('$[0].name', byType())
jsonPath('$[0].dob', byDate())
}
}
},
Contract.make {
description("test interaction with an array like matcher")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
}
body([
"data": [
"array1": [[
"dob": "2016-07-19",
"id": 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
]],
"array2": [[
"address": "127.0.0.1",
"name": "jvxrzduZnwwxpFYrQnpd"
]],
"array3": [[
[
"itemCount": 652571349
]
]]
],
"id": 7183997828
])
bodyMatchers {
jsonPath('$.data.array3[0]', byType() {
maxOccurrence(5)
})
jsonPath('$.data.array1', byType() {
minOccurrence(0)
})
jsonPath('$.data.array2', byType() {
minOccurrence(1)
})
jsonPath('$.id', byType())
jsonPath('$.data.array2[*].name', byType())
jsonPath('$.data.array2[*].address', byRegex("(\\d{1,3}\\.)+\\d{1,3}"))
jsonPath('$.data.array1[*].name', byType())
jsonPath('$.data.array1[*].id', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3Json)
then:
contracts == expectedContracts
}
def "should convert from pact v3 messaging to one SC message contract"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
label 'message sent to activemq:output'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
body([
bookName: "foo"
])
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
bodyMatchers {
jsonPath('$.bookName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3MessagingJson)
then:
contracts == expectedContracts
}
def "should fail to convert a pact v3 contract with unsupported rule logic"() {
when:
converter.convertFrom(pactv3UnsupportedRuleLogicJson)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("Currently only the AND combination rule logic is supported")
}
}
@@ -319,6 +660,6 @@ class PactContractConverterSpec extends Specification {
// file creator
/*
pacts.entrySet().each {
new File("target/${it.key.replace("groovy", "json")}").text = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V2))
new File("target/${it.key.replace("groovy", "json")}").text = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V3))
}
*/

View File

@@ -0,0 +1,41 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
body([
someInteger: $(c(anyInteger()), p(1234567890)),
someDecimal: $(c(anyDouble()), p(123.123)),
someHex: $(c(anyHex()), p('DEADC0DE')),
someAlphaNumeric: $(c(anyAlphaNumeric()), p('Some alpha numeric string with 1234567890')),
someUUID: $(c(anyUuid()), p('00000000-0000-0000-0000-000000000000')),
someDate: $(c(anyDate()), p('2018-03-26')),
someTime: $(c(anyTime()), p('13:37:00')),
someDateTime: $(c(anyDateTime()), p('2018-03-26 13:37:00')),
someBoolean: $(c(anyBoolean()), p('true')),
someRegex: $(c(regex('[0-9]{10}')), p(1234567890))
])
headers {
contentType('application/json')
}
}
response {
status OK()
body([
someInteger: $(c(1234567890), p(anyInteger())),
someDecimal: $(c(123.123), p(anyDouble())),
someHex: $(c('DEADC0DE'), p(anyHex())),
someAlphaNumeric: $(c('Some alpha numeric string with 1234567890'), p(anyAlphaNumeric())),
someUUID: $(c('00000000-0000-0000-0000-000000000000'), p(anyUuid())),
someDate: $(c('2018-03-26'), p(anyDate())),
someTime: $(c('13:37:00'), p(anyTime())),
someDateTime: $(c('2018-03-26 13:37:00'), p(anyDateTime())),
someBoolean: $(c('true'), p(anyBoolean())),
someRegex: $(c(1234567890), p(regex('[0-9]{10}')))
])
headers {
contentType('application/json')
}
}
}

View File

@@ -0,0 +1,280 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger":{
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal":{
"type": "RandomDecimal",
"digits": 10
},
"$.someHex":{
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric":{
"type": "RandomString",
"size": 10
},
"$.someUUID":{
"type": "Uuid"
},
"$.someDate":{
"type": "Date"
},
"$.someTime":{
"type": "Time"
},
"$.someDateTime":{
"type": "DateTime"
},
"$.someBoolean":{
"type": "RandomBoolean"
},
"$.someRegex":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header": {
"Content-Type":{
"matchers":[
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.someHex":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someAlphaNumeric":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someUUID":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDate":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDateTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someBoolean":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger":{
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal":{
"type": "RandomDecimal",
"digits": 10
},
"$.someHex":{
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric":{
"type": "RandomString",
"size": 10
},
"$.someUUID":{
"type": "Uuid"
},
"$.someDate":{
"type": "Date"
},
"$.someTime":{
"type": "Time"
},
"$.someDateTime":{
"type": "DateTime"
},
"$.someBoolean":{
"type": "RandomBoolean"
},
"$.someRegex":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.someHex":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someAlphaNumeric":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someUUID":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDate":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDateTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someBoolean":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,68 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
body([
someInteger: 1234567890,
someDecimal: 123.123,
someHex: 'DEADC0DE',
someAlphaNumeric: 'Some alpha numeric string with 1234567890',
someUUID: '00000000-0000-0000-0000-000000000000',
someDate: '2018-03-26',
someTime: '13:37:00',
someDateTime: '2018-03-26 13:37:00',
someBoolean: 'true',
someNullValue: null
])
headers {
contentType('application/json')
header("Some-Header", $(c(regex('[a-zA-Z]{9}')), p('someValue')))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
}
}
response {
status OK()
body([
someInteger: 1234567890,
someDecimal: 123.123,
someHex: 'DEADC0DE',
someAlphaNumeric: 'Some alpha numeric string with 1234567890',
someUUID: '00000000-0000-0000-0000-000000000000',
someDate: '2018-03-26',
someTime: '13:37:00',
someDateTime: '2018-03-26 13:37:00',
someBoolean: 'true',
someRegex: 1234567890
])
headers {
contentType('application/json')
header("Some-Header", $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
jsonPath('$.someNullValue', byNull())
}
}
}

View File

@@ -0,0 +1,276 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someNullValue": null,
"someInteger": 1234567890,
"someBoolean": "true",
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someInteger": 1234567890,
"someBoolean": "true",
"someRegex": 1234567890,
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
},
"$.someNullValue": {
"matchers": [
{
"match": "null"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -17,6 +17,37 @@
"body": {
"clientId": "8532032713",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.clientId":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
@@ -27,16 +58,47 @@
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.fraudCheckStatus":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.rejectionReason":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
"version": "3.0.0"
},
"pact-jvm": {
"version": "2.4.18"
"version": "3.5.13"
}
}
}

View File

@@ -17,6 +17,37 @@
"body": {
"clientId": "1234567890",
"loanAmount": 123.123
},
"generators": {
"body": {
"$.clientId":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.clientId":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
@@ -27,16 +58,39 @@
"body": {
"fraudCheckStatus": "OK",
"rejectionReason": null
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.fraudCheckStatus":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
"version": "3.0.0"
},
"pact-jvm": {
"version": "2.4.18"
"version": "3.5.13"
}
}
}

View File

@@ -19,16 +19,30 @@
},
"body": {
"count": 200
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
"version": "3.0.0"
},
"pact-jvm": {
"version": "2.4.18"
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,20 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
]

View File

@@ -0,0 +1,45 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to activemq:output",
"metaData":{
"BOOK-NAME":"foo",
"contentType":"application/json"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"bookReturnedTriggered()"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,27 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
]

View File

@@ -0,0 +1,44 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to jms:output",
"metaData":{
"BOOK-NAME":"foo"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"received message from jms:input"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,19 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
]

View File

@@ -0,0 +1,29 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"assert that bookWasDeleted()",
"metaData":{
},
"providerStates":[
{
"name":"received message from jms:delete"
}
]
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,94 @@
{
"provider": {
"name": "266_provider"
},
"consumer": {
"name": "test_consumer"
},
"interactions": [
{
"description": "get all users for max",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"userName": "AJQrokEGPAVdOHprQpKP"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"max": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
},
{
"description": "get all users for min",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
[
{
"email": "DPvAfkCZpOBZWzKYiDMC",
"id": "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
"userName": "GIUlVKoiLdHLYNKGbcSy"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"min": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -0,0 +1,278 @@
{
"provider": {
"name": "test_provider_array"
},
"consumer": {
"name": "test_consumer_array"
},
"interactions": [
{
"description": "java test interaction with a DSL array body",
"request": {
"method": "GET",
"path": "/",
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
]
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"body": [
{
"dob": "07/19/2016",
"id": 8958464620,
"name": "Rogger the Dogger",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
},
{
"dob": "07/19/2016",
"id": 4143398442,
"name": "Cat in the Hat",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
}
],
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{ "match" : "regex", "regex" : "[a-zA-Z]{9}" }
]
}
},
"body": {
"$[0].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].id": {
"matchers": [
{ "match": "type" }
]
},
"$[*].nullValue": {
"matchers": [
{ "match": "null" }
]
},
"$[*].aNumber": {
"matchers": [
{ "match": "number" }
]
},
"$[*].positiveInteger": {
"matchers": [
{ "match": "integer" }
]
},
"$[*].negativeInteger": {
"matchers": [
{ "match": "integer" }
]
},
"$[*].positiveDecimalNumber": {
"matchers": [
{ "match": "decimal" }
]
},
"$[*].negativeDecimalNumber": {
"matchers": [
{ "match": "decimal" }
]
}
}
}
}
},
{
"description": "test interaction with a array body with templates",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
}
],
"matchingRules": {
"body": {
"$[2].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].id": {
"matchers": [
{ "match": "type" }
]
},
"$[2].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].dob": {
"matchers": [
{ "date": "yyyy-MM-dd" }
]
}
}
}
}
},
{
"description": "test interaction with an array like matcher",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": {
"data": {
"array1": [
{
"dob": "2016-07-19",
"id": 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
}
],
"array2": [
{
"address": "127.0.0.1",
"name": "jvxrzduZnwwxpFYrQnpd"
}
],
"array3": [
[
{
"itemCount": 652571349
}
]
]
},
"id": 7183997828
},
"matchingRules": {
"body": {
"$.data.array3[0]": {
"matchers": [
{ "max": 5, "match": "type" }
]
},
"$.data.array1": {
"matchers": [
{ "min": 0, "match": "type" }
]
},
"$.data.array2": {
"matchers": [
{ "min": 1, "match": "type" }
]
},
"$.id": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array2[*].name": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array2[*].address": {
"matchers": [
{ "regex": "(\\d{1,3}\\.)+\\d{1,3}" }
]
},
"$.data.array1[*].name": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array1[*].id": {
"matchers": [
{ "match": "type" }
]
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to activemq:output",
"metaData":{
"BOOK-NAME":"foo",
"contentType":"application/json"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"bookReturnedTriggered()"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,47 @@
{
"provider": {
"name": "test_unsupported_rule_logic"
},
"consumer": {
"name": "test_unsupported_rule_logic"
},
"interactions": [
{
"description": "test unsupported rule logic",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
{
"optionalField": 1234567890
}
],
"matchingRules": {
"body": {
"$[*].optionalField": {
"matchers": [
{ "match": "integer" },
{ "match": "null" }
],
"combine": "OR"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -35,6 +35,7 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.reco
* Do not change to {@code @CompileStatic} since it's using double dispatch.
*
* @author Olga Maciaszek-Sharma, codearte.io
* @author Tim Ysewyn
*
* @since 1.0.0
*/
@@ -85,7 +86,7 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder {
} else {
bb.startBlock()
}
validateResponseBodyBlock(bb, outputMessage.matchers, outputMessage.body.serverValue)
validateResponseBodyBlock(bb, outputMessage.bodyMatchers, outputMessage.body.serverValue)
}
if (outputMessage.assertThat) {
bb.addLine(outputMessage.assertThat.executionCommand)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -57,6 +57,7 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.extr
* Do not change to {@code @CompileStatic} since it's using double dispatch.
*
* @author Olga Maciaszek-Sharma, codearte.io
* @author Tim Ysewyn
*
* @since 1.0.0
*/
@@ -373,7 +374,9 @@ abstract class MethodBodyBuilder {
bb.startBlock()
// for the rest we'll do JsonPath matching in brute force
bodyMatchers.jsonPathMatchers().each {
if (MatchingType.regexRelated(it.matchingType()) || it.matchingType() == MatchingType.EQUALITY) {
if (it.matchingType() == MatchingType.NULL) {
methodForNullCheck(it, bb)
} else if (MatchingType.regexRelated(it.matchingType()) || it.matchingType() == MatchingType.EQUALITY) {
methodForEqualityCheck(it, bb, copiedBody)
} else if (it.matchingType() == MatchingType.COMMAND) {
methodForCommandExecution(it, bb, copiedBody)
@@ -453,6 +456,13 @@ abstract class MethodBodyBuilder {
addColonIfRequired(bb)
}
protected void methodForNullCheck(BodyMatcher bodyMatcher, BlockBuilder bb) {
String quotedAndEscaptedPath = quotedAndEscaped(bodyMatcher.path())
String method = "assertThat(parsedJson.read(${quotedAndEscaptedPath})).isNull()"
bb.addLine(postProcessJsonPathCall(method))
addColonIfRequired(bb)
}
protected void methodForTypeCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) {
Object elementFromBody = value(copiedBody, bodyMatcher)
if (bodyMatcher.minTypeOccurrence() != null || bodyMatcher.maxTypeOccurrence() != null) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -43,6 +43,7 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.reco
* Do not change to {@code @CompileStatic} since it's using double dispatch.
*
* @author Olga Maciaszek-Sharma, codearte.io
* @author Tim Ysewyn
*
* @since 1.0.0
*/
@@ -171,7 +172,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder {
if (response.body) {
bb.endBlock()
bb.addLine(addCommentSignIfRequired('and:')).startBlock()
validateResponseBodyBlock(bb, response.matchers, response.body.serverValue)
validateResponseBodyBlock(bb, response.bodyMatchers, response.body.serverValue)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-2018 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.
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
*
* @since 1.2.1
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
@CompileStatic
class YamlContract {
@@ -130,12 +131,12 @@ class YamlContract {
@CompileStatic
static enum StubMatcherType {
by_date, by_time, by_timestamp, by_regex, by_equality
by_date, by_time, by_timestamp, by_regex, by_equality, by_null
}
@CompileStatic
static enum TestMatcherType {
by_date, by_time, by_timestamp, by_regex, by_equality, by_type, by_command
by_date, by_time, by_timestamp, by_regex, by_equality, by_type, by_command, by_null
}
@CompileStatic

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-2018 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.
@@ -53,6 +53,7 @@ import org.springframework.cloud.contract.verifier.util.MapConverter
*
* @since 1.2.1
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
@CompileStatic
class YamlContractConverter implements ContractConverter<List<YamlContract>> {
@@ -167,7 +168,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
}
multipart(multipartMap)
}
stubMatchers {
bodyMatchers {
yamlContract.request.matchers?.body?.each { BodyStubMatcher matcher ->
MatchingTypeValue value = null
switch (matcher.type) {
@@ -214,7 +215,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
if (yamlContract.response.body) body(yamlContract.response.body)
if (yamlContract.response.bodyFromFile) body(file(yamlContract.response.bodyFromFile))
if (yamlContract.response.async) async()
testMatchers {
bodyMatchers {
yamlContract.response?.matchers?.body?.each { BodyTestMatcher testMatcher ->
MatchingTypeValue value = null
switch (testMatcher.type) {
@@ -246,6 +247,9 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
case TestMatcherType.by_command:
value = byCommand(testMatcher.value)
break
case TestMatcherType.by_null:
value = byNull()
break
}
jsonPath(testMatcher.path, value)
}
@@ -266,7 +270,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
}
if (yamlContract.input.messageBody) messageBody(yamlContract.input.messageBody)
if (yamlContract.input.messageBodyFromFile) messageBody(file(yamlContract.input.messageBodyFromFile))
stubMatchers {
bodyMatchers {
yamlContract.input.matchers.body?.each { BodyStubMatcher matcher ->
MatchingTypeValue value = null
switch (matcher.type) {
@@ -306,7 +310,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
if (outputMsg.body) body(outputMsg.body)
if (outputMsg.bodyFromFile) body(file(outputMsg.bodyFromFile))
if (outputMsg.matchers) {
testMatchers {
bodyMatchers {
yamlContract.outputMessage?.matchers?.body?.each { BodyTestMatcher testMatcher ->
MatchingTypeValue value = null
switch (testMatcher.type) {
@@ -334,6 +338,9 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
case TestMatcherType.by_command:
value = byCommand(testMatcher.value)
break
case TestMatcherType.by_null:
value = byNull()
break
}
jsonPath(testMatcher.path, value)
}
@@ -456,7 +463,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
headers = (contract?.request?.headers as Headers)?.asTestSideMap()
body = MapConverter.getTestSideValues(contract?.request?.body)
matchers = new StubMatchers()
contract?.request?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
contract?.request?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
matchers.body << new BodyStubMatcher(
path: matcher.path(),
type: stubMatcherType(matcher.matchingType()),
@@ -469,7 +476,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
status = contract?.response?.status?.clientValue as Integer
headers = (contract?.response?.headers as Headers)?.asStubSideMap()
body = MapConverter.getStubSideValues(contract?.response?.body)
contract?.response?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
contract?.response?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
matchers.body << new BodyTestMatcher(
path: matcher.path(),
type: testMatcherType(matcher.matchingType()),
@@ -488,7 +495,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
yamlContract.input.messageBody = MapConverter.getTestSideValues(contract?.input?.messageBody)
yamlContract.input.messageFrom = contract?.input?.messageFrom?.serverValue
yamlContract.input.matchers.body.each {
contract?.input?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
contract?.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
yamlContract.input.matchers.body << new BodyStubMatcher(
path: matcher.path(),
type: stubMatcherType(matcher.matchingType()),
@@ -502,7 +509,7 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
yamlContract.outputMessage.headers = (contract?.outputMessage?.headers as Headers)?.asStubSideMap()
yamlContract.outputMessage.body = MapConverter.getStubSideValues(contract?.outputMessage?.body)
yamlContract.outputMessage.matchers.body.each {
contract?.input?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
contract?.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher ->
yamlContract.outputMessage.matchers.body << new BodyTestMatcher(
path: matcher.path(),
type: testMatcherType(matcher.matchingType()),

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -52,6 +52,8 @@ import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.bu
/**
* Converts a {@link Request} into {@link RequestPattern}
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 1.0.0
*/
@TypeChecked
@@ -95,17 +97,17 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
if (contentType == ContentType.JSON) {
def originalBody = getMatchingStrategyFromBody(request.body)?.clientValue
def body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.matchers)
def body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.bodyMatchers)
JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body)
if ((values.empty && !request.matchers?.hasMatchers()) || onlySizeAssertionsArePresent(values)) {
if ((values.empty && !request.bodyMatchers?.hasMatchers()) || onlySizeAssertionsArePresent(values)) {
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 {
if (request.bodyMatchers?.hasMatchers()) {
request.bodyMatchers.jsonPathMatchers().each {
String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it, originalBody)
requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\")))
}
@@ -122,7 +124,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
private boolean onlySizeAssertionsArePresent(JsonPaths values) {
return !values.empty && !request.matchers?.hasMatchers() && values.every { it.assertsSize() }
return !values.empty && !request.bodyMatchers?.hasMatchers() && values.every { it.assertsSize() }
}
private void appendMultipart(RequestPatternBuilder requestPattern) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -24,6 +24,7 @@ import spock.lang.Shared
import spock.lang.Specification
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class MessagingMethodBodyBuilderSpec extends Specification {
@@ -544,7 +545,9 @@ Contract.make {
body([
alpha: $(anyAlphaUnicode()),
number: $(anyNumber()),
anInteger: $(anyInteger()),
positiveInt: $(anyPositiveInt()),
aDouble: $(anyDouble()),
aBoolean: $(aBoolean()),
ip: $(anyIpAddress()),
hostname: $(anyHostname()),
@@ -571,12 +574,14 @@ Contract.make {
test.contains('assertThatJson(parsedJson).field("[\'aBoolean\']").matches("(true|false)")')
test.contains('assertThatJson(parsedJson).field("[\'alpha\']").matches("[\\\\p{L}]*")')
test.contains('assertThatJson(parsedJson).field("[\'hostname\']").matches("((http[s]?|ftp):/)/?([^:/\\\\s]+)(:[0-9]{1,5})?")')
test.contains('assertThatJson(parsedJson).field("[\'url\']").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'httpsUrl\']").matches("^(?:https:/{2}(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'number\']").matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'anInteger\']").matches("-?(\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'positiveInt\']").matches("([1-9]\\\\d*)")')
test.contains('assertThatJson(parsedJson).field("[\'aDouble\']").matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'email\']").matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6}")')
test.contains('assertThatJson(parsedJson).field("[\'ip\']").matches("([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])")')
test.contains('assertThatJson(parsedJson).field("[\'url\']").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'httpsUrl\']").matches("^(?:https:/{2}(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'uuid\']").matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")')
test.contains('assertThatJson(parsedJson).field("[\'date\']").matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
test.contains('assertThatJson(parsedJson).field("[\'dateTime\']").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])')
@@ -589,7 +594,7 @@ Contract.make {
!test.contains('REGEXP>>')
and:
String jsonSample = '''\
String json = "{\\"shouldFail\\":123,\\"duck\\":\\"8\\",\\"allpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"positiveInt\\":345,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"httpsUrl\\":\\"https://baz-486093581.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}";
String json = "{\\"shouldFail\\":123,\\"duck\\":\\"8\\",\\"alpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"anInteger\\":1780305902,\\"positiveInt\\":345,\\"aDouble\\":42.345,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"httpsUrl\\":\\"https://baz-486093581.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}";
DocumentContext parsedJson = JsonPath.parse(json);
'''
and:

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -684,4 +684,36 @@ DocumentContext parsedJson = JsonPath.parse(json);
"MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
}
def "should assert null values without matchers [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method GET()
url "test"
}
response {
status OK()
body([
nullValue: null
])
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
then:
blockBuilder.toString().contains("""assertThatJson(parsedJson).field("['nullValue']").isNull()""")
and:
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString())
and:
stubMappingIsValidWireMockStub(contractDsl)
where:
methodBuilderName | methodBuilder
"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

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -32,6 +32,7 @@ import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier
import org.springframework.cloud.contract.verifier.util.SyntaxChecker
/**
* @author Jakub Kubrynski, codearte.io
* @author Tim Ysewyn
*/
class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStubVerifier {
@@ -2073,6 +2074,7 @@ World.'''"""
duck: $(regex("[0-9]")),
alpha: $(anyAlphaUnicode()),
number: $(anyNumber()),
anInteger: $(anyInteger()),
positiveInt: $(positiveInt()),
aDouble: $(anyDouble()),
aBoolean: $(aBoolean()),
@@ -2099,6 +2101,7 @@ World.'''"""
body([
alpha: $(anyAlphaUnicode()),
number: $(anyNumber()),
anInteger: $(anyInteger()),
positiveInt: $(positiveInt()),
aDouble: $(anyDouble()),
aBoolean: $(aBoolean()),
@@ -2130,13 +2133,14 @@ World.'''"""
test.contains('assertThatJson(parsedJson).field("[\'aBoolean\']").matches("(true|false)")')
test.contains('assertThatJson(parsedJson).field("[\'alpha\']").matches("[\\\\p{L}]*")')
test.contains('assertThatJson(parsedJson).field("[\'hostname\']").matches("((http[s]?|ftp):/)/?([^:/\\\\s]+)(:[0-9]{1,5})?")')
test.contains('assertThatJson(parsedJson).field("[\'url\']").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'httpsUrl\']").matches("^(?:https:/{2}(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'number\']").matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'anInteger\']").matches("-?(\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'positiveInt\']").matches("([1-9]\\\\d*)")')
test.contains('assertThatJson(parsedJson).field("[\'aDouble\']").matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThatJson(parsedJson).field("[\'email\']").matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6}")')
test.contains('assertThatJson(parsedJson).field("[\'ip\']").matches("([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])")')
test.contains('assertThatJson(parsedJson).field("[\'url\']").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'httpsUrl\']").matches("^(?:https:/{2}(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)')
test.contains('assertThatJson(parsedJson).field("[\'uuid\']").matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")')
test.contains('assertThatJson(parsedJson).field("[\'date\']").matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
test.contains('assertThatJson(parsedJson).field("[\'dateTime\']").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])')
@@ -2151,7 +2155,7 @@ World.'''"""
SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString())
and:
String jsonSample = '''\
String json = "{\\"duck\\":\\"8\\",\\"alpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"positiveInt\\":41,\\"aDouble\\":42.345,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"httpsUrl\\":\\"https://baz-486093581.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}";
String json = "{\\"duck\\":\\"8\\",\\"alpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"anInteger\\":1780305902,\\"positiveInt\\":345,\\"aDouble\\":42.345,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"httpsUrl\\":\\"https://baz-486093581.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}";
DocumentContext parsedJson = JsonPath.parse(json);
'''
and:

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 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.
@@ -56,7 +56,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
'complex.key' : 'foo'
]
])
stubMatchers {
bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
@@ -78,6 +78,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
duck: 123,
alpha: "abc",
number: 123,
positiveInteger: 1234567890,
negativeInteger: -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
aBoolean: true,
date: "2017-01-01",
dateTime: "2017-01-01T01:23:45",
@@ -97,9 +101,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
valueWithMaxEmpty: [],
key: [
'complex.key' : 'foo'
]
],
nullValue: null
])
testMatchers {
bodyMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
// asserts the jsonpath value against the provided value
@@ -108,6 +113,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.positiveInteger', byRegex(anInteger()))
jsonPath('$.negativeInteger', byRegex(anInteger()))
jsonPath('$.positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$.negativeDecimalNumber', byRegex(aDouble()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
// asserts vs inbuilt time related regex
jsonPath('$.date', byDate())
@@ -139,9 +148,11 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
jsonPath('$.nullValue', byNull())
}
headers {
contentType(applicationJson())
header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
}
}
}
@@ -157,6 +168,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).matches("[\\\\p{L}]*")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).isEqualTo("abc")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.number", String.class)).matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveDecimalNumber", String.class)).matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeDecimalNumber", 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])")')
@@ -174,6 +189,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class)).as("' + rootElement + '.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0)')
test.contains('assertThatValueIsANumber(parsedJson.read("' + rootElement + '.duck")')
test.contains('assertThat(parsedJson.read("' + rootElement + '''.['key'].['complex.key']", String.class)).isEqualTo("foo")''')
test.contains('assertThat(parsedJson.read("' + rootElement + '.nullValue")).isNull()')
!test.contains('cursor')
and:
try {
@@ -224,7 +240,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
]
],
])
testMatchers {
bodyMatchers {
jsonPath('$.phoneNumbers', byType {
minOccurrence(0) // min occurrence of 1
maxOccurrence(4) // max occurrence of 3
@@ -276,7 +292,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
number: "foo"
]
])
testMatchers {
bodyMatchers {
jsonPath('$.phoneNumbers[*].number', byType {
minOccurrence(0)
maxOccurrence(4)
@@ -329,7 +345,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
number: "foo"
]
])
testMatchers {
bodyMatchers {
jsonPath('$.phoneNumbers[*].number', byCommand('foo($it)'))
}
}
@@ -364,7 +380,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
number: "foo"
]
])
testMatchers {
bodyMatchers {
jsonPath('$.nonExistingPhoneNumbers[*].number', byCommand('foo($it)'))
}
}
@@ -401,7 +417,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
headers {
contentType(applicationJson())
}
testMatchers {
bodyMatchers {
jsonPath('''$[0][0].access_token''', byEquality())
}
}
@@ -445,7 +461,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
]
}
""")
testMatchers {
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })
@@ -488,7 +504,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
"title": "lorem ipsum"
]
])
testMatchers {
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-2018 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.
@@ -31,6 +31,7 @@ import org.springframework.cloud.contract.verifier.util.MapConverter
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class YamlContractConverterSpec extends Specification {
@@ -81,9 +82,9 @@ class YamlContractConverterSpec extends Specification {
contract.request.headers.entries.find { it.name == "fooReq" &&
it.serverValue == "baz" }
contract.request.body.clientValue == [foo: "bar"]
contract.request.matchers.jsonPathRegexMatchers[0].path() == '$.foo'
contract.request.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.request.matchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.request.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo'
contract.request.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.request.bodyMatchers.jsonPathRegexMatchers[0].value() == 'bar'
and:
contract.response.status.clientValue == 200
contract.response.headers.entries.find { it.name == "foo2" &&
@@ -92,13 +93,16 @@ class YamlContractConverterSpec extends Specification {
((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)" }
contract.response.headers.entries.find { it.name == "fooRes" &&
it.clientValue == "baz" }
contract.response.body.clientValue == [foo2: "bar", foo3: "baz"]
contract.response.matchers.jsonPathRegexMatchers[0].path() == '$.foo2'
contract.response.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.response.matchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.response.matchers.jsonPathRegexMatchers[1].path() == '$.foo3'
contract.response.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND
contract.response.matchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)')
contract.response.body.clientValue == [foo2: "bar", foo3: "baz", nullValue: null]
contract.response.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo2'
contract.response.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.response.bodyMatchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.response.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.foo3'
contract.response.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND
contract.response.bodyMatchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)')
contract.response.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.nullValue'
contract.response.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.NULL
contract.response.bodyMatchers.jsonPathRegexMatchers[2].value() == null
where:
yamlFile << [ymlWithRest, ymlWithRest2, ymlWithRest3]
}
@@ -151,81 +155,81 @@ class YamlContractConverterSpec extends Specification {
RegexPatterns patterns = new RegexPatterns()
contract.request.headers.entries.find { it.name == "Content-Type" &&
((Pattern) it.clientValue).pattern == "application/json.*" && it.serverValue == "application/json" }
contract.request.matchers.jsonPathRegexMatchers[0].path() == '$.duck'
contract.request.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.request.matchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}'
contract.request.matchers.jsonPathRegexMatchers[1].path() == '$.duck'
contract.request.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY
contract.request.matchers.jsonPathRegexMatchers[2].path() == '$.alpha'
contract.request.matchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX
contract.request.matchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode().pattern()
contract.request.matchers.jsonPathRegexMatchers[3].path() == '$.alpha'
contract.request.matchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY
contract.request.matchers.jsonPathRegexMatchers[4].path() == '$.number'
contract.request.matchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX
contract.request.matchers.jsonPathRegexMatchers[4].value() == patterns.number().pattern()
contract.request.matchers.jsonPathRegexMatchers[5].path() == '$.aBoolean'
contract.request.matchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX
contract.request.matchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean().pattern()
contract.request.matchers.jsonPathRegexMatchers[6].path() == '$.date'
contract.request.matchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE
contract.request.matchers.jsonPathRegexMatchers[6].value() == patterns.isoDate()
contract.request.matchers.jsonPathRegexMatchers[7].path() == '$.dateTime'
contract.request.matchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP
contract.request.matchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime()
contract.request.matchers.jsonPathRegexMatchers[8].path() == '$.time'
contract.request.matchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME
contract.request.matchers.jsonPathRegexMatchers[8].value() == patterns.isoTime()
contract.request.matchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']"
contract.request.matchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY
contract.request.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck'
contract.request.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.request.bodyMatchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}'
contract.request.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck'
contract.request.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY
contract.request.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha'
contract.request.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX
contract.request.bodyMatchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode().pattern()
contract.request.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha'
contract.request.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY
contract.request.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number'
contract.request.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX
contract.request.bodyMatchers.jsonPathRegexMatchers[4].value() == patterns.number().pattern()
contract.request.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean'
contract.request.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX
contract.request.bodyMatchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean().pattern()
contract.request.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date'
contract.request.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE
contract.request.bodyMatchers.jsonPathRegexMatchers[6].value() == patterns.isoDate()
contract.request.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime'
contract.request.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP
contract.request.bodyMatchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime()
contract.request.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time'
contract.request.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME
contract.request.bodyMatchers.jsonPathRegexMatchers[8].value() == patterns.isoTime()
contract.request.bodyMatchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']"
contract.request.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY
and:
contract.response.status.clientValue == 200
contract.response.matchers.jsonPathRegexMatchers[0].path() == '$.duck'
contract.response.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.response.matchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}'
contract.response.matchers.jsonPathRegexMatchers[1].path() == '$.duck'
contract.response.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY
contract.response.matchers.jsonPathRegexMatchers[2].path() == '$.alpha'
contract.response.matchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX
contract.response.matchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode().pattern()
contract.response.matchers.jsonPathRegexMatchers[3].path() == '$.alpha'
contract.response.matchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY
contract.response.matchers.jsonPathRegexMatchers[4].path() == '$.number'
contract.response.matchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX
contract.response.matchers.jsonPathRegexMatchers[4].value() == patterns.number().pattern()
contract.response.matchers.jsonPathRegexMatchers[5].path() == '$.aBoolean'
contract.response.matchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX
contract.response.matchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean().pattern()
contract.response.matchers.jsonPathRegexMatchers[6].path() == '$.date'
contract.response.matchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE
contract.response.matchers.jsonPathRegexMatchers[6].value() == patterns.isoDate()
contract.response.matchers.jsonPathRegexMatchers[7].path() == '$.dateTime'
contract.response.matchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP
contract.response.matchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime()
contract.response.matchers.jsonPathRegexMatchers[8].path() == '$.time'
contract.response.matchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME
contract.response.matchers.jsonPathRegexMatchers[8].value() == patterns.isoTime()
contract.response.matchers.jsonPathRegexMatchers[9].path() == '$.valueWithTypeMatch'
contract.response.matchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin'
contract.response.matchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1
contract.response.matchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax'
contract.response.matchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3
contract.response.matchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax'
contract.response.matchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1
contract.response.matchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3
contract.response.matchers.jsonPathRegexMatchers[13].path() == '$.valueWithMinEmpty'
contract.response.matchers.jsonPathRegexMatchers[13].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[13].minTypeOccurrence() == 0
contract.response.matchers.jsonPathRegexMatchers[14].path() == '$.valueWithMaxEmpty'
contract.response.matchers.jsonPathRegexMatchers[14].matchingType() == MatchingType.TYPE
contract.response.matchers.jsonPathRegexMatchers[14].maxTypeOccurrence() == 0
contract.response.matchers.jsonPathRegexMatchers[15].path() == '$.duck'
contract.response.matchers.jsonPathRegexMatchers[15].matchingType() == MatchingType.COMMAND
contract.response.matchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)')
contract.response.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck'
contract.response.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.response.bodyMatchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}'
contract.response.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck'
contract.response.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY
contract.response.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha'
contract.response.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX
contract.response.bodyMatchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode().pattern()
contract.response.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha'
contract.response.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY
contract.response.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number'
contract.response.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX
contract.response.bodyMatchers.jsonPathRegexMatchers[4].value() == patterns.number().pattern()
contract.response.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean'
contract.response.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX
contract.response.bodyMatchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean().pattern()
contract.response.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date'
contract.response.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE
contract.response.bodyMatchers.jsonPathRegexMatchers[6].value() == patterns.isoDate()
contract.response.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime'
contract.response.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP
contract.response.bodyMatchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime()
contract.response.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time'
contract.response.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME
contract.response.bodyMatchers.jsonPathRegexMatchers[8].value() == patterns.isoTime()
contract.response.bodyMatchers.jsonPathRegexMatchers[9].path() == '$.valueWithTypeMatch'
contract.response.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin'
contract.response.bodyMatchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1
contract.response.bodyMatchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax'
contract.response.bodyMatchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3
contract.response.bodyMatchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax'
contract.response.bodyMatchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1
contract.response.bodyMatchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3
contract.response.bodyMatchers.jsonPathRegexMatchers[13].path() == '$.valueWithMinEmpty'
contract.response.bodyMatchers.jsonPathRegexMatchers[13].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[13].minTypeOccurrence() == 0
contract.response.bodyMatchers.jsonPathRegexMatchers[14].path() == '$.valueWithMaxEmpty'
contract.response.bodyMatchers.jsonPathRegexMatchers[14].matchingType() == MatchingType.TYPE
contract.response.bodyMatchers.jsonPathRegexMatchers[14].maxTypeOccurrence() == 0
contract.response.bodyMatchers.jsonPathRegexMatchers[15].path() == '$.duck'
contract.response.bodyMatchers.jsonPathRegexMatchers[15].matchingType() == MatchingType.COMMAND
contract.response.bodyMatchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)')
}
def "should convert YAML with REST with response from request"() {
@@ -285,9 +289,9 @@ class YamlContractConverterSpec extends Specification {
contract.input.messageHeaders.entries.find { it.name == "foo" &&
((Pattern) it.clientValue).pattern == "bar" && it.serverValue == "bar" }
contract.input.messageBody.clientValue == [foo: "bar"]
contract.input.matchers.jsonPathRegexMatchers[0].path() == '$.bar'
contract.input.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.input.matchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.input.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.bar'
contract.input.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.input.bodyMatchers.jsonPathRegexMatchers[0].value() == 'bar'
and:
contract.outputMessage.assertThat.toString() == "baz()"
contract.outputMessage.headers.entries.find { it.name == "foo2" &&
@@ -297,12 +301,12 @@ class YamlContractConverterSpec extends Specification {
contract.outputMessage.headers.entries.find { it.name == "fooRes" &&
it.clientValue == "baz" }
contract.outputMessage.body.clientValue == [foo2: "bar", foo3: "baz"]
contract.outputMessage.matchers.jsonPathRegexMatchers[0].path() == '$.foo2'
contract.outputMessage.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.outputMessage.matchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.outputMessage.matchers.jsonPathRegexMatchers[1].path() == '$.foo3'
contract.outputMessage.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND
contract.outputMessage.matchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)')
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo2'
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].value() == 'bar'
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.foo3'
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND
contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)')
}
def "should convert YAML with messaging triggered by a method to DSL"() {

View File

@@ -54,6 +54,7 @@ response:
body:
foo2: bar
foo3: baz
nullValue: null
matchers:
body:
- path: $.foo2
@@ -62,6 +63,9 @@ response:
- path: $.foo3
type: by_command
value: executeMe($it)
- path: $.nullValue
type: by_null
value: null
headers:
- key: foo2
regex: bar

View File

@@ -13,6 +13,7 @@ request:
valueWithTypeMatch: "string"
key:
"complex.key": 'foo'
nullValue: null
matchers:
headers:
- key: Content-Type
@@ -42,6 +43,8 @@ request:
type: by_time
- path: "$.['key'].['complex.key']"
type: by_equality
- path: $.nullvalue
type: by_null
headers:
Content-Type: application/json
response:
@@ -72,6 +75,7 @@ response:
valueWithMaxEmpty: []
key:
'complex.key' : 'foo'
nulValue: null
matchers:
headers:
- key: Content-Type
@@ -120,5 +124,8 @@ response:
- path: $.duck
type: by_command
value: assertThatValueIsANumber($it)
- path: $.nullValue
type: by_null
value: null
headers:
Content-Type: application/json

View File

@@ -30,6 +30,7 @@ response:
body:
foo2: bar
foo3: baz
nullValue: null
matchers:
body:
- path: $.foo2
@@ -38,6 +39,9 @@ response:
- path: $.foo3
type: by_command
value: executeMe($it)
- path: $.nullValue
type: by_null
value: null
headers:
- key: foo2
regex: bar

View File

@@ -32,6 +32,7 @@ response:
body:
foo2: bar
foo3: baz
nullValue: null
matchers:
body:
- path: $.foo2
@@ -40,6 +41,9 @@ response:
- path: $.foo3
type: by_command
value: executeMe($it)
- path: $.nullValue
type: by_null
value: null
headers:
- key: foo2
regex: bar

View File

@@ -17,11 +17,11 @@ Contract.make {
}
{{/request_headers_present}}
{{#request_json_paths_present}}
stubMatchers {
bodyMatchers {
{{#request_json_paths}}
jsonPath('''{{jsonPath}}''', byEquality())
{{/request_json_paths}}
}
}
{{/request_json_paths_present}}
}
response {

View File

@@ -91,7 +91,7 @@ public class ContractDslSnippetTests {
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue().toString()).startsWith("/");
then(parsedContract.getRequest().getBody().getClientValue()).isNotNull();
then(parsedContract.getRequest().getMatchers().hasMatchers()).isTrue();
then(parsedContract.getRequest().getBodyMatchers().hasMatchers()).isTrue();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getResponse().getBody().getClientValue()).isNotNull();
@@ -128,7 +128,7 @@ public class ContractDslSnippetTests {
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue().toString()).startsWith("/");
then(parsedContract.getRequest().getBody().getClientValue()).isNotNull();
then(parsedContract.getRequest().getMatchers().hasMatchers()).isTrue();
then(parsedContract.getRequest().getBodyMatchers().hasMatchers()).isTrue();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getResponse().getBody().getClientValue()).isNotNull();
@@ -152,7 +152,7 @@ public class ContractDslSnippetTests {
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders()).isNull();
then(parsedContract.getResponse().getBody()).isNull();
then(parsedContract.getResponse().getMatchers()).isNull();
then(parsedContract.getResponse().getBodyMatchers()).isNull();
}
private Set<String> headerNames(Set<Header> headers) {