diff --git a/docs/src/main/asciidoc/_project-features-contract.adoc b/docs/src/main/asciidoc/_project-features-contract.adoc index 9ddc5a94dd..5e68a61597 100644 --- a/docs/src/main/asciidoc/_project-features-contract.adoc +++ b/docs/src/main/asciidoc/_project-features-contract.adoc @@ -6,20 +6,7 @@ Spring Cloud Contract supports the DSLs written in the following languages: * Groovy * YAML - -TIP: If you are not familiar with Groovy, do not worry - you can use Java syntax in the -Groovy DSL files as well. - -If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy -before. Knowledge of the language is not really needed, as the Contract DSL uses only a -tiny subset of it (only literals, method calls, and closures). Also, the DSL is statically -typed, to make it programmer-readable without any knowledge of the DSL itself. - -IMPORTANT: Remember that, inside the Groovy contract file, you have to provide the fully -qualified name to the `Contract` class and `make` static imports, such as -`org.springframework.cloud.spec.Contract.make { ... }`. You can also provide an import to -the `Contract` class (`import org.springframework.cloud.spec.Contract`) and then call -`Contract.make { ... }`. +* Java TIP: Spring Cloud Contract supports defining multiple contracts in a single file. @@ -37,8 +24,13 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0] ---- -==== +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest.java[tags=class,indent=0] +---- +==== [TIP] ==== @@ -49,6 +41,54 @@ mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert ---- ==== +[[contract-groovy]] +=== Contract DSL in Groovy + +If you are not familiar with Groovy, do not worry - you can use Java syntax in the +Groovy DSL files as well. + +If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls, and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself. + +IMPORTANT: Remember that, inside the Groovy contract file, you have to provide the fully +qualified name to the `Contract` class and `make` static imports, such as +`org.springframework.cloud.spec.Contract.make { ... }`. You can also provide an import to +the `Contract` class (`import org.springframework.cloud.spec.Contract`) and then call +`Contract.make { ... }`. + +[[contract-java]] +=== Contract DSL in Java + +To write a contract definition in Java, you need to create a class, that implements either the `Supplier` interface for a single contract or `Supplier>` for multiple contracts. + +You can also write the contract definitions under `src/test/java` (e.g. `src/test/java/contracts`) so that you don't have to modify the classpath of your project. In this case you'll have to provide a new location of contract definitions to your Spring Cloud Contract plugin. + +==== +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven +---- + + org.springframework.cloud + spring-cloud-contract-maven-plugin + ${spring-cloud-contract.version} + true + + src/test/java/contracts + + +---- + +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- +contracts { + contractsDslDir = new File(project.rootDir, "src/test/java/contracts") +} +---- +==== + [[contract-limitations]] === Limitations @@ -90,6 +130,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co ---- include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=description,indent=0] +---- ==== [[contract-dsl-name]] @@ -119,6 +165,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co ---- include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=name,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=name,indent=0] +---- ==== [[contract-dsl-ignoring-contracts]] @@ -140,6 +192,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co ---- include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=ignored,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=ignored,indent=0] +---- ==== [[contract-dsl-in-progress]] @@ -164,6 +222,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co ---- include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=in_progress,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=in_progress,indent=0] +---- ==== You can set the value of the `failOnInProgress` Spring Cloud Contract plugin property to ensure that your build will break when at least one contract in progress remains in your sources. @@ -199,6 +263,12 @@ include::{verifier_core_path}/src/test/resources/classpath/readFromFile.groovy[i ---- include::{verifier_core_path}/src/test/resources/yml/contract_from_file.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_from_file.java[tags=class,indent=0] +---- ==== Further assume that the JSON files is as follows: @@ -222,7 +292,7 @@ of a request or a response. The name of the file needs to be a file with locatio relative to the folder in which the contract lays. If you need to pass the contents of a file in binary form, -you can use the `fileAsBytes` method in Groovy DSL or a `bodyFromFileAsBytes` field in YAML. +you can use the `fileAsBytes` method in the coded DSL or a `bodyFromFileAsBytes` field in YAML. The following example shows how to pass the contents of binary files: @@ -238,6 +308,12 @@ include::{verifier_core_path}/src/test/resources/body_builder/worksWithPdf.groov ---- include::{verifier_core_path}/src/test/resources/yml/contract_pdf.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_from_pdf.java[tags=class,indent=0] +---- ==== IMPORTANT: You should use this approach whenever you want to work with binary payloads, @@ -280,6 +356,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response,indent=0] ... ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=http_dsl,indent=0] +---- ==== IMPORTANT: If you want to make your contract have a higher priority, @@ -306,6 +388,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request_obligatory,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=request,indent=0] +---- ==== You can specify an absolute rather than a relative `url`, but using `urlPath` is @@ -325,6 +413,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract_rest_with_path.yml[tags=url_path,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=url,indent=0] +---- ==== `request` may contain query parameters, as the following example (which uses `urlPath`) shows: @@ -343,6 +437,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i ... include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=query_params,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=urlpath,indent=0] +---- ==== `request` can contain additional request headers, as the following example shows: @@ -361,6 +461,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i ... include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=headers,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=headers,indent=0] +---- ==== `request` may contain additional request cookies, as the following example shows: @@ -379,6 +485,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i ... include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=cookies,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=cookies,indent=0] +---- ==== `request` may contain a request body, as the following example shows: @@ -397,6 +509,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i ... include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=body,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=body,indent=0] +---- ==== `request` can contain multipart elements. To include multipart elements, use the @@ -414,11 +532,17 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract_multipart.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_multipart.java[tags=class,indent=0] +---- ==== In the preceding example, we define parameters in either of two ways: -.Groovy DSL +.Coded DSL * Directly, by using the map notation, where the value can be a dynamic property (such as `formParameter: $(consumer(...), producer(...))`). * By using the `named(...)` method that lets you set a named parameter. A named parameter @@ -488,6 +612,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response, ... include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response_obligatory,indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=response,indent=0] +---- ==== Besides status, the response may contain headers, cookies, and a body, which are @@ -516,7 +646,7 @@ For YAML, you can use only the `matchers` section. [[contract-dsl-dynamic-properties-in-body]] ==== Dynamic Properties inside the Body -IMPORTANT: This section is valid only for the Groovy DSL. Check out the +IMPORTANT: This section is valid only for the Coded DSL (Groovy, Java etc.). Check out the <> section for YAML examples of a similar feature. You can set the properties inside the body either with the `value` method or, if you use @@ -564,14 +694,23 @@ There are several additional < The following example shows how to use regular expressions to write a request: -[source,groovy,indent=0] +==== +[source,groovy,indent=0,role="primary"] +.groovy ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=regex,indent=0] ---- +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=regex,indent=0] +---- +==== + You can also provide only one side of the communication with a regular expression. If you do so, then the contract engine automatically provides the generated string that matches -the provided regular expression. The following code shows an example: +the provided regular expression. The following code shows an example for Groovy: [source,groovy,indent=0] ---- @@ -589,7 +728,7 @@ use in your contracts, as the following example shows: include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexPatterns.java[tags=regexps,indent=0] ---- -In your contract, you can use it as follows: +In your contract, you can use it as follows (example for the Groovy DSL): [source,groovy,indent=0] ---- @@ -605,7 +744,7 @@ All of those methods start with the `any` prefix, as follows: include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexCreatingProperty.java[tags=regex_creating_props,indent=0] ---- -The following example shows how you can reference those methods: +The following example shows how you can reference those methods (example for Groovy DSL): [source,groovy,indent=0] ---- @@ -637,20 +776,32 @@ optional parameters only for the following: The following example shows how to provide optional parameters: -[source,groovy,indent=0] +==== +[source,groovy,indent=0,role="primary"] +.groovy ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals,indent=0] ---- +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=optionals,indent=0] +---- +==== + By wrapping a part of the body with the `optional()` method, you create a regular expression that must be present 0 or more times. If you use Spock, the following test would be generated from the previous example: +==== [source,groovy,indent=0] +.groovy ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals_test,indent=0] ---- +==== The following stub would also be generated: @@ -669,11 +820,21 @@ You can define a method call that runs on the server side during the test. Such method can be added to the class defined as `baseClassForTests` in the configuration. The following code shows an example of the contract portion of the test case: -[source,groovy,indent=0] + +==== +[source,groovy,indent=0,role="primary"] +.groovy ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=method,indent=0] ---- +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=method,indent=0] +---- +==== + The following code shows the base class portion of the test case: [source,groovy,indent=0] @@ -747,7 +908,7 @@ given name. * `fromRequest().body(String jsonPath)`: Returns the element from the request that matches the JSON Path. -If you use the YAML contract definition, you have to use the +If you use the YAML contract definition or the Java one, you have to use the https://handlebarsjs.com/[Handlebars] `{{{ }}}` notation with custom Spring Cloud Contract functions to achieve this. In that case, you can use the following options: @@ -777,6 +938,46 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract_reference_request.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +package contracts.beer.rest; + +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.map; + +class shouldReturnStatsForAUser implements Supplier { + + @Override + public Contract get() { + return Contract.make(c -> { + c.request(r -> { + r.method("POST"); + r.url("/stats"); + r.body(map().entry("name", r.anyAlphaUnicode())); + r.headers(h -> { + h.contentType(h.applicationJson()); + }); + }); + c.response(r -> { + r.status(r.OK()); + r.body(map() + .entry("text", + "Dear {{{jsonPath request.body '$.name'}}} thanks for your interested in drinking beer") + .entry("quantity", r.$(r.c(5), r.p(r.anyNumber())))); + r.headers(h -> { + h.contentType(h.applicationJson()); + }); + }); + }); + } + +} +---- ==== Running a JUnit test generation leads to a test that resembles the following example: @@ -896,7 +1097,7 @@ contract. Currently, Spring Cloud Contract Verifier supports only JSON path-based matchers with the following matching possibilities: -===== Groovy DSL +===== Coded DSL * For the stubs (in tests on the consumer's side): ** `byEquality()`: The value taken from the consumer's request in the provided JSON path must be @@ -1149,8 +1350,6 @@ As you can see, the assertion is malformed. Only the first element of the array asserted. In order to fix this, you should apply the assertion to the whole `$.events` collection and assert it with the `byCommand(...)` method. - - [[contract-dsl-async]] === Asynchronous Support @@ -1181,6 +1380,27 @@ org.springframework.cloud.contract.spec.Contract.make { response: async: true ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +class contract implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + // ... + }); + c.response(r -> { + r.async(); + // ... + }); + })); + } + +} +---- ==== You can also use the `fixedDelayMilliseconds` method or property to add delay to your stubs. @@ -1209,6 +1429,27 @@ org.springframework.cloud.contract.spec.Contract.make { response: fixedDelayMilliseconds: 1000 ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +class contract implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + // ... + }); + c.response(r -> { + r.fixedDelayMilliseconds(1000); + // ... + }); + })); + } + +} +---- ==== [[contract-dsl-xml]] @@ -1235,6 +1476,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ---- include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_xml.java[tags=class,indent=0] +---- ==== The following example shows an automatically generated test for XML in the response body: @@ -1286,6 +1533,28 @@ include::{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/mul ---- include::{verifier_core_path}/src/test/resources/yml/multiple_contracts.yml[indent=0] ---- + +[source,java,indent=0,subs="verbatim,attributes",role="secondary"] +.java +---- +class contract implements Supplier> { + + @Override + public Collection get() { + return Arrays.asList( + Contract.make(c -> { + c.name("should post a user"); + // ... + }), Contract.make(c -> { + // ... + }), Contract.make(c -> { + // ... + }) + ); + } + +} +---- ==== In the preceding example, one contract has the `name` field and the other does not. This @@ -1395,9 +1664,5 @@ name of `scenario1` and the three following steps: . showCart, marked as `Step1` pointing to... . logout, marked as `Step2` (which closes the scenario). -You can find nore details about WireMock scenarios at -https://wiremock.org/docs/stateful-behaviour/[https://wiremock.org/docs/stateful-behaviour/]. - -Spring Cloud Contract also generates tests with a guaranteed order of execution. -// TODO: How can someone specify the order of execution in SC Contract? -// That sentence is a great lead-in to more content, which then does not appear. +You can find more details about WireMock scenarios at +https://wiremock.org/docs/stateful-behaviour/[https://wiremock.org/docs/stateful-behaviour/]. \ No newline at end of file diff --git a/docs/src/main/asciidoc/gradle-project.adoc b/docs/src/main/asciidoc/gradle-project.adoc index 1854cd170d..60e5ac637b 100644 --- a/docs/src/main/asciidoc/gradle-project.adoc +++ b/docs/src/main/asciidoc/gradle-project.adoc @@ -195,7 +195,7 @@ contracts { testMode = 'MockMvc' generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts") generatedTestResourcesDir = project.file("${project.buildDir}/generated-test-resources/contracts") - contractsDslDir = "${project.rootDir}/src/test/resources/contracts" + contractsDslDir = project.file("${project.rootDir}/src/test/resources/contracts") basePackageForTests = 'org.springframework.cloud.verifier.tests' stubsOutputDir = project.file("${project.buildDir}/stubs") diff --git a/pom.xml b/pom.xml index 5b206b8ddd..5551814fd0 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ 2.2.0.BUILD-SNAPSHOT 2.2.0.BUILD-SNAPSHOT 2.2.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT 5.0.4 3.2.9 1.3-groovy-2.5 @@ -61,6 +62,7 @@ 2.11.0 0.8.4 1.6.0 + 1.3.0 @@ -88,6 +90,11 @@ pom import + + org.springframework.cloud + spring-cloud-function-compiler + ${spring-cloud-function.version} + org.apache.camel camel-spring-boot-starter diff --git a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/TestControllerTests.java b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/TestControllerTests.java new file mode 100644 index 0000000000..bba41b7822 --- /dev/null +++ b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/TestControllerTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2019 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 + * + * https://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 com.example.loan; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerPort; +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::autoconfigure_stubrunner[] +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = { + "com.example:http-server-dsl:+:stubs:6565"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL) +public class TestControllerTests { + // end::autoconfigure_stubrunner[] + + @StubRunnerPort("http-server-dsl") + int port; + + @Test + public void should_work_with_java_dsl() { + // when: + ResponseEntity response = new RestTemplate() + .getForEntity("http://localhost:" + this.port + "/example", String.class); + // then: + assertThat(response.getStatusCodeValue()).isEqualTo(200); + } + +} diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java index 3eeb280f55..5dd6e5f998 100644 --- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.util.Arrays; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -60,6 +61,10 @@ public class TestController { return new Test("ok"); } + @GetMapping("/example") + public void example() { + } + @PutMapping(value = "/1", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public byte[] response(@RequestBody byte[] requestBody) { if (!Arrays.equals(this.request, requestBody)) { diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/TestBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/TestBase.java new file mode 100644 index 0000000000..8ecf907049 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/TestBase.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2019 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 + * + * https://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 com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; + +public class TestBase { + + @Before + public void setUp() throws Exception { + RestAssuredMockMvc.standaloneSetup(new TestController()); + } + +} diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/test/MyHttpContract.java b/samples/standalone/dsl/http-server/src/test/resources/contracts/test/MyHttpContract.java new file mode 100644 index 0000000000..315470a7b7 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/test/MyHttpContract.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2019 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 + * + * https://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 test; + +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +class MyHttpContract implements Supplier { + @Override + public Contract get() { + return Contract.make(c -> { + c.request(r -> { + r.method(r.GET()); + r.url("/example"); + }); + c.response(r -> { + r.status(r.OK()); + }); + }); + } +} diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/test/request.json b/samples/standalone/dsl/http-server/src/test/resources/contracts/test/request.json new file mode 100644 index 0000000000..7cef5bbd05 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/test/request.json @@ -0,0 +1 @@ +{ "hello" : "world" } \ No newline at end of file diff --git a/spring-cloud-contract-stub-runner/src/test/resources/logback.xml b/spring-cloud-contract-stub-runner/src/test/resources/logback.xml index 9da197fb0d..3053f65510 100644 --- a/spring-cloud-contract-stub-runner/src/test/resources/logback.xml +++ b/spring-cloud-contract-stub-runner/src/test/resources/logback.xml @@ -19,6 +19,8 @@ + + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy index 52f4d53df3..65146b7d4b 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy @@ -59,12 +59,13 @@ class RecursiveFilesConverter { } void processFiles() { - ContractFileScanner scanner = new ContractFileScannerBuilder(). - baseDir(props.contractsDslDir) - .excluded(props.excludedFiles as Set) - .ignored([] as Set).included([] as Set) - .includeMatcher(props.includedContracts) - .build() + ContractFileScanner scanner = new ContractFileScannerBuilder() + .baseDir(props.contractsDslDir) + .excluded(props.excludedFiles as Set) + .ignored([] as Set) + .included([] as Set) + .includeMatcher(props.includedContracts) + .build() ListMultimap contracts = scanner.findContracts() if (log.isDebugEnabled()) { log.debug("Found the following contracts $contracts") diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 16ecdc00eb..cb56b670b3 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -177,10 +177,14 @@ spring-boot-starter-jersey test + + org.springframework.cloud + spring-cloud-function-compiler + org.mdkt.compiler InMemoryJavaCompiler - 1.2 + ${InMemoryJavaCompiler.version} test @@ -242,6 +246,24 @@ none:none + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-test-sources + + add-test-source + + + + src/test/resources/contractsToCompile + + + + + diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy index 0ef22f88d1..ee2933db8f 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy @@ -123,6 +123,9 @@ class TestGenerator { void generateTestClasses(final String basePackageName) { ListMultimap contracts = contractFileScanner. findContracts() + if (log.isDebugEnabled()) { + log.debug("Found the following contracts " + contracts.keySet()) + } Set>> inProgress = contracts.asMap().entrySet() .findAll { Map.Entry> entry -> entry.value.any { it.anyInProgress() }} if (!inProgress.isEmpty() && configProperties.failOnInProgress) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ToYamlConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ToYamlConverter.groovy index c22dc9b3f4..ce4591d536 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ToYamlConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ToYamlConverter.groovy @@ -41,6 +41,7 @@ class ToYamlConverter { } private static YamlContractConverter yamlContractConverter = new YamlContractConverter() + private static final List CONTRACT_CONVERTERS = converters() protected static void doReplaceContractWithYaml(ContractConverter converter, File file) { @@ -105,27 +106,7 @@ class ToYamlConverter { List converters = SpringFactoriesLoader.loadFactories(ContractConverter, null) converters.add(YamlContractConverter.INSTANCE) - converters.add(GroovyContractConverter.INSTANCE) + converters.add(ContractVerifierDslConverter.INSTANCE) return converters } } - -class GroovyContractConverter implements ContractConverter> { - - static final GroovyContractConverter INSTANCE = new GroovyContractConverter() - - @Override - boolean isAccepted(File file) { - return file.name.endsWith(".groovy") - } - - @Override - Collection convertFrom(File file) { - return ContractVerifierDslConverter.convertAsCollection(file) - } - - @Override - Collection convertTo(Collection contract) { - return contract - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy index 0658d2debd..91fa7b68ca 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy @@ -139,6 +139,7 @@ class ContractFileScanner { protected List convertersWithYml() { List converters = converters() + converters.add(ContractVerifierDslConverter.INSTANCE) converters.add(YamlContractConverter.INSTANCE) return converters } @@ -213,22 +214,7 @@ class ContractFileScanner { } private boolean isContractFile(File file) { - return file.isFile() && getFilenameExtension(file.toString())?.equalsIgnoreCase("groovy") - } - - private static String getFilenameExtension(String path) { - if (path == null) { - return null - } - int extIndex = path.lastIndexOf('.') - if (extIndex == -1) { - return null - } - int folderIndex = path.lastIndexOf('/') - if (folderIndex > extIndex) { - return null - } - return path.substring(extIndex + 1) + return file.isFile() && ContractVerifierDslConverter.INSTANCE.isAccepted(file) } /** diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy index f69c5735f8..103594ad4f 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy @@ -85,7 +85,7 @@ final class ContractScanner { contractDescriptors .addAll(ContractVerifierDslConverter .convertAsCollection( - file.getParentFile(), file)) + file.getParentFile(), file).findAll { it }) } else if (converter != null && converter.isAccepted(file)) { @@ -120,6 +120,6 @@ final class ContractScanner { } private static boolean isContractDescriptor(File file) { - return file.isFile() && file.getName().endsWith(".groovy") + return ContractVerifierDslConverter.INSTANCE.isAccepted(file) } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy index 3fa8dad012..f5590a8159 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy @@ -16,16 +16,23 @@ package org.springframework.cloud.contract.verifier.util +import java.lang.reflect.Constructor +import java.util.function.Supplier +import java.util.regex.Matcher +import java.util.regex.Pattern + import groovy.transform.CompileStatic import groovy.util.logging.Commons import org.codehaus.groovy.control.CompilerConfiguration -import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.cloud.function.compiler.java.CompilationResult +import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler import org.springframework.util.StringUtils /** - * Converts a file or String into a {@link Contract} + * Converts a String or a Groovy or Java file into a {@link Contract}. * * @author Marcin Grzejszczak * @@ -33,7 +40,15 @@ import org.springframework.util.StringUtils */ @CompileStatic @Commons -class ContractVerifierDslConverter { +class ContractVerifierDslConverter implements ContractConverter> { + + public static final ContractVerifierDslConverter INSTANCE = new ContractVerifierDslConverter() + + private static final Pattern PACKAGE_PATTERN = Pattern.compile(".+?package (.+?);.+?", Pattern.DOTALL) + + private static final Pattern CLASS_PATTERN = Pattern.compile(".+?class (.+?)( |\\{).+?", Pattern.DOTALL) + + private static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler() /** * @deprecated - use {@link ContractVerifierDslConverter#convertAsCollection(java.io.File, java.lang.String)} @@ -80,7 +95,7 @@ class ContractVerifierDslConverter { ClassLoader classLoader = ContractVerifierDslConverter.getClassLoader() try { ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader) - Object object = groovyShell(urlCl, rootFolder).evaluate(dsl) + Object object = toObject(urlCl, rootFolder, dsl) return listOfContracts(dsl, object) } catch (DslParseException e) { @@ -98,14 +113,80 @@ class ContractVerifierDslConverter { private static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { ClassLoader urlCl = URLClassLoader .newInstance([rootFolder.toURI().toURL()] as URL[], classLoader) - Thread.currentThread().setContextClassLoader(urlCl) + updateTheThreadClassLoader(urlCl) return urlCl } + private static void updateTheThreadClassLoader(ClassLoader urlCl) { + Thread.currentThread().setContextClassLoader(urlCl) + } + private static GroovyShell groovyShell() { return new GroovyShell(ContractVerifierDslConverter.classLoader, new CompilerConfiguration(sourceEncoding: 'UTF-8')) } + private static Object toObject(ClassLoader cl, File rootFolder, File dsl) { + if (isJava(dsl)) { + try { + return parseJavaFile(cl, dsl) + } + catch (Exception ex) { + if (log.isWarnEnabled()) { + log.warn("Exception occurred while trying to parse the file [" + dsl + "] as a contract. Will not parse it.", ex) + } + return null + } + } + return groovyShell(cl, rootFolder).evaluate(dsl) + } + + private static Object parseJavaFile(ClassLoader cl, File dsl) { + Constructor constructor = classConstructor(dsl) + Object newInstance = constructor.newInstance() + if (!newInstance instanceof Supplier) { + if (log.isDebugEnabled()) { + log.debug("The class [" + dsl + "] is not instance of Supplier. Will not parse it as a contract") + } + return null + } + Supplier supplier = (Supplier) newInstance + return supplier.get() + } + + private static Constructor classConstructor(File dsl) { + String classText = dsl.text + String fqn = fqn(classText) + CompilationResult compilationResult = COMPILER + .compile(fqn, classText) + if (!compilationResult.wasSuccessful()) { + throw new IllegalStateException("Exceptions occurred while trying to compile the file " + compilationResult.compilationMessages) + } + Class clazz = compilationResult.compiledClasses.find { it.name == fqn} + if (clazz == null) { + throw new IllegalStateException("Class with name [" + fqn + "] not found") + } + Constructor constructor = clazz.getDeclaredConstructor() + constructor.setAccessible(true) + return constructor + } + + private static boolean isJava(File dsl) { + return dsl.name.endsWith(".java") + } + + private static String fqn(String classText) { + Matcher packageMatcher = PACKAGE_PATTERN.matcher(classText) + String fqn = ""; + if (packageMatcher.matches()) { + fqn = packageMatcher.group(1) + "." + } + Matcher classMatcher = CLASS_PATTERN.matcher(classText) + if (!classMatcher.matches()) { + throw new IllegalAccessException("Can't parse the class name") + } + return fqn + classMatcher.group(1) + } + private static GroovyShell groovyShell(ClassLoader cl, File rootFolder) { return new GroovyShell(cl, new CompilerConfiguration(sourceEncoding: 'UTF-8', @@ -123,7 +204,10 @@ class ContractVerifierDslConverter { } private static Collection listOfContracts(File file, Object object) { - if (object instanceof Collection) { + if (object == null) { + return Collections.emptyList() + } + else if (isACollectionOfContracts(object)) { return withName(file, object as Collection) } else if (!object instanceof Contract) { @@ -132,6 +216,10 @@ class ContractVerifierDslConverter { return withName(file, [object] as Collection) } + private static boolean isACollectionOfContracts(object) { + return object instanceof Collection && ((Collection) object).every { it instanceof Contract } + } + private static Collection withName(File file, Collection contracts) { int counter = 0 return contracts.collect { @@ -146,4 +234,19 @@ class ContractVerifierDslConverter { private static boolean contractNameEmpty(Contract it) { return it != null && StringUtils.isEmpty(it.name) } + + @Override + boolean isAccepted(File file) { + return file.name.endsWith(".groovy") || file.name.endsWith(".gvy") || file.name.endsWith(".java") + } + + @Override + Collection convertFrom(File file) { + return convertAsCollection(file) + } + + @Override + Collection convertTo(Collection contract) { + return contract + } } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java index 8b8ae12c79..6ea7fcf107 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java @@ -22,6 +22,10 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; @@ -105,4 +109,95 @@ public final class ContractVerifierUtil { } } + /** + * @return a builder for map + */ + public static ContractVerifierMap map() { + return new ContractVerifierMap(); + } + + /** + * A map with a fluent interface. + */ + public static class ContractVerifierMap implements Map { + + private final Map delegate = new HashMap<>(); + + public ContractVerifierMap entry(String key, Object value) { + put(key, value); + return this; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.delegate.containsValue(value); + } + + @Override + public Object get(Object key) { + return this.delegate.get(key); + } + + @Override + public Object put(Object key, Object value) { + return this.delegate.put(key, value); + } + + @Override + public Object remove(Object key) { + return this.delegate.remove(key); + } + + @Override + public void putAll(Map m) { + this.delegate.putAll(m); + } + + @Override + public void clear() { + this.delegate.clear(); + } + + @Override + public Set keySet() { + return this.delegate.keySet(); + } + + @Override + public Collection values() { + return this.delegate.values(); + } + + @Override + public Set> entrySet() { + return this.delegate.entrySet(); + } + + @Override + public boolean equals(Object o) { + return this.delegate.equals(o); + } + + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + } + } 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 1565ef0899..3a00fabf70 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 @@ -17,6 +17,7 @@ package org.springframework.cloud.contract.verifier.builder import org.junit.Rule +import org.mdkt.compiler.CompilationException import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification @@ -237,10 +238,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements try { SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) } - catch (ClassFormatError classFormatError) { - String output = outputCapture.toString() - assert output.contains('error: cannot find symbol') - assert output.contains('assertThatValueIsANumber(parsedJson.read("$.duck"));') + catch (CompilationException classFormatError) { + String output = classFormatError.message + assert output.contains('cannot find symbol') + assert output.contains('assertThatValueIsANumber') } where: methodBuilderName | methodBuilder | rootElement diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/JavaContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/JavaContractConverterSpec.groovy new file mode 100644 index 0000000000..a6f5b6b520 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/JavaContractConverterSpec.groovy @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2019 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 + * + * https://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.converter + +import groovy.json.JsonSlurper +import spock.lang.Shared +import spock.lang.Specification + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.FromFileProperty +import org.springframework.cloud.contract.spec.internal.RegexProperty +import org.springframework.cloud.contract.spec.internal.Url +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter +import org.springframework.cloud.contract.verifier.util.MapConverter + +import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND +import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL +import static org.springframework.cloud.contract.spec.internal.MatchingType.REGEX +/** + * @author Marcin Grzejszczak + */ +class JavaContractConverterSpec extends Specification { + + @Shared + URL javaRest = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest.java") + @Shared + File javaRestFile = new File(javaRest.toURI()) + + @Shared + URL javaRestWithTags = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_with_tags.java") + @Shared + File javaRestWithTagsFile = new File(javaRestWithTags.toURI()) + + @Shared + URL contractBody = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_file.java") + @Shared + File contractBodyFile = new File(contractBody.toURI()) + + @Shared + URL contractBodyBytes = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_pdf.java") + @Shared + File contractBodyBytesFile = new File(contractBodyBytes.toURI()) + + @Shared + URL docs = JavaContractConverterSpec.getResource("/contractsToCompile/contract_docs_examples.java") + @Shared + File docsFile = new File(docs.toURI()) + + def "should convert Java DSL with REST to DSL for [#contractFile]"() { + when: + Collection contracts = ContractVerifierDslConverter.convertAsCollection(contractFile.parentFile, contractFile) + then: + Contract contract = contracts.first() + contract.description == "Some description" + contract.name == "some name" + contract.priority == 8 + contract.ignored == true + Url url = contract.request.url + url.clientValue == "/foo" + url.queryParameters.parameters[0].name == "a" + url.queryParameters.parameters[0].serverValue == "b" + url.queryParameters.parameters[1].name == "b" + url.queryParameters.parameters[1].serverValue == "c" + contract.request.method.clientValue == "PUT" + contract.request.headers.entries.find { + it.name == "foo" && + ((RegexProperty) it.clientValue).pattern() == "bar" && it.serverValue == "bar" + } + contract.request.headers.entries.find { + it.name == "fooReq" && + it.serverValue == "baz" + } + MapConverter.getStubSideValues(contract.request.body) == [foo: "bar"] + contract.request.bodyMatchers.matchers[0].path() == '$.foo' + contract.request.bodyMatchers.matchers[0].matchingType() == REGEX + contract.request.bodyMatchers.matchers[0].value().pattern() == 'bar' + and: + contract.response.status.clientValue == 200 + contract.response.delay.clientValue == 1000 + contract.response.headers.entries.find { + it.name == "foo2" && + ((RegexProperty) it.serverValue).pattern() == "bar" && it.clientValue == "bar" + } + contract.response.headers.entries.find { + it.name == "foo3" && + ((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)" + } + contract.response.headers.entries.find { + it.name == "fooRes" && + it.clientValue == "baz" + } + MapConverter.getStubSideValues(contract.response.body) == [foo2: "bar", foo3: "baz", nullValue: null] + contract.response.bodyMatchers.matchers[0].path() == '$.foo2' + contract.response.bodyMatchers.matchers[0].matchingType() == REGEX + contract.response.bodyMatchers.matchers[0].value().pattern() == 'bar' + contract.response.bodyMatchers.matchers[1].path() == '$.foo3' + contract.response.bodyMatchers.matchers[1].matchingType() == COMMAND + contract.response.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)') + contract.response.bodyMatchers.matchers[2].path() == '$.nullValue' + contract.response.bodyMatchers.matchers[2].matchingType() == NULL + contract.response.bodyMatchers.matchers[2].value() == null + where: + contractFile << [javaRestFile, javaRestWithTagsFile] + } + + def "should convert java with REST with body from file"() { + when: + Collection contracts = ContractVerifierDslConverter.convertAsCollection(contractBodyFile.parentFile, contractBodyFile) + then: + contracts.size() == 1 + Contract contract = contracts.first() + new JsonSlurper().parseText(contract.request.body.clientValue.toString()) == + new JsonSlurper().parseText('''{ "hello" : "request" }''') + and: + new JsonSlurper().parseText(contract.response.body.clientValue.toString()) == + new JsonSlurper().parseText('''{ "hello" : "response" }''') + } + + def "should convert java with REST with body as bytes"() { + when: + Collection contracts = ContractVerifierDslConverter.convertAsCollection(contractBodyBytesFile.parentFile, contractBodyBytesFile) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.request.body.clientValue instanceof FromFileProperty + and: + contract.response.body.clientValue instanceof FromFileProperty + } + + def "should convert java with REST for docs"() { + when: + Collection contracts = ContractVerifierDslConverter.convertAsCollection(docsFile.parentFile, docsFile) + then: + contracts.size() == 1 + } +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverterSpec.groovy index 5b99ae7439..6181745733 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverterSpec.groovy @@ -27,6 +27,10 @@ class ContractVerifierDslConverterSpec extends Specification { URL single = ContractVerifierDslConverterSpec.getResource("/contract.groovy") File singleContract = new File(single.toURI()) + URL singleJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract.java") + File singleContractJava = new File(singleJava.toURI()) + URL singleRestJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract_rest.java") + File singleContractRestJava = new File(singleRestJava.toURI()) URL multiple = ContractVerifierDslConverterSpec.getResource("/multiple_contracts.groovy") File multipleContracts = new File(multiple.toURI()) URL invalid = ContractVerifierDslConverterSpec.getResource("/contract.yml") @@ -51,6 +55,47 @@ class ContractVerifierDslConverterSpec extends Specification { } } + Contract expectedSingleContractForJava = Contract.make( { + description("Some description"); + name("some name"); + priority(8); + ignored(); + request( { + url("/foo", { + queryParameters({ + parameter("a", "b"); + parameter("b", "c"); + }); + }); + method(PUT()); + headers( { + header("foo", value(client(regex("bar")), server("bar"))); + header("fooReq", "baz"); + }); + body(ContractVerifierUtil.map().entry("foo", "bar")); + bodyMatchers( { + jsonPath("\$.foo", byRegex("bar")); + }); + }); + response( { + fixedDelayMilliseconds(1000); + status(OK()); + headers( { + header("foo2", value(server(regex("bar")), client("bar"))); + header("foo3", value(server(execute("andMeToo(\$it)")), + client("foo33"))); + header("fooRes", "baz"); + }); + body(ContractVerifierUtil.map().entry("foo2", "bar") + .entry("foo3", "baz").entry("nullValue", null)); + bodyMatchers( { + jsonPath("\$.foo2", byRegex("bar")); + jsonPath("\$.foo3", byCommand("executeMe(\$it)")); + jsonPath("\$.nullValue", byNull()); + }); + }); + }) + Contract expectedSingleContractForText = Contract.make { request { method('PUT') @@ -154,6 +199,20 @@ class ContractVerifierDslConverterSpec extends Specification { contract == [expectedSingleContract] } + def "should convert file to a list of Contracts when there's only one declared java contract"() { + when: + Collection contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractJava) + then: + contract == [expectedSingleContract] + } + + def "should convert file to a list of Contracts for a REST contract for docs"() { + when: + Collection contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractRestJava) + then: + contract == [expectedSingleContractForJava] + } + def "should convert text to a list of Contracts when there's only one declared contract"() { when: Collection contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContract.text) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy index 2b33695997..63ca02dd59 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy @@ -45,6 +45,7 @@ import org.springframework.cloud.contract.verifier.messaging.internal.ContractVe import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper import org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil +import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler import org.springframework.util.ReflectionUtils /** * checking the syntax of produced scripts @@ -53,7 +54,7 @@ import org.springframework.util.ReflectionUtils @Commons class SyntaxChecker { - Entity entity + public static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler() private static final String[] DEFAULT_IMPORTS = [ Contract.name, @@ -179,20 +180,26 @@ private void test(String test) { String fqnClassName = "com.example.${className}" test = test.replaceAll("class FooTest", "class " + className) .replaceAll("import javax.ws.rs.core.Response", "import javax.ws.rs.core.Response; import javax.ws.rs.client.WebTarget;") - return InMemoryJavaCompiler.compile(fqnClassName, test) + return compileJava(fqnClassName, test) + + } + + private static Class compileJava(String fqnClassName, String test) { + return InMemoryJavaCompiler.newInstance() + .ignoreWarnings() + .compile(fqnClassName, test) } private static String className(String test) { Random random = new Random() int first = Math.abs(random.nextInt()) int hashCode = Math.abs(test.hashCode()) - StringBuffer sourceCode = new StringBuffer() String className = "TestClass_${first}_${hashCode}" return className } static boolean tryToCompileJavaWithoutImports(String fqn, String test) { - InMemoryJavaCompiler.compile(fqn, test) + compileJava(fqn, test) return true } diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract.java new file mode 100644 index 0000000000..36868e5c00 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2019 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 + * + * https://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 example; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +class contract implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + r.method(r.PUT()); + r.headers(h -> { + h.contentType(h.applicationJson()); + }); + r.body(" { \"status\" : \"OK\" } "); + r.url("/1"); + }); + c.response(r -> { + r.status(r.OK()); + r.body(" { \"status\" : \"OK\" } "); + r.headers(h -> { + h.contentType(h.textPlain()); + }); + }); + })); + } + +} diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_docs_examples.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_docs_examples.java new file mode 100644 index 0000000000..7213f08f32 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_docs_examples.java @@ -0,0 +1,354 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil; + +class contract_docs_examples implements Supplier> { + + org.springframework.cloud.contract.spec.Contract httpDsl = + // tag::http_dsl[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + // Definition of HTTP request part of the contract + // (this can be a valid request or invalid depending + // on type of contract being specified). + c.request(r -> { + r.method(r.GET()); + r.url("/foo"); + // ... + }); + + // Definition of HTTP response part of the contract + // (a service implementing this contract should respond + // with following response after receiving request + // specified in "request" part above). + c.response(r -> { + r.status(200); + // ... + }); + + // Contract priority, which can be used for overriding + // contracts (1 is highest). Priority is optional. + c.priority(1); + }); + + // end::http_dsl[] + + org.springframework.cloud.contract.spec.Contract methodDsl = org.springframework.cloud.contract.spec.Contract + .make(c -> { + c.request(r -> { + // tag::method[] + r.method(r.GET()); + // end::method[] + r.url("/foo"); + }); + + c.response(r -> { + r.status(200); + }); + + c.priority(1); + }); + + org.springframework.cloud.contract.spec.Contract request = + // tag::request[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // HTTP request method (GET/POST/PUT/DELETE). + r.method("GET"); + + // Path component of request URL is specified as follows. + r.urlPath("/users"); + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::request[] + + org.springframework.cloud.contract.spec.Contract url = + // tag::url[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + r.method("GET"); + + // Specifying `url` and `urlPath` in one contract is illegal. + r.url("http://localhost:8888/users"); + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::url[] + + org.springframework.cloud.contract.spec.Contract urlPaths = + // tag::urlpath[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + + r.urlPath("/users", u -> { + + // Each parameter is specified in form + // `'paramName' : paramValue` where parameter value + // may be a simple literal or one of matcher functions, + // all of which are used in this example. + u.queryParameters(q -> { + + // If a simple literal is used as value + // default matcher function is used (equalTo) + q.parameter("limit", 100); + + // `equalTo` function simply compares passed value + // using identity operator (==). + q.parameter("filter", r.equalTo("email")); + + // `containing` function matches strings + // that contains passed substring. + q.parameter("gender", + r.value(r.consumer(r.containing("[mf]")), + r.producer("mf"))); + + // `matching` function tests parameter + // against passed regular expression. + q.parameter("offset", + r.value(r.consumer(r.matching("[0-9]+")), + r.producer(123))); + + // `notMatching` functions tests if parameter + // does not match passed regular expression. + q.parameter("loginStartsWith", + r.value(r.consumer(r.notMatching(".{0,2}")), + r.producer(3))); + }); + }); + + // ... + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::urlpath[] + + org.springframework.cloud.contract.spec.Contract headers = + // tag::headers[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + r.url("/foo"); + + // Each header is added in form `'Header-Name' : 'Header-Value'`. + // there are also some helper methods + r.headers(h -> { + h.header("key", "value"); + h.contentType(h.applicationJson()); + }); + + // ... + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::headers[] + + org.springframework.cloud.contract.spec.Contract cookies = + // tag::cookies[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + r.url("/foo"); + + // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`. + // there are also some helper methods + r.cookies(ck -> { + ck.cookie("key", "value"); + ck.cookie("another_key", "another_value"); + }); + + // ... + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::cookies[] + + org.springframework.cloud.contract.spec.Contract body = + // tag::body[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + r.url("/foo"); + + // Currently only JSON format of request body is supported. + // Format will be determined from a header or body's content. + r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }"); + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::body[] + + org.springframework.cloud.contract.spec.Contract bodyAsXml = + // tag::bodyAsXml[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + r.url("/foo"); + + // In this case body will be formatted as XML. + r.body(r.equalToXml( + "johnJohn The Contract")); + }); + + c.response(r -> { + // ... + r.status(200); + }); + }); + + // end::bodyAsXml[] + + org.springframework.cloud.contract.spec.Contract response = + // tag::response[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + // ... + r.method(r.GET()); + r.url("/foo"); + }); + c.response(r -> { + // Status code sent by the server + // in response to request specified above. + r.status(r.OK()); + }); + }); + + // end::response[] + + org.springframework.cloud.contract.spec.Contract regex = + // tag::regex[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + r.method("GET"); + r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12"))); + }); + c.response(r -> { + r.status(r.OK()); + r.body(ContractVerifierUtil.map().entry("id", r.$(r.anyNumber())) + .entry("surname", r.$(r.consumer("Kowalsky"), + r.producer(r.regex("[a-zA-Z]+"))))); + r.headers(h -> { + h.header("Content-Type", "text/plain"); + }); + }); + }); + + // end::regex[] + + org.springframework.cloud.contract.spec.Contract optionals = + // tag::optionals[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.priority(1); + c.name("optionals"); + c.request(r -> { + r.method("POST"); + r.url("/users/password"); + r.headers(h -> { + h.contentType(h.applicationJson()); + }); + r.body(ContractVerifierUtil.map() + .entry("email", + r.$(r.consumer(r.optional(r.regex(r.email()))), + r.producer("abc@abc.com"))) + .entry("callback_url", r.$(r.consumer(r.regex(r.hostname())), + r.producer("https://partners.com")))); + }); + c.response(r -> { + r.status(404); + r.headers(h -> { + h.header("Content-Type", "application/json"); + }); + r.body(ContractVerifierUtil.map().entry("code", r.value( + r.consumer("123123"), r.producer(r.optional("123123"))))); + }); + }); + + // end::optionals[] + + org.springframework.cloud.contract.spec.Contract method = + // tag::methodBuilder[] + org.springframework.cloud.contract.spec.Contract.make(c -> { + c.request(r -> { + r.method("PUT"); + r.url(r.$(r.consumer(r.regex("^/api/[0-9]{2}$")), + r.producer("/api/12"))); + r.headers(h -> { + h.header("Content-Type", "application/json"); + }); + r.body("[{\"text\": \"Gonna see you at Warsaw\" }]"); + }); + c.response(r -> { + r.body(ContractVerifierUtil.map() + .entry("path", + r.$(r.consumer("/api/12"), + r.producer(r.regex("^/api/[0-9]{2}$")))) + .entry("correlationId", r.$(r.consumer("1223456"), r + .producer(r.execute("isProperCorrelationId($it)"))))); + r.status(r.OK()); + }); + }); + + // end::methodBuilder[] + + @Override + public Collection get() { + return Collections.singletonList(method); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_multipart.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_multipart.java new file mode 100644 index 0000000000..f3985893dd --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_multipart.java @@ -0,0 +1,71 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil; + +class contract_multipart implements Supplier> { + + private static Map namedProps(Request r) { + Map map = new HashMap<>(); + // name of the file + map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv"))); + // content of the file + map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content"))); + // content type for the part + map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json"))); + return map; + } + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + r.method("PUT"); + r.url("/multipart"); + r.headers(h -> { + h.contentType("multipart/form-data;boundary=AaB03x"); + }); + r.multipart(ContractVerifierUtil.map() + // key (parameter name), value (parameter value) pair + .entry("formParameter", + r.$(r.c(r.regex("\".+\"")), + r.p("\"formParameterValue\""))) + .entry("someBooleanParameter", + r.$(r.c(r.regex(r.anyBoolean())), r.p("true"))) + // a named parameter (e.g. with `file` name) that represents file + // with + // `name` and `content`. You can also call `named("fileName", + // "fileContent")` + .entry("file", r.named(namedProps(r)))); + }); + c.response(r -> { + r.status(r.OK()); + }); + })); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest.java new file mode 100644 index 0000000000..a9745656b2 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest.java @@ -0,0 +1,72 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil; + +class contract_rest implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.description("Some description"); + c.name("some name"); + c.priority(8); + c.ignored(); + c.request(r -> { + r.url("/foo", u -> { + u.queryParameters(q -> { + q.parameter("a", "b"); + q.parameter("b", "c"); + }); + }); + r.method(r.PUT()); + r.headers(h -> { + h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar"))); + h.header("fooReq", "baz"); + }); + r.body(ContractVerifierUtil.map().entry("foo", "bar")); + r.bodyMatchers(m -> { + m.jsonPath("$.foo", m.byRegex("bar")); + }); + }); + c.response(r -> { + r.fixedDelayMilliseconds(1000); + r.status(r.OK()); + r.headers(h -> { + h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar"))); + h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")), + r.client("foo33"))); + h.header("fooRes", "baz"); + }); + r.body(ContractVerifierUtil.map().entry("foo2", "bar") + .entry("foo3", "baz").entry("nullValue", null)); + r.bodyMatchers(m -> { + m.jsonPath("$.foo2", m.byRegex("bar")); + m.jsonPath("$.foo3", m.byCommand("executeMe($it)")); + m.jsonPath("$.nullValue", m.byNull()); + }); + }); + })); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_file.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_file.java new file mode 100644 index 0000000000..addea24703 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_file.java @@ -0,0 +1,42 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +class contract_rest_from_file implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + r.url("/foo"); + r.method(r.GET()); + r.body(r.file("request.json")); + }); + c.response(r -> { + r.status(r.OK()); + r.body(r.file("response.json")); + }); + })); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_pdf.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_pdf.java new file mode 100644 index 0000000000..df507556e0 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_pdf.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +//tag::class[] +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +class contract_rest_from_pdf implements Supplier> { + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.request(r -> { + r.url("/1"); + r.method(r.PUT()); + r.body(r.fileAsBytes("request.pdf")); + r.headers(h -> { + h.contentType(h.applicationOctetStream()); + }); + }); + c.response(r -> { + r.status(r.OK()); + r.body(r.fileAsBytes("response.pdf")); + r.headers(h -> { + h.contentType(h.applicationOctetStream()); + }); + }); + })); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java new file mode 100644 index 0000000000..6d975f113a --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java @@ -0,0 +1,105 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil; + +class contract_rest_with_tags implements Supplier> { + + static Object description = Collections.singletonList( + // tag::description[] + Contract.make(c -> { + c.description("Some description"); + })); + + // end::description[] + + static Object name = Collections.singletonList( + // tag::name[] + Contract.make(c -> { + c.name("some name"); + })); + + // end::name[] + + static Object ignored = Collections.singletonList( + // tag::ignored[] + Contract.make(c -> { + c.ignored(); + })); + + // end::ignored[] + + static Object in_progress = Collections.singletonList( + // tag::in_progress[] + Contract.make(c -> { + c.inProgress(); + })); + + // end::in_progress[] + + @Override + public Collection get() { + return Collections.singletonList(Contract.make(c -> { + c.description("Some description"); + c.name("some name"); + c.priority(8); + c.ignored(); + c.inProgress(); + c.request(r -> { + r.url("/foo", u -> { + u.queryParameters(q -> { + q.parameter("a", "b"); + q.parameter("b", "c"); + }); + }); + r.method(r.PUT()); + r.headers(h -> { + h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar"))); + h.header("fooReq", "baz"); + }); + r.body(ContractVerifierUtil.map().entry("foo", "bar")); + r.bodyMatchers(m -> { + m.jsonPath("$.foo", m.byRegex("bar")); + }); + }); + c.response(r -> { + r.fixedDelayMilliseconds(1000); + r.status(r.OK()); + r.headers(h -> { + h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar"))); + h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")), + r.client("foo33"))); + h.header("fooRes", "baz"); + }); + r.body(ContractVerifierUtil.map().entry("foo2", "bar") + .entry("foo3", "baz").entry("nullValue", null)); + r.bodyMatchers(m -> { + m.jsonPath("$.foo2", m.byRegex("bar")); + m.jsonPath("$.foo3", m.byCommand("executeMe($it)")); + m.jsonPath("$.nullValue", m.byNull()); + }); + }); + })); + } + +} +// end::class[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_xml.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_xml.java new file mode 100644 index 0000000000..319a6d2e65 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_xml.java @@ -0,0 +1,66 @@ + +/* + * Copyright 2013-2019 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 + * + * https://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. + */ + +import java.util.function.Supplier; + +import org.springframework.cloud.contract.spec.Contract; + +class contract_xml implements Supplier { + + @Override + public Contract get() { + return Contract.make(c -> { + c.request(r -> { + r.method(r.GET()); + r.urlPath("/get"); + r.headers(h -> { + h.contentType(h.applicationXml()); + }); + }); + c.response(r -> { + r.status(r.OK()); + r.headers(h -> { + h.contentType(h.applicationXml()); + }); + r.body("\n" + "123\n" + + "abc\n" + "\n" + "abc\n" + + "def\n" + "ghi\n" + "\n" + + "123\n" + "true\n" + + "2017-01-01\n" + + "2017-01-01T01:23:45\n" + + "\n" + + "foo\n" + + "foo\n" + ""); + r.bodyMatchers(m -> { + m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}")); + m.xPath("/test/duck/text()", m.byCommand("equals($it)")); + m.xPath("/test/duck/xxx", m.byNull()); + m.xPath("/test/duck/text()", m.byEquality()); + m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode())); + m.xPath("/test/alpha/text()", m.byEquality()); + m.xPath("/test/number/text()", m.byRegex(r.number())); + m.xPath("/test/date/text()", m.byDate()); + m.xPath("/test/dateTime/text()", m.byTimestamp()); + m.xPath("/test/time/text()", m.byTime()); + m.xPath("/test/*/complex/text()", m.byEquality()); + m.xPath("/test/duck/@type", m.byEquality()); + }); + }); + }); + }; + +} diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.json b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.json new file mode 100644 index 0000000000..55f5fd83a8 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.json @@ -0,0 +1,3 @@ +{ + "hello": "request" +} diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.pdf b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.pdf new file mode 100644 index 0000000000..69503635d8 Binary files /dev/null and b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.pdf differ diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.json b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.json new file mode 100644 index 0000000000..b52d1df636 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.json @@ -0,0 +1,3 @@ +{ + "hello": "response" +} diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.pdf b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.pdf new file mode 100644 index 0000000000..c3b233056b Binary files /dev/null and b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.pdf differ diff --git a/spring-cloud-contract-verifier/src/test/resources/logback.xml b/spring-cloud-contract-verifier/src/test/resources/logback.xml index 1690956653..38fb9e2a71 100644 --- a/spring-cloud-contract-verifier/src/test/resources/logback.xml +++ b/spring-cloud-contract-verifier/src/test/resources/logback.xml @@ -9,6 +9,8 @@ + + diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index fa5679883e..3a3d788db5 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -45,5 +45,6 @@ +