diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index ffd89aa5ab..847ae76451 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -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" ] + }, } ---- diff --git a/pom.xml b/pom.xml index dccf5b0ad0..59d709b586 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ 2.17 + 3.5.13 0.0.9 2.0.0.BUILD-SNAPSHOT 2.0.0.BUILD-SNAPSHOT @@ -111,10 +112,27 @@ commons-text ${commons-text.version} + + au.com.dius + pact-jvm-consumer-java8_2.12 + ${pact.version} + + + org.codehaus.groovy + groovy-all + + + au.com.dius pact-jvm-model - 2.4.18 + ${pact.version} + + + org.codehaus.groovy + groovy-all + + com.github.jknack diff --git a/samples/standalone/pact/pact-http-client/build.gradle b/samples/standalone/pact/pact-http-client/build.gradle index 3db8de8ac6..65cb923715 100644 --- a/samples/standalone/pact/pact-http-client/build.gradle +++ b/samples/standalone/pact/pact-http-client/build.gradle @@ -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[] } diff --git a/samples/standalone/pact/pact-http-client/pom.xml b/samples/standalone/pact/pact-http-client/pom.xml index 26ac2d6b73..322c055d53 100644 --- a/samples/standalone/pact/pact-http-client/pom.xml +++ b/samples/standalone/pact/pact-http-client/pom.xml @@ -52,7 +52,7 @@ au.com.dius pact-jvm-model - 2.4.18 + 3.5.13 test diff --git a/samples/standalone/pact/pact-http-server/build.gradle b/samples/standalone/pact/pact-http-server/build.gradle index 64690c9519..61ecf49bf1 100644 --- a/samples/standalone/pact/pact-http-server/build.gradle +++ b/samples/standalone/pact/pact-http-server/build.gradle @@ -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[] } } diff --git a/samples/standalone/pact/pact-http-server/pom.xml b/samples/standalone/pact/pact-http-server/pom.xml index 5db25dc22d..bd4bfa4a71 100644 --- a/samples/standalone/pact/pact-http-server/pom.xml +++ b/samples/standalone/pact/pact-http-server/pom.xml @@ -83,7 +83,7 @@ au.com.dius pact-jvm-model - 2.4.18 + 3.5.13 diff --git a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json index fc172767cb..40675c6dac 100644 --- a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json +++ b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json @@ -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" } } } \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy index d11e946296..9c8fb3246a 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -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 diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy index 465df6461d..cbcc62ccc3 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy @@ -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() } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy index 4668869eff..d0983aedb3 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy @@ -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 diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy index 9f0baa32bf..6ab34597a0 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy @@ -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() } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy index dfe1c17036..863be3cf16 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy @@ -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 { 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 { 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) } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy index c2f3545e99..f0292ceafe 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy @@ -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 } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy index ffdc9ba666..f75b72463e 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy @@ -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() } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy index d21c50ce6a..f36af5bba4 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -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() } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy index 2e22bce084..e98e5f5f54 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy @@ -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 diff --git a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy index 17e2abb59f..cacdc94ba0 100644 --- a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy +++ b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy @@ -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() + } } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java index dfc6c282a5..e9da60e4be 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java @@ -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); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java index f28b34cb8a..3e916a62cd 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java @@ -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); diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy index 62cc6f3bae..91121462ab 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelectorSpec.groovy @@ -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}")) } } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy index 3f85df542f..abd0ea5d65 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelectorSpec.groovy @@ -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}")) } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy index 6f80c18ef0..3345834f38 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -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())) } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml index 4a2a27e42f..0903e15de3 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml @@ -52,7 +52,7 @@ au.com.dius pact-jvm-model - 2.4.18 + 3.5.13 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml index 4fe0364be0..f557aeb13d 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml @@ -35,8 +35,7 @@ au.com.dius - pact-jvm-model - true + pact-jvm-consumer-java8_2.12 org.spockframework diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy new file mode 100644 index 0000000000..2c525b2313 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy @@ -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 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(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(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(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 generatorsPerPath, Closure 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) + } + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy new file mode 100644 index 0000000000..615477e9d2 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy @@ -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}" + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy new file mode 100644 index 0000000000..99e9b5026c --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy @@ -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 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() + } + } +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy new file mode 100644 index 0000000000..447b4cfc74 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy @@ -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 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() + "()" + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy index 1b047d902d..8c2a5184e2 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy @@ -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 { +class PactContractConverter implements ContractConverter> { - 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 { @Override Collection convertFrom(File file) { Pact pact = PactReader.loadPact(file) - List 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 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> rules = requestResponseInteraction.request.matchingRules - rules.each { String key, Map 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> rules = requestResponseInteraction.response.matchingRules - Map 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 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) { - Provider provider = new Provider() - provider.name = "Provider" - Consumer consumer = new Consumer() - consumer.name = "Consumer" - List 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 convertTo(Collection contracts) { + List 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 headers(Headers headers, Closure closure) { - return headers.entries.collectEntries { - String name = it.name - String value = closure(it) - return [(name) : value] - } - } - - protected Map> 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 matchingRule = [:] - switch (matchingType) { - case MatchingType.EQUALITY: - matchingRule << [(MATCH_KEY) : MatchingType.EQUALITY.toString().toLowerCase() as Object] - break - case MatchingType.TYPE: - Map 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 } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy new file mode 100644 index 0000000000..a745d4f7eb --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy @@ -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 + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy new file mode 100644 index 0000000000..46f94491ab --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy @@ -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 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 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 params = it.params + description += "(" + params.forEach({ String k, Object v -> + description += k + ": " + v.toString() + if (params.keySet().last() != k) { + description += ", " + } + }) + description += ")" + } + }) + return description + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy new file mode 100644 index 0000000000..18efdc5f30 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy @@ -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 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 + } + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy index f9a0dae471..4d3b6ba497 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy @@ -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> contracts = contractResources.collectEntries { [(it.filename) : ContractVerifierDslConverter.convertAsCollection(new File("/"), it.file)] } Map jsonPacts = pactResources.collectEntries { [(it.filename) : it.file.text] } when: - Map pacts = contracts.entrySet().collectEntries { [(it.key) : converter.convertTo(it.value)] } + Map> 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 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 contracts = converter.convertFrom(pactv2Json) + then: + contracts == expectedContracts + } + + def "should convert from pact v3 to three SC contracts"() { + given: + Collection 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 contracts = converter.convertFrom(pactv3Json) + then: + contracts == expectedContracts + } + + def "should convert from pact v3 messaging to one SC message contract"() { + + given: + Collection 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 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)) } */ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.groovy new file mode 100644 index 0000000000..9d6c697e9d --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.groovy @@ -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') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.json new file mode 100644 index 0000000000..fa3ba8bf23 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllGenerators.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.groovy new file mode 100644 index 0000000000..38cfee255e --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.groovy @@ -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()) + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.json new file mode 100644 index 0000000000..d44a56f190 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldConvertAllMatchers.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json index 7de1aeddf0..200a4c569c 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json @@ -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" } } } \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json index db5f37e92e..4720e2c6c7 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json @@ -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" } } } \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json index 32a0a0b806..e3c9668686 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json @@ -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" } } } \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.groovy new file mode 100644 index 0000000000..fa076a648b --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.groovy @@ -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()) + } + } + } +] \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.json new file mode 100644 index 0000000000..443495f733 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenBookReturned.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.groovy new file mode 100644 index 0000000000..8d3ace2be5 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.groovy @@ -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') + } + } + } +] \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.json new file mode 100644 index 0000000000..1c9cdf549c --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldSendMessageWhenMessageReceived.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.groovy new file mode 100644 index 0000000000..e16640a4df --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.groovy @@ -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()') + } + } +] \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.json new file mode 100644 index 0000000000..8b3ccd86f0 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldVerifyWhenBookWasDeleted.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v2.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v2.json new file mode 100644 index 0000000000..02eaba2487 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v2.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3.json new file mode 100644 index 0000000000..2c964d2918 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_messaging.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_messaging.json new file mode 100644 index 0000000000..443495f733 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_messaging.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_unsupported_rule_logic.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_unsupported_rule_logic.json new file mode 100644 index 0000000000..166e87ed0e --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_v3_unsupported_rule_logic.json @@ -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" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy index 73e250fe34..a1d16f8e3b 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy @@ -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) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index 63592dec5f..abfd754071 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -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) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index 8632ffcdc3..85eb2f881e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -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) } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy index 19f389f2f6..341f4cfa86 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy @@ -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 diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy index bbba931f1d..e5c77117e3 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy @@ -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> { @@ -167,7 +168,7 @@ class YamlContractConverter implements ContractConverter> { } 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> { 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> { 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> { } 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> { 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> { 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> { 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> { 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> { 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> { 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()), diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index 480c4e6271..64634d8a9b 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -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) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy index d61bdd27e8..3cc4e121ec 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy @@ -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: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 8db5583b2a..437773ee97 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -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) } + } + } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index f92471ed8c..fc31b856bf 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -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: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy index 75a875c9d5..ce7b49fa2d 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -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) }) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index 66e15e229d..665ce7ca72 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -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"() { diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml index 0e25f7399a..c8246a7532 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml @@ -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 diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml index a9c8e9b161..dfa11813c5 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml @@ -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 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml index d50e335f0b..29a67be1b3 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml @@ -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 diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml index 1d0ad18e6b..81bc88a682 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml @@ -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 diff --git a/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet b/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet index 14c4bdeacb..d22792c819 100644 --- a/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet +++ b/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet @@ -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 { diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java index 47475a1b6f..f69831bb1a 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java @@ -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 headerNames(Set
headers) {