From 706044bfe53a162da963655cf9f500ea55225fa3 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 12 Aug 2019 13:40:23 +0200 Subject: [PATCH] Added support for writing contracts in Java fixes gh-1161 --- .../asciidoc/_project-features-contract.adoc | 333 +++++++++++++++-- pom.xml | 7 + .../com/example/loan/TestControllerTests.java | 53 +++ .../com/example/fraud/TestController.java | 5 + .../test/java/com/example/fraud/TestBase.java | 29 ++ .../contracts/test/MyHttpContract.java | 36 ++ .../src/test/resources/logback.xml | 2 + .../converter/RecursiveFilesConverter.groovy | 13 +- spring-cloud-contract-verifier/pom.xml | 24 +- .../contract/verifier/TestGenerator.groovy | 3 + .../verifier/converter/ToYamlConverter.groovy | 23 +- .../verifier/file/ContractFileScanner.groovy | 18 +- .../verifier/util/ContractScanner.groovy | 4 +- .../util/ContractVerifierDslConverter.groovy | 106 +++++- .../verifier/util/ContractVerifierUtil.java | 95 +++++ ...vcMethodBodyBuilderWithMatchersSpec.groovy | 9 +- .../JavaContractConverterSpec.groovy | 151 ++++++++ .../ContractVerifierDslConverterSpec.groovy | 37 ++ .../verifier/util/SyntaxChecker.groovy | 15 +- .../contractsToCompile/contract.java | 47 +++ .../contract_docs_examples.java | 351 ++++++++++++++++++ .../contract_multipart.java | 71 ++++ .../contractsToCompile/contract_rest.java | 71 ++++ .../contract_rest_from_file.java | 41 ++ .../contract_rest_from_pdf.java | 48 +++ .../contract_rest_with_tags.java | 104 ++++++ .../contractsToCompile/contract_xml.java | 66 ++++ .../resources/contractsToCompile/request.json | 3 + .../resources/contractsToCompile/request.pdf | Bin 0 -> 14631 bytes .../contractsToCompile/response.json | 3 + .../resources/contractsToCompile/response.pdf | Bin 0 -> 15767 bytes .../src/test/resources/logback.xml | 2 + 32 files changed, 1677 insertions(+), 93 deletions(-) create mode 100644 samples/standalone/dsl/http-client/src/test/java/com/example/loan/TestControllerTests.java create mode 100644 samples/standalone/dsl/http-server/src/test/java/com/example/fraud/TestBase.java create mode 100644 samples/standalone/dsl/http-server/src/test/resources/contracts/test/MyHttpContract.java create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/JavaContractConverterSpec.groovy create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_docs_examples.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_multipart.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_file.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_pdf.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_xml.java create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.json create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/request.pdf create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.json create mode 100644 spring-cloud-contract-verifier/src/test/resources/contractsToCompile/response.pdf 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/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/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..507977b4ec 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) { @@ -106,6 +121,65 @@ class ContractVerifierDslConverter { 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(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(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} + 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 +197,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 +209,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 +227,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..cedc826bf9 --- /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(new File("/"), 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(new File("/"), 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(new File("/"), 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(new File("/"), 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..2da91cf496 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,25 @@ class ContractVerifierDslConverterSpec extends Specification { } } + Contract expectedSingleContractForJava = Contract.make { + name("contract") + request { + method('PUT') + headers { + contentType(applicationJson()) + } + body(""" { "status" : "OK" } """) + url("/1") + } + response { + status OK() + body(""" { "status" : "OK" } """) + headers { + contentType(textPlain()) + } + } + } + Contract expectedSingleContractForText = Contract.make { request { method('PUT') @@ -154,6 +177,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..2dc497bf0e --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract.java @@ -0,0 +1,47 @@ +/* + * 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..b62b09dd25 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_docs_examples.java @@ -0,0 +1,351 @@ + +/* + * 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; +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..f8dee04c57 --- /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. + */ +//tag::class[] +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..0d7fd42fe6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest.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.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..af0b7ca125 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_from_file.java @@ -0,0 +1,41 @@ + +/* + * 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..a325f5e604 --- /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..ac90bec202 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java @@ -0,0 +1,104 @@ + +/* + * 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 0000000000000000000000000000000000000000..69503635d87cbdfa660d5cd8be66d3e74b3b20e8 GIT binary patch literal 14631 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c3b233056b758d1cc9bf3176e99a95add89ba607 GIT binary patch literal 15767 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 + +