From 45008110e076cb0ba030ae05d94ce46856bdac76 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:11:45 +0100 Subject: [PATCH 01/12] Initial commit --- .gitignore | 12 ++++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 215 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..32858aad3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..4bfe307bba --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# spring-cloud-contract +Support for Consumer Driven Contracts in Spring From 83793c105b4ca3f20e7821bb410c50991bd5892f Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:20:44 +0100 Subject: [PATCH 02/12] Add TODO list --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4bfe307bba..4c5131d65d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ -# spring-cloud-contract -Support for Consumer Driven Contracts in Spring +# Spring Cloud Contract + +What you always need it confidence in pushing a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. + +Feature ideas: + +- [ ] Move DSL and stub runner code over from [Accurest](https://github.com/Codearte/accurest) +- [ ] Spring Restdocs generators +- [ ] Wiremock support for Spring Boot apps +- [ ] Wiremock mock servers using `MockRestServiceServer` (from Spring Test) +- [ ] `DiscoveryClient` support so backends can be stubbed or mocked transparently (declaratively) +- [ ] Actuator endpoints that verify the state of contracts supported by the host service +- [ ] Something to make PACT easier to use in Spring Boot apps? From 95cd2d5c74a1fa38e9ba25e0f93a607a7dd96e20 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:23:05 +0100 Subject: [PATCH 03/12] Add messaging topic more explicitly --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4c5131d65d..71c41d99e9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Feature ideas: - [ ] Spring Restdocs generators - [ ] Wiremock support for Spring Boot apps - [ ] Wiremock mock servers using `MockRestServiceServer` (from Spring Test) +- [ ] POJO-based contracts (packaging DTOs and stub declarations from the producer) +- [ ] Sample messages packaged and injectable into Spring Cloud Stream consumer tests - [ ] `DiscoveryClient` support so backends can be stubbed or mocked transparently (declaratively) - [ ] Actuator endpoints that verify the state of contracts supported by the host service - [ ] Something to make PACT easier to use in Spring Boot apps? From 6db762dbdd766a8c74a969c9dffac9bac089c864 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:24:54 +0100 Subject: [PATCH 04/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71c41d99e9..d5fc84f93e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spring Cloud Contract -What you always need it confidence in pushing a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. +What you always need it confidence in pushing mew features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. Feature ideas: From e9f0c390c9e1825997bb26616210c235dab56707 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:25:12 +0100 Subject: [PATCH 05/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5fc84f93e..79b9aad0e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spring Cloud Contract -What you always need it confidence in pushing mew features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. +What you always need it confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. Feature ideas: From 4ae4d1f1cd4dad51b50b46a2c5c365901211bb8b Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 12 Jun 2016 18:42:38 +0100 Subject: [PATCH 06/12] Add schema registry --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 79b9aad0e1..098b1dc23c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spring Cloud Contract -What you always need it confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. +What you always need it confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. Feature ideas: @@ -13,3 +13,4 @@ Feature ideas: - [ ] `DiscoveryClient` support so backends can be stubbed or mocked transparently (declaratively) - [ ] Actuator endpoints that verify the state of contracts supported by the host service - [ ] Something to make PACT easier to use in Spring Boot apps? +- [ ] Schema registries for binary formats like protobuf, thrift, avro From 058ec4425a97e550874b75ae89677d5ae6ff78db Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 27 Jun 2016 07:41:38 -0700 Subject: [PATCH 07/12] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 098b1dc23c..4ddd96ebdf 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ What you always need it confidence in pushing new features into a new applicatio Feature ideas: - [ ] Move DSL and stub runner code over from [Accurest](https://github.com/Codearte/accurest) +- [ ] Support [Citrus](http://www.citrusframework.org/) users (somehow?) - [ ] Spring Restdocs generators - [ ] Wiremock support for Spring Boot apps - [ ] Wiremock mock servers using `MockRestServiceServer` (from Spring Test) From feae41fb35d9df572582f0fd586e2494c3c37d5f Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Fri, 8 Jul 2016 10:49:34 +0000 Subject: [PATCH 08/12] Add Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4ddd96ebdf..16436bf979 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Spring Cloud Contract +[![Join the chat at https://gitter.im/spring-cloud/spring-cloud-contract](https://badges.gitter.im/spring-cloud/spring-cloud-contract.svg)](https://gitter.im/spring-cloud/spring-cloud-contract?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + What you always need it confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. Feature ideas: From bd3c38d097fdc1274e44029c298f35055a4e5c4c Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 8 Jul 2016 12:50:50 +0200 Subject: [PATCH 09/12] Initial commit of Spring Cloud Contract Verifier --- .../.gitignore | 13 + .../.mvn/codesigning.asc.enc | Bin 0 -> 7280 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + .../.travis.yml | 5 + .../LICENSE.md | 194 + .../README.adoc | 60 + .../docs/pom.xml | 57 + .../docs/src/main/asciidoc/README.adoc | 15 + ...-cloud-contract-verifier-maven-plugin.adoc | 3 + .../docs/src/main/ruby/generate_readme.sh | 37 + .../mvnw | 243 + .../mvnw.cmd | 145 + .../pom.xml | 151 + .../pom.xml | 473 ++ .../src/etc/header.txt | 14 + .../verifier/maven/CopyContracts.groovy | 63 + .../verifier/maven/ManifestCreator.groovy | 36 + .../AetherStubDownloaderFactory.groovy | 49 + .../maven/stubrunner/LocalStubRunner.groovy | 38 + .../maven/stubrunner/RemoteStubRunner.groovy | 52 + .../contract/verifier/maven/ConvertMojo.java | 105 + .../verifier/maven/GenerateStubsMojo.java | 100 + .../verifier/maven/GenerateTestsMojo.java | 142 + .../contract/verifier/maven/RunMojo.java | 151 + .../resources/META-INF/plexus/components.xml | 45 + .../src/site/asciidoc/complex-content.adoc | 38 + .../src/site/asciidoc/complex.adoc | 3 + .../src/site/asciidoc/index.adoc | 10 + .../src/site/asciidoc/junit.adoc | 29 + .../src/site/asciidoc/spock.adoc | 29 + .../src/site/asciidoc/usage.adoc | 35 + .../src/site/site.xml | 58 + .../contract/verifier/maven/PluginIT.java | 113 + .../verifier/maven/PluginUnitTest.java | 115 + .../src/test/projects/basic/pom.xml | 37 + .../test/resources/contracts/Messaging.groovy | 37 + .../test/resources/contracts/Sample.groovy | 34 + .../projects/complex-configuration/.gitignore | 1 + .../projects/complex-configuration/pom.xml | 131 + .../frauddetection/Application.java | 28 + .../FraudDetectionController.java | 56 + .../frauddetection/model/FraudCheck.java | 45 + .../model/FraudCheckResult.java | 48 + .../model/FraudCheckStatus.java | 21 + .../src/test/contracts/brokenContract.groovy | 44 + .../contracts/shouldMarkClientAsFraud.groovy | 43 + .../shouldMarkClientAsNotFraud.groovy | 44 + .../frauddetection/BaseAccurest.java | 30 + .../matchers/CustomMatchers.java | 27 + .../src/test/projects/generatedStubs/pom.xml | 37 + .../test/projects/plugin-extension/pom.xml | 102 + .../src/main/java/hello/Application.java | 28 + .../src/main/java/hello/Greeting.java | 36 + .../main/java/hello/GreetingController.java | 36 + .../src/test/java/hello/BaseAccurest.java | 30 + .../resources/contracts/greetings_ok.groovy | 37 + .../greetings_with_default_value.groovy | 33 + .../src/test/projects/pomless/Sample.groovy | 34 + .../test/projects/spring-boot-groovy/pom.xml | 145 + .../src/main/java/hello/Application.java | 28 + .../src/main/java/hello/Greeting.java | 36 + .../main/java/hello/GreetingController.java | 36 + .../src/test/groovy/hello/BaseAccurest.groovy | 28 + .../resources/contracts/greetings_ok.groovy | 37 + .../greetings_with_default_value.groovy | 33 + .../test/projects/spring-boot-java/pom.xml | 110 + .../src/main/java/hello/Application.java | 28 + .../src/main/java/hello/Greeting.java | 36 + .../main/java/hello/GreetingController.java | 36 + .../src/test/java/hello/BaseAccurest.java | 30 + .../resources/contracts/greetings_ok.groovy | 37 + .../greetings_with_default_value.groovy | 33 + .../src/test/projects/withStubs/pom.xml | 37 + .../src/test/resources/stubs/Sample.groovy | 34 + .../spring-cloud-contract-verifier/.gitignore | 11 + .../spring-cloud-contract-verifier/LICENSE | 202 + .../README.adoc | 221 + .../asciidoctor.css | 399 ++ .../build.gradle | 88 + .../spring-cloud-contract-verifier/build.sh | 3 + .../spring-cloud-contract-verifier/deploy.sh | 3 + .../docs/build.gradle | 90 + .../docs/src/main/asciidoc/README.adoc | 19 + .../docs/src/main/asciidoc/contract.adoc | 220 + .../main/asciidoc/deprecated/contract.html | 1232 ++++++ .../main/asciidoc/deprecated/images/Deps.png | Bin 0 -> 37192 bytes .../asciidoc/deprecated/images/Stubs1.png | Bin 0 -> 35170 bytes .../asciidoc/deprecated/images/Stubs2.png | Bin 0 -> 18143 bytes .../src/main/asciidoc/deprecated/index.html | 3914 +++++++++++++++++ .../asciidoc/deprecated/introduction.html | 797 ++++ .../main/asciidoc/deprecated/messaging.html | 914 ++++ .../main/asciidoc/deprecated/migration.html | 594 +++ .../src/main/asciidoc/deprecated/rest.html | 1032 +++++ .../main/asciidoc/deprecated/stubrunner.html | 1258 ++++++ .../asciidoc/deprecated/stubrunner_msg.html | 1247 ++++++ .../docs/src/main/asciidoc/images/Deps.png | Bin 0 -> 37192 bytes .../docs/src/main/asciidoc/images/Stubs1.png | Bin 0 -> 35170 bytes .../docs/src/main/asciidoc/images/Stubs2.png | Bin 0 -> 18143 bytes .../docs/src/main/asciidoc/index.adoc | 1 + .../docs/src/main/asciidoc/introduction.adoc | 148 + .../docs/src/main/asciidoc/links.adoc | 11 + .../docs/src/main/asciidoc/messaging.adoc | 150 + .../docs/src/main/asciidoc/rest.adoc | 437 ++ .../spring-cloud-contract-verifier.adoc | 22 + .../docs/src/main/asciidoc/stubrunner.adoc | 97 + .../src/main/asciidoc/stubrunner_msg.adoc | 61 + .../docs/src/main/ruby/generate_readme.sh | 37 + .../generateReadme.sh | 3 + .../gradle.properties | 33 + .../gradle/release.gradle | 92 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../spring-cloud-contract-verifier/gradlew | 160 + .../gradlew.bat | 90 + .../publishDocs.sh | 3 + .../samples-messaging-camel/build.gradle | 32 + .../verifier/samples/camel/BookDeleted.groovy | 30 + .../verifier/samples/camel/BookDeleter.groovy | 44 + .../samples/camel/BookReturned.groovy | 30 + .../camel/BookRouteConfiguration.groovy | 53 + .../verifier/samples/camel/BookService.groovy | 44 + .../camel/CamelMessagingApplication.groovy | 28 + .../CamelMessagingApplicationSpec.groovy | 160 + .../build.gradle | 28 + .../samples/messaging/BookDeleted.groovy | 30 + .../samples/messaging/BookListener.groovy | 54 + .../samples/messaging/BookReturned.groovy | 30 + .../samples/messaging/BookService.groovy | 48 + .../IntegrationMessagingApplication.groovy | 30 + .../main/resources/integration-context.xml | 56 + ...IntegrationMessagingApplicationSpec.groovy | 168 + .../samples-messaging-spring/build.gradle | 43 + .../samples/spring/BookDeleted.groovy | 30 + .../samples/spring/BookListener.groovy | 74 + .../samples/spring/BookReturned.groovy | 30 + .../samples/spring/BookService.groovy | 56 + .../spring/SpringMessagingApplication.groovy | 30 + .../spring/SpringApplicationSpec.groovy | 158 + ...ontractVerifierSpringMessageBuilder.groovy | 51 + .../ContractVerifierSpringMessaging.groovy | 93 + ...ManualContractVerifierConfiguration.groovy | 35 + .../spring/config/SpringMessage.groovy | 68 + .../samples-messaging-stream/build.gradle | 40 + .../samples/messaging/BookDeleted.groovy | 30 + .../samples/messaging/BookListener.groovy | 64 + .../samples/messaging/BookReturned.groovy | 30 + .../samples/messaging/BookService.groovy | 52 + .../samples/messaging/DeleteSink.groovy | 32 + .../StreamMessagingApplication.groovy | 32 + .../src/main/resources/application.yml | 2 + .../StreamMessagingApplicationSpec.groovy | 152 + .../scripts/runTests.sh | 25 + .../settings.gradle | 39 + .../cloud/contract/spec/Contract.groovy | 93 + .../spec/ContractVerifierException.groovy | 34 + .../cloud/contract/spec/internal/Body.groovy | 64 + .../spec/internal/ClientDslProperty.groovy | 32 + .../contract/spec/internal/Common.groovy | 166 + .../contract/spec/internal/DslProperty.groovy | 45 + .../spec/internal/ExecutionProperty.groovy | 45 + .../contract/spec/internal/Header.groovy | 44 + .../contract/spec/internal/Headers.groovy | 73 + .../cloud/contract/spec/internal/Input.groovy | 127 + .../spec/internal/JSONCompareMode.groovy | 26 + .../spec/internal/MatchingStrategy.groovy | 67 + .../contract/spec/internal/Multipart.groovy | 57 + .../spec/internal/NamedProperty.groovy | 48 + .../spec/internal/OptionalProperty.groovy | 41 + .../spec/internal/OutputMessage.groovy | 93 + .../spec/internal/QueryParameter.groovy | 55 + .../spec/internal/QueryParameters.groovy | 39 + .../spec/internal/RegexPatterns.groovy | 74 + .../contract/spec/internal/Request.groovy | 193 + .../contract/spec/internal/Response.groovy | 104 + .../spec/internal/ServerDslProperty.groovy | 36 + .../cloud/contract/spec/internal/Url.groovy | 53 + .../contract/spec/internal/UrlPath.groovy | 41 + .../contract/spec/util/ValidateUtils.groovy | 69 + .../internal/ExecutionPropertySpec.groovy | 35 + .../spec/internal/RegexPatternsSpec.groovy | 78 + .../README.adoc | 54 + .../build.gradle | 40 + .../boot/HttpStubsController.groovy | 54 + .../stubrunner/boot/StubRunnerBoot.groovy | 52 + .../stubrunner/boot/TriggerController.groovy | 75 + .../main/resources/META-INF/spring.factories | 3 + .../stubrunner/boot/StubRunnerBootSpec.groovy | 118 + .../src/test/resources/application.yml | 2 + .../streamService-0.0.1-SNAPSHOT.pom | 25 + .../stubs/streamService/maven-metadata.xml | 28 + .../README.adoc | 65 + .../build.gradle | 35 + .../stubrunner/junit/StubRunnerRule.java | 223 + .../StubRunnerRuleCustomPortJUnitTest.java | 85 + .../junit/StubRunnerRuleJUnitTest.java | 81 + .../junit/StubRunnerRuleSpec.groovy | 58 + .../src/test/resources/logback.xml | 30 + .../fraudDetectionServer-0.0.1-SNAPSHOT.pom | 25 + .../fraudDetectionServer/maven-metadata.xml | 28 + .../loanIssuance-0.0.1-SNAPSHOT.pom | 25 + .../stubs/loanIssuance/maven-metadata.xml | 28 + .../README.adoc | 123 + .../build.gradle | 33 + .../camel/StubRunnerCamelConfiguration.groovy | 51 + .../camel/StubRunnerCamelPredicate.groovy | 79 + .../camel/StubRunnerCamelProcessor.groovy | 55 + .../main/resources/META-INF/spring.factories | 3 + .../messaging/camel/BookReturned.groovy | 32 + .../camel/CamelStubRunnerSpec.groovy | 227 + .../camel/StubRunnerCamelProcessorSpec.groovy | 98 + .../src/test/resources/application.yml | 2 + .../camelService-0.0.1-SNAPSHOT.pom | 25 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 28 + .../camelService/maven-metadata-local.xml | 28 + .../stubs/camelService/maven-metadata.xml | 28 + .../README.adoc | 131 + .../build.gradle | 30 + .../StubRunnerIntegrationConfiguration.groovy | 75 + ...tubRunnerIntegrationMessageSelector.groovy | 78 + .../StubRunnerIntegrationTransformer.groovy | 48 + .../main/resources/META-INF/spring.factories | 3 + .../messaging/integration/BookReturned.groovy | 32 + .../IntegrationStubRunnerSpec.groovy | 224 + ...tubRunnerIntegrationTransformerSpec.groovy | 95 + .../src/test/resources/application.yml | 2 + .../test/resources/integration-context.xml | 35 + .../integrationService-0.0.1-SNAPSHOT.pom | 25 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 28 + .../maven-metadata-local.xml | 28 + .../integrationService/maven-metadata.xml | 28 + .../README.adoc | 132 + .../build.gradle | 31 + .../StubRunnerStreamConfiguration.groovy | 94 + .../StubRunnerStreamMessageSelector.groovy | 79 + .../stream/StubRunnerStreamTransformer.groovy | 48 + .../main/resources/META-INF/spring.factories | 3 + .../messaging/stream/BookReturned.groovy | 32 + .../stream/StreamStubRunnerSpec.groovy | 226 + .../StubRunnerStreamTransformerSpec.groovy | 132 + .../src/test/resources/application.yml | 11 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 28 + .../streamService-0.0.1-SNAPSHOT.pom | 25 + .../streamService/maven-metadata-local.xml | 28 + .../stubs/streamService/maven-metadata.xml | 28 + .../README.adoc | 27 + .../build.gradle | 41 + .../spring/cloud/StubMapperProperties.java | 73 + .../cloud/StubRunnerDiscoveryClient.java | 97 + .../cloud/StubRunnerServiceInstance.java | 75 + ...tubRunnerSpringCloudAutoConfiguration.java | 61 + .../StubRunnerRibbonAutoConfiguration.java | 37 + .../StubRunnerRibbonBeanPostProcessor.java | 79 + .../ribbon/StubRunnerRibbonConfiguration.java | 51 + .../ribbon/StubRunnerRibbonServerList.java | 113 + .../main/resources/META-INF/spring.factories | 4 + ...nerSpringCloudAutoConfigurationSpec.groovy | 74 + .../src/test/resources/application.yml | 6 + .../src/test/resources/logback.xml | 30 + .../fraudDetectionServer-0.0.1-SNAPSHOT.pom | 25 + .../fraudDetectionServer/maven-metadata.xml | 28 + .../loanIssuance-0.0.1-SNAPSHOT.pom | 25 + .../stubs/loanIssuance/maven-metadata.xml | 28 + .../README.adoc | 21 + .../build.gradle | 38 + .../spring/StubRunnerConfiguration.java | 85 + .../main/resources/META-INF/spring.factories | 3 + .../spring/StubRunnerConfigurationSpec.groovy | 57 + .../src/test/resources/application.yml | 2 + .../src/test/resources/logback.xml | 30 + .../fraudDetectionServer-0.0.1-SNAPSHOT.pom | 25 + .../fraudDetectionServer/maven-metadata.xml | 28 + .../loanIssuance-0.0.1-SNAPSHOT.pom | 25 + .../stubs/loanIssuance/maven-metadata.xml | 28 + .../README.adoc | 99 + .../build.gradle | 119 + .../stubrunner/AetherFactories.groovy | 68 + .../stubrunner/AetherStubDownloader.groovy | 155 + .../contract/stubrunner/Arguments.groovy | 43 + .../stubrunner/AvailablePortScanner.groovy | 89 + .../stubrunner/BatchStubRunner.groovy | 141 + .../stubrunner/BatchStubRunnerFactory.groovy | 59 + .../stubrunner/GroovyDslWrapper.groovy | 39 + .../MessageNotMatchingException.groovy | 28 + .../contract/stubrunner/RunningStubs.groovy | 84 + .../stubrunner/StubConfiguration.groovy | 156 + .../cloud/contract/stubrunner/StubData.groovy | 31 + .../contract/stubrunner/StubDownloader.groovy | 28 + .../contract/stubrunner/StubFinder.groovy | 49 + .../contract/stubrunner/StubRepository.groovy | 105 + .../contract/stubrunner/StubRunner.groovy | 122 + .../stubrunner/StubRunnerExecutor.groovy | 155 + .../stubrunner/StubRunnerFactory.groovy | 63 + .../contract/stubrunner/StubRunnerMain.groovy | 94 + .../StubRunnerMessagingTrigger.groovy | 35 + .../stubrunner/StubRunnerOptions.groovy | 86 + .../StubRunnerOptionsBuilder.groovy | 135 + .../contract/stubrunner/StubRunning.groovy | 25 + .../contract/stubrunner/StubServer.groovy | 88 + .../contract/stubrunner/StubTrigger.groovy | 54 + .../WiremockMappingDescriptor.groovy | 43 + .../stubrunner/util/StringUtils.groovy | 140 + .../stubrunner/util/StubsParser.groovy | 81 + .../stubrunner/util/ZipCategory.groovy | 72 + .../accurestStubrunnerGrapeConfig.xml | 28 + .../AvailablePortScannerSpec.groovy | 70 + .../stubrunner/BatchStubRunnerSpec.groovy | 59 + .../stubrunner/MappingDescriptorSpec.groovy | 39 + .../stubrunner/RunningStubsSpec.groovy | 87 + .../stubrunner/StubConfigurationSpec.groovy | 37 + .../stubrunner/StubRepositorySpec.groovy | 50 + .../stubrunner/StubRunnerExecutorSpec.groovy | 130 + .../stubrunner/StubRunnerFactorySpec.groovy | 55 + .../StubRunnerOptionsBuilderSpec.groovy | 84 + .../contract/stubrunner/StubRunnerSpec.groovy | 55 + .../contract/stubrunner/StubServerSpec.groovy | 51 + .../stubrunner/util/ZipCategorySpec.groovy | 40 + .../mappings/spring/cloud/bar/bar.json | 13 + .../mappings/spring/cloud/foo/bar/foobar.json | 13 + .../mappings/spring/cloud/foo/foo.json | 13 + .../projects/brokers/brokers.json | 5 + .../brokers/nested/anotherDescriptor.json | 5 + .../projects/descriptor.json | 5 + .../src/test/resources/emptyrepo/.gitkeep | 0 .../src/test/resources/file.zip | Bin 0 -> 171 bytes .../src/test/resources/logback.xml | 30 + .../repository/contracts/contract1.groovy | 27 + .../mappings/lv/spring/cloud/bye/lv_bye.json | 13 + .../mappings/pl/spring/cloud/bye/pl_bye.json | 13 + .../spring/cloud/bye/pl_overridden_bye.json | 14 + .../spring/cloud/bye/admin/admin.json | 9 + .../mappings/spring/cloud/bye/bye.json | 13 + .../mappings/spring/cloud/hello/README.md | 1 + .../spring/cloud/hello/admin/admin.json | 9 + .../mappings/spring/cloud/hello/hello.json | 13 + .../mappings/spring/cloud/ping/ping.json | 13 + .../build.gradle | 109 + .../main/groovy/nl/flotsam/xeger/Xeger.java | 118 + .../converter/SingleFileConverter.groovy | 44 + ...ConversionContractVerifierException.groovy | 33 + .../DslToWireMockClientConverter.groovy | 38 + .../wiremock/DslToWireMockConverter.groovy | 45 + .../wiremock/RecursiveFilesConverter.groovy | 93 + .../wiremock/WireMockToDslConverter.groovy | 209 + .../groovy/nl/flotsam/xeger/XegerTest.java | 64 + .../nl/flotsam/xeger/XegerUtilsTest.java | 39 + .../DslToWireMockClientConverterSpec.groovy | 284 ++ .../RecursiveFilesConverterSpec.groovy | 115 + .../WireMockToDslConverterSpec.groovy | 559 +++ .../WiremockScenarioConverterSpec.groovy | 62 + .../scenario/main_scenario/01_login.groovy | 27 + .../scenario/main_scenario/02_showCart.groovy | 27 + .../scenario/main_scenario/03_logout.groovy | 27 + .../converter/source/dir1/dsl1.groovy | 30 + .../converter/source/dir1/dsl1b.groovy | 30 + .../converter/source/dir2/dsl2.groovy | 30 + .../resources/converter/source/dslRoot.groovy | 30 + .../cloud/contract/verifier/FileSaver.groovy | 54 + .../contract/verifier/TestGenerator.groovy | 105 + .../verifier/builder/BlockBuilder.groovy | 106 + .../verifier/builder/ClassBuilder.groovy | 173 + .../JUnitMessagingMethodBodyBuilder.groovy | 192 + .../builder/JUnitMethodBodyBuilder.groovy | 156 + .../JaxRsClientJUnitMethodBodyBuilder.groovy | 155 + ...kMethodRequestProcessingBodyBuilder.groovy | 151 + .../builder/MessagingMethodBodyBuilder.groovy | 109 + .../verifier/builder/MethodBodyBuilder.groovy | 383 ++ .../verifier/builder/MethodBuilder.groovy | 96 + .../MockMvcJUnitMethodBodyBuilder.groovy | 70 + ...kMethodRequestProcessingBodyBuilder.groovy | 78 + .../RequestProcessingMethodBodyBuilder.groovy | 209 + .../builder/SingleTestGenerator.groovy | 176 + .../SpockMessagingMethodBodyBuilder.groovy | 185 + ...kMethodRequestProcessingBodyBuilder.groovy | 135 + .../ContractVerifierConfigProperties.groovy | 105 + .../verifier/config/TestFramework.groovy | 82 + .../contract/verifier/config/TestMode.groovy | 41 + .../wiremock/BaseWireMockStubStrategy.groovy | 126 + .../WireMockRequestStubStrategy.groovy | 292 ++ .../WireMockResponseStubStrategy.groovy | 88 + .../dsl/wiremock/WireMockStubStrategy.groovy | 80 + .../verifier/file/ContractFileScanner.groovy | 112 + .../verifier/file/ContractMetadata.groovy | 53 + .../verifier/util/BodyExtractor.groovy | 84 + .../contract/verifier/util/ContentType.groovy | 36 + .../verifier/util/ContentUtils.groovy | 373 ++ .../util/ContractVerifierDslConverter.groovy | 44 + .../util/DelegatingJsonVerifiable.java | 327 ++ .../FinishedDelegatingJsonVerifiable.java | 38 + .../contract/verifier/util/JsonPaths.groovy | 29 + .../util/JsonToJsonPathsConverter.groovy | 265 ++ .../verifier/util/MapConverter.groovy | 129 + .../verifier/util/MethodBuffering.java | 29 + .../util/MethodBufferingJsonVerifiable.java | 74 + .../contract/verifier/util/NamesUtil.groovy | 111 + .../verifier/util/RegexpBuilders.groovy | 155 + .../verifier/util/ShouldTraverse.java | 33 + .../stubs/wiremocks/other/sample.json | 23 + .../resources/stubs/wiremocks/sample.json | 23 + .../verifier/GeneratorScannerSpec.groovy | 53 + .../cloud/contract/verifier/MainTest.groovy | 32 + .../verifier/builder/BookReturned.groovy | 30 + .../builder/ContractHttpDocsSpec.groovy | 317 ++ .../JaxRsClientMethodBuilderSpec.groovy | 683 +++ .../MessagingMethodBodyBuilderSpec.groovy | 447 ++ .../MockMvcMethodBodyBuilderSpec.groovy | 1447 ++++++ .../builder/SingleTestGeneratorSpec.groovy | 149 + .../verifier/dsl/WireMockGroovyDslSpec.groovy | 1577 +++++++ .../verifier/dsl/WireMockStubVerifier.groovy | 39 + .../dsl/internal/ExecutionPropertySpec.groovy | 35 + .../dsl/internal/RegexPatternsSpec.groovy | 78 + .../file/ContractFileScannerSpec.groovy | 76 + .../util/JsonToJsonPathsConverterSpec.groovy | 611 +++ .../directory/with/scenario/01_login.groovy | 16 + .../with/scenario/02_showCart.groovy | 16 + .../directory/with/scenario/03_logout.groovy | 16 + .../with/stubs/different/diff.groovy | 16 + .../with/stubs/other/different/diff.groovy | 16 + .../directory/with/stubs/other/other.groovy | 16 + .../directory/with/stubs/other/sample.groovy | 16 + .../with/stubs/package/exceptions/test.groovy | 16 + .../stubs/package/v1/exceptions/testv1.groovy | 16 + .../stubs/package/v2/exceptions/testv2.groovy | 16 + .../with/stubs/stubsRepositoryIndicator | 0 .../test/resources/dsl/basic/sampleDsl.groovy | 48 + .../src/test/resources/main.json | 0 .../strange_[3.3.3]_directory/02_login.groovy | 16 + .../bar/03_login.groovy | 16 + .../foo/01_login.groovy | 16 + .../plugin/GenerateServerTestsTask.groovy | 61 + ...erateWireMockClientStubsFromDslTask.groovy | 54 + ...ngCloudContractVerifierGradlePlugin.groovy | 206 + .../contract-verifier.properties | 17 + .../plugin/BasicFunctionalSpec.groovy | 113 + .../ContractVerifierIntegrationSpec.groovy | 154 + .../plugin/MessagingProjectSpec.groovy | 47 + .../plugin/SampleJerseyProjectSpec.groovy | 46 + .../verifier/plugin/SampleProjectSpec.groovy | 46 + .../plugin/ScenarioProjectSpec.groovy | 46 + .../functionalTest/bootSimple/build.gradle | 69 + .../bootSimple/gradle.properties | 22 + .../gradle/wrapper/gradle-wrapper.properties | 22 + .../functionalTest/bootSimple/gradlew | 164 + .../functionalTest/bootSimple/gradlew.bat | 90 + .../pairId/collerate_PlacesFrom_Tweet.groovy | 36 + .../pairId/moreComplexVersion.groovy | 43 + .../functionalTest/bootSimple/settings.gradle | 17 + .../twitter/place/PairIdController.groovy | 52 + .../cloud/twitter/place/Tweet.java | 13 + .../twitter/places/AcceptanceSpec.groovy | 39 + .../twitter/places/BaseMockMvcSpec.groovy | 39 + .../src/test/resources/logback-test.groovy | 30 + .../messagingProject/build.gradle | 80 + .../messagingProject/gradle.properties | 22 + .../gradle/wrapper/gradle-wrapper.properties | 22 + .../functionalTest/messagingProject/gradlew | 160 + .../messagingProject/gradlew.bat | 90 + .../repository/mappings/a_foo.groovy | 28 + .../repository/mappings/bookDeleted.groovy | 31 + .../repository/mappings/bookReturned1.groovy | 30 + .../repository/mappings/bookReturned2.groovy | 38 + .../messagingProject/settings.gradle | 17 + .../cloud/samples/book/BookDeleted.groovy | 30 + .../cloud/samples/book/BookListener.groovy | 54 + .../cloud/samples/book/BookReturned.groovy | 30 + .../cloud/samples/book/BookService.groovy | 48 + .../IntegrationMessagingApplication.groovy | 38 + .../main/resources/integration-context.xml | 56 + .../samples/book/MessagingBaseSpec.groovy | 47 + .../cloud/samples/book/MessagingBaseTest.java | 52 + .../sampleJerseyProject/build.gradle | 119 + .../shouldMarkClientAsFraud.groovy | 45 + .../shouldMarkClientAsNotFraud.groovy | 46 + .../cloud/frauddetection/Application.java | 24 + .../FraudDetectionController.java | 38 + .../frauddetection/FraudRestApplication.java | 13 + .../frauddetection/model/FraudCheck.java | 29 + .../model/FraudCheckResult.java | 32 + .../model/FraudCheckStatus.java | 5 + .../src/main/resources/application.yml | 1 + .../org/springframework/cloud/MvcSpec.groovy | 73 + .../org/springframework/cloud/MvcTest.java | 70 + .../gradle/wrapper/gradle-wrapper.properties | 22 + .../sampleJerseyProject/gradlew | 164 + .../sampleJerseyProject/gradlew.bat | 90 + .../loanApplicationService/mappings/.gitkeep | 0 .../cloud/frauddetection/Application.java | 33 + .../LoanApplicationService.java | 84 + .../cloud/frauddetection/model/Client.java | 30 + .../model/FraudCheckStatus.java | 21 + .../model/FraudServiceRequest.java | 50 + .../model/FraudServiceResponse.java | 43 + .../frauddetection/model/LoanApplication.java | 52 + .../model/LoanApplicationResult.java | 48 + .../model/LoanApplicationStatus.java | 21 + .../src/main/resources/application.yml | 1 + .../cloud/LoanApplicationServiceSpec.groovy | 72 + .../shouldMarkClientAsFraud.json | 23 + .../shouldMarkClientAsNotFraud.json | 23 + .../sampleJerseyProject/settings.gradle | 18 + .../functionalTest/sampleProject/build.gradle | 116 + .../shouldMarkClientAsFraud.groovy | 44 + .../shouldMarkClientAsNotFraud.groovy | 45 + .../frauddetection/Application.java | 18 + .../FraudDetectionController.java | 39 + .../frauddetection/model/FraudCheck.java | 29 + .../model/FraudCheckResult.java | 32 + .../model/FraudCheckStatus.java | 5 + .../src/main/resources/application.yml | 1 + .../org/springframework/cloud/MvcSpec.groovy | 31 + .../org/springframework/cloud/MvcTest.java | 16 + .../gradle/wrapper/gradle-wrapper.properties | 22 + .../functionalTest/sampleProject/gradlew | 164 + .../functionalTest/sampleProject/gradlew.bat | 90 + .../loanApplicationService/mappings/.gitkeep | 0 .../cloud/frauddetection/Application.java | 18 + .../LoanApplicationService.java | 68 + .../cloud/frauddetection/model/Client.java | 14 + .../model/FraudCheckStatus.java | 5 + .../model/FraudServiceRequest.java | 34 + .../model/FraudServiceResponse.java | 27 + .../frauddetection/model/LoanApplication.java | 36 + .../model/LoanApplicationResult.java | 32 + .../model/LoanApplicationStatus.java | 5 + .../src/main/resources/application.yml | 1 + .../cloud/LoanApplicationServiceSpec.groovy | 72 + .../shouldMarkClientAsFraud.json | 23 + .../shouldMarkClientAsNotFraud.json | 23 + .../sampleProject/settings.gradle | 18 + .../scenarioProject/build.gradle | 134 + .../1_shouldMarkClientAsNotFraud.groovy | 45 + .../2_shouldMarkClientAsFraud.groovy | 45 + .../cloud/frauddetection/Application.java | 18 + .../FraudDetectionController.java | 39 + .../frauddetection/model/FraudCheck.java | 29 + .../model/FraudCheckResult.java | 32 + .../model/FraudCheckStatus.java | 5 + .../src/main/resources/application.yml | 1 + .../org/springframework/cloud/MvcSpec.groovy | 31 + .../org/springframework/cloud/MvcTest.java | 16 + .../gradle/wrapper/gradle-wrapper.properties | 22 + .../functionalTest/scenarioProject/gradlew | 164 + .../scenarioProject/gradlew.bat | 90 + .../loanApplicationService/mappings/.gitkeep | 0 .../cloud/frauddetection/Application.java | 18 + .../LoanApplicationService.java | 68 + .../cloud/frauddetection/model/Client.java | 14 + .../model/FraudCheckStatus.java | 5 + .../model/FraudServiceRequest.java | 34 + .../model/FraudServiceResponse.java | 27 + .../frauddetection/model/LoanApplication.java | 36 + .../model/LoanApplicationResult.java | 32 + .../model/LoanApplicationStatus.java | 5 + .../src/main/resources/application.yml | 1 + .../cloud/LoanApplicationServiceSpec.groovy | 74 + .../shouldMarkClientAsFraud.json | 23 + .../shouldMarkClientAsNotFraud.json | 23 + .../scenarioProject/settings.gradle | 18 + .../README.adoc | 23 + .../build.gradle | 22 + .../messaging/camel/CamelMessage.java | 56 + .../ContractVerifierCamelConfiguration.java | 37 + .../ContractVerifierCamelMessageBuilder.java | 48 + .../camel/ContractVerifierCamelMessaging.java | 107 + .../main/resources/META-INF/spring.factories | 3 + .../build.gradle | 23 + ...tractVerifierIntegrationConfiguration.java | 36 + ...ractVerifierIntegrationMessageBuilder.java | 45 + .../ContractVerifierIntegrationMessaging.java | 102 + .../integration/IntegrationMessage.java | 54 + .../main/resources/META-INF/spring.factories | 3 + .../build.gradle | 24 + .../messaging/ContractVerifierFilter.java | 30 + .../messaging/ContractVerifierMessage.java | 48 + .../ContractVerifierMessageBuilder.java | 38 + .../messaging/ContractVerifierMessaging.java | 51 + .../ContractVerifierMessagingUtil.java | 114 + .../ContractVerifierObjectMapper.java | 46 + .../noop/NoOpContractVerifierMessage.java | 46 + .../NoOpContractVerifierMessageBuilder.java | 38 + .../noop/NoOpContractVerifierMessaging.java | 58 + .../build.gradle | 24 + ...ntractVerifierStreamAutoConfiguration.java | 38 + .../ContractVerifierStreamMessageBuilder.java | 46 + .../ContractVerifierStreamMessaging.java | 115 + .../messaging/stream/StreamMessage.java | 54 + .../main/resources/META-INF/spring.factories | 3 + .../verifier/util/AssertionUtil.groovy | 28 + 586 files changed, 50358 insertions(+) create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.gitignore create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/codesigning.asc.enc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/wrapper/maven-wrapper.properties create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.travis.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/LICENSE.md create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/spring-cloud-contract-verifier-maven-plugin.adoc create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/ruby/generate_readme.sh create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw.cmd create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/etc/header.txt create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/CopyContracts.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/ManifestCreator.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/AetherStubDownloaderFactory.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/LocalStubRunner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/RemoteStubRunner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/ConvertMojo.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateStubsMojo.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateTestsMojo.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/RunMojo.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/resources/META-INF/plexus/components.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex-content.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/index.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/junit.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/spock.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/usage.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/site.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginIT.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginUnitTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Messaging.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Sample.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/.gitignore create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/FraudDetectionController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheck.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/brokenContract.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsNotFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/BaseAccurest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/matchers/CustomMatchers.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Greeting.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/GreetingController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/java/hello/BaseAccurest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_ok.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_with_default_value.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/pomless/Sample.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Greeting.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/GreetingController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/groovy/hello/BaseAccurest.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_ok.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_with_default_value.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Greeting.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/GreetingController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/java/hello/BaseAccurest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_ok.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_with_default_value.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/pom.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/src/test/resources/stubs/Sample.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/.gitignore create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/LICENSE create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/asciidoctor.css create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.sh create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/deploy.sh create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/contract.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/contract.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Deps.png create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Stubs1.png create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Stubs2.png create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/index.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/introduction.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/messaging.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/migration.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/rest.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner_msg.html create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Deps.png create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Stubs1.png create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Stubs2.png create mode 120000 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/index.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/introduction.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/links.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/messaging.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/rest.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner_msg.adoc create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/ruby/generate_readme.sh create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/generateReadme.sh create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle.properties create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/release.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew.bat create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/publishDocs.sh create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookRouteConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookService.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplication.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/test/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplicationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplication.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/resources/integration-context.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookListener.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookService.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringMessagingApplication.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringApplicationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessageBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessaging.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ManualContractVerifierConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/SpringMessage.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/DeleteSink.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplication.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplicationSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/scripts/runTests.sh create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractVerifierException.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Header.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/JSONCompareMode.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingStrategy.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Multipart.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/ValidateUtils.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ExecutionPropertySpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/HttpStubsController.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBoot.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/TriggerController.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBootSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/main/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/logback.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformer.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/integration-context.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/integrationService-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformer.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata-local.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubMapperProperties.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerServiceInstance.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonAutoConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonBeanPostProcessor.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonServerList.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfigurationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/logback.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfigurationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/logback.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherFactories.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloader.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/Arguments.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScanner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/GroovyDslWrapper.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/MessageNotMatchingException.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/RunningStubs.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubConfiguration.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubData.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubDownloader.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubFinder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRepository.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMain.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMessagingTrigger.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunning.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubServer.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubTrigger.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/WiremockMappingDescriptor.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StringUtils.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StubsParser.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategory.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/resources/accurestStubrunnerGrapeConfig.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScannerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/MappingDescriptorSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/RunningStubsSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRepositorySpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactorySpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubServerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategorySpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/bar/bar.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/bar/foobar.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/foo.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/brokers.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/nested/anotherDescriptor.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/descriptor.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/emptyrepo/.gitkeep create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/file.zip create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/logback.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/lv/spring/cloud/bye/lv_bye.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_bye.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_overridden_bye.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/admin/admin.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/bye.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/README.md create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/admin/admin.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/hello.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/ping/ping.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/nl/flotsam/xeger/Xeger.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/SingleFileConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/ConversionContractVerifierException.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerUtilsTest.java create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverterSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverterSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WiremockScenarioConverterSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/01_login.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/02_showCart.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/03_logout.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1b.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir2/dsl2.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dslRoot.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/FileSaver.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcJUnitMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestFramework.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/BodyExtractor.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentType.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonPaths.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/NamesUtil.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ShouldTraverse.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/other/sample.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/sample.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/MainTest.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/ExecutionPropertySpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/RegexPatternsSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/01_login.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/02_showCart.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/03_logout.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/different/diff.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/different/diff.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/other.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/sample.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/exceptions/test.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v1/exceptions/testv1.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v2/exceptions/testv2.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/stubsRepositoryIndicator create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/dsl/basic/sampleDsl.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/main.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/02_login.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/bar/03_login.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/foo/01_login.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateWireMockClientStubsFromDslTask.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/resources/META-INF/gradle-plugins/contract-verifier.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/BasicFunctionalSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/MessagingProjectSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleJerseyProjectSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ScenarioProjectSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew.bat create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/collerate_PlacesFrom_Tweet.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/moreComplexVersion.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/PairIdController.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/Tweet.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/AcceptanceSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/resources/logback-test.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew.bat create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/a_foo.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned1.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned2.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookDeleted.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookListener.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookReturned.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookService.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/IntegrationMessagingApplication.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/resources/integration-context.xml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/groovy/org/springframework/cloud/samples/book/MessagingBaseSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/java/org/springframework/cloud/samples/book/MessagingBaseTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudRestApplication.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew.bat create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/mappings/.gitkeep create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/FraudDetectionController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheck.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew.bat create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/mappings/.gitkeep create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/1_shouldMarkClientAsNotFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/2_shouldMarkClientAsFraud.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew.bat create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/mappings/.gitkeep create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/resources/application.yml create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/settings.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/README.adoc create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelMessage.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessaging.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessageBuilder.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessaging.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/IntegrationMessage.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierFilter.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessage.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessageBuilder.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessaging.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessagingUtil.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierObjectMapper.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessage.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessageBuilder.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessaging.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/build.gradle create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamAutoConfiguration.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessageBuilder.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessaging.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamMessage.java create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-testing-utils/src/main/groovy/org/springframework/cloud/contract/verifier/util/AssertionUtil.groovy diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.gitignore b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.gitignore new file mode 100644 index 0000000000..2c95b068c5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.gitignore @@ -0,0 +1,13 @@ +.idea/ +*.iml +/target +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +target \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/codesigning.asc.enc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/codesigning.asc.enc new file mode 100644 index 0000000000000000000000000000000000000000..da867ac9b11c107076098d6f7c5ab3cc24b1d503 GIT binary patch literal 7280 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 + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright 2016 Codearte + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/README.adoc new file mode 100644 index 0000000000..ccd77f7328 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/README.adoc @@ -0,0 +1,60 @@ +// Do not edit this file (e.g. go instead to src/main/asciidoc) + +image::https://travis-ci.org/Codearte/accurest-maven-plugin.svg?branch=master[Build Status, link=https://travis-ci.org/Codearte/accurest-maven-plugin] +image::https://img.shields.io/github/issues/Codearte/accurest/maven.svg[Github Issues, link=https://github.com/Codearte/accurest/labels/maven] + +//[![Maven Central](https://img.shields.io/maven-central/v/io.codearte.accurest/accurest-maven-plugin.svg)](https://maven-badges.herokuapp.com/maven-central/io.codearte.accurest/accurest-maven-plugin) + += Spring Cloud Contract Verifier Maven Plugin + +== Documentation + +* http://codearte.github.io/accurest/[Spring Cloud Contract Verifier Documentation] +* http://codearte.github.io/accurest-maven-plugin/plugin-info.html[Spring Cloud Contract Verifier Maven Plugin Documentation Site] + +== Contributing + +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. + +=== Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +https://support.springsource.com/spring_committer_signup[contributor's agreement]. +Signing the contributor's agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. + +=== Code of Conduct +This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of +conduct]. By participating, you are expected to uphold this code. Please report +unacceptable behavior to spring-code-of-conduct@pivotal.io. + +=== Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + +* Use the Spring Framework code format conventions. If you use Eclipse + you can import formatter settings using the + `eclipse-code-formatter.xml` file from the + https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring + Cloud Build] project. If using IntelliJ, you can use the + http://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter + Plugin] to import the same file. +* Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +* Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +* Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +* Add some Javadocs and, if you change the namespace, some XSD doc elements. +* A few unit tests would help a lot as well -- someone has to do it. +* If no-one else is using your branch, please rebase it against the current master (or + other target branch in the main project). +* When writing a commit message please follow http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], + if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit + message (where XXXX is the issue number). \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/pom.xml new file mode 100644 index 0000000000..1c02de1ddb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-build + 1.1.1.RELEASE + + + + spring-cloud-contract-verifier-docs + pom + Spring Cloud Contract Verifier Docs + Spring Contract Verifier Docs + 1.0.0.BUILD-SNAPSHOT + + spring-cloud-contract-verifier-maven-plugin + ${basedir}/.. + + + + docs + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.3 + false + + + org.apache.maven.plugins + maven-antrun-plugin + false + + + org.codehaus.mojo + build-helper-maven-plugin + false + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/README.adoc new file mode 100644 index 0000000000..ac49eb468b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/README.adoc @@ -0,0 +1,15 @@ +image::https://travis-ci.org/Codearte/accurest-maven-plugin.svg?branch=master[Build Status, link=https://travis-ci.org/Codearte/accurest-maven-plugin] +image::https://img.shields.io/github/issues/Codearte/accurest/maven.svg[Github Issues, link=https://github.com/Codearte/accurest/labels/maven] + +//[![Maven Central](https://img.shields.io/maven-central/v/io.codearte.accurest/accurest-maven-plugin.svg)](https://maven-badges.herokuapp.com/maven-central/io.codearte.accurest/accurest-maven-plugin) + += Spring Cloud Contract Verifier Maven Plugin + +== Documentation + +* http://codearte.github.io/accurest/[Spring Cloud Contract Verifier Documentation] +* http://codearte.github.io/accurest-maven-plugin/plugin-info.html[Spring Cloud Contract Verifier Maven Plugin Documentation Site] + +== Contributing + +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/spring-cloud-contract-verifier-maven-plugin.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/spring-cloud-contract-verifier-maven-plugin.adoc new file mode 100644 index 0000000000..b2fd89ebfd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/asciidoc/spring-cloud-contract-verifier-maven-plugin.adoc @@ -0,0 +1,3 @@ +:root_folder: ../../../../spring-cloud-contract-verifier-maven-plugin + +include::{root_folder}/src/site/asciidoc/complex-content.adoc[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/ruby/generate_readme.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/ruby/generate_readme.sh new file mode 100755 index 0000000000..6d0ce9dc54 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/docs/src/main/ruby/generate_readme.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby + +base_dir = File.join(File.dirname(__FILE__),'../../..') +src_dir = File.join(base_dir, "/src/main/asciidoc") +require 'asciidoctor' +require 'optparse' + +options = {} +file = "#{src_dir}/README.adoc" + +OptionParser.new do |o| + o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' } + o.on('-h', '--help') { puts o; exit } + o.parse! +end + +file = ARGV[0] if ARGV.length>0 + +# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb +doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes] +header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a +header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k } +attrs = doc.attributes +attrs['allow-uri-read'] = true +puts attrs + +out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n" +doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs +out << doc.reader.read + +unless options[:to_file] + puts out +else + File.open(options[:to_file],'w+') do |file| + file.write(out) + end +end diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw new file mode 100755 index 0000000000..349bef124b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw @@ -0,0 +1,243 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +VERSION=$(awk '/ 0) {$0=$0} 1' `dirname $0`/pom.xml| grep '\(.*\)<.*/\1/') +if echo $VERSION | egrep -q 'M|RC'; then + echo Activating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pmilestone" +else + echo Deactivating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pmilestone//') +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} ${MAVEN_ARGS} "$@" + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw.cmd b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw.cmd new file mode 100644 index 0000000000..001048081d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/pom.xml new file mode 100644 index 0000000000..2139d3fc35 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/pom.xml @@ -0,0 +1,151 @@ + + + 4.0.0 + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin-parent + pom + 1.0.0.BUILD-SNAPSHOT + + Spring Cloud Contract Verifier Maven Plugin + Spring Cloud Contract Verifier Maven Plugin Parent + https://github.com/Codearte/accurest-maven-plugin + + 2016 + + + docs + spring-cloud-contract-verifier-maven-plugin + + + + [3.2.1,) + + + + Codearte + http://codearte.io/ + + + + + mariuszs + Mariusz Smykula + mariuszs@gmail.com + + + marcingrzejszczak + Marcin Grzejszczak + marcin@grzejszczak.pl + + + + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + scm:git:https://github.com/Codearte/accurest-maven-plugin.git + scm:git:git@github.com:Codearte/accurest-maven-plugin.git + https://github.com/Codearte/accurest-maven-plugin + HEAD + + + + GitHub Issues + https://github.com/Codearte/accurest/labels/maven + + + + Travis + https://travis-ci.org/Codearte/accurest-maven-plugin + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + org.codehaus.plexus + plexus-component-metadata + 1.6 + + + maven-plugin-plugin + ${maven.plugin.plugin.version} + + + io.takari.maven.plugins + takari-lifecycle-plugin + 1.12.0 + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.19.1 + + + com.mycila + license-maven-plugin + 3.0.rc1 + + + org.jacoco + jacoco-maven-plugin + 0.7.6.201602180812 + + + org.eluder.coveralls + coveralls-maven-plugin + 4.1.0 + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + + org.apache.maven.plugins + maven-scm-publish-plugin + 1.1 + + + org.apache.maven.plugins + maven-site-plugin + 3.4 + + + maven-install-plugin + 2.5.2 + + + maven-source-plugin + 3.0.0 + + + maven-javadoc-plugin + 2.10.3 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/pom.xml new file mode 100644 index 0000000000..17d3dfe4d6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/pom.xml @@ -0,0 +1,473 @@ + + + 4.0.0 + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + maven-plugin + 1.0.0.BUILD-SNAPSHOT + + Spring Cloud Contract Verifier Maven Plugin + Spring Cloud Contract Verifier Maven Plugin + https://github.com/Codearte/accurest-maven-plugin + + + 3.2.5 + 3.4 + + UTF-8 + UTF-8 + + 1.0.0.BUILD-SNAPSHOT + 1.1.0 + + 1.7 + 1.7 + + + 2016 + + + + repo.spring.io + Spring Release Repository + https://repo.spring.io/plugins-release-local + + + repo.spring.io + Spring Snapshot Repository + https://repo.spring.io/plugins-snapshot-local + + + github + scm:git:ssh://git@github.com:Codearte/accurest-maven-plugin.git + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + addSources + generateStubs + compile + removeStubs + + + + + + org.codehaus.plexus + plexus-component-metadata + + + + generate-metadata + generate-test-metadata + + + + + + maven-plugin-plugin + + true + + + + generated-helpmojo + + helpmojo + + + + generate-descriptor + + descriptor + + + + + + io.takari.maven.plugins + takari-lifecycle-plugin + true + + + testProperties + process-test-resources + + testProperties + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + com.mycila + license-maven-plugin + +
src/etc/header.txt
+ + src/** + + + **/.idea/** + **/*.adoc + + true + true + + ${project.inceptionYear} + Codearte + + UTF-8 +
+ + + + check + + + +
+ + org.jacoco + jacoco-maven-plugin + + + **/HelpMojo.class + + + + + prepare-agent + + prepare-agent + + + + + + org.eluder.coveralls + coveralls-maven-plugin + + + org.apache.maven.plugins + maven-release-plugin + + false + true + v@{project.version} + + + + org.apache.maven.plugins + maven-scm-publish-plugin + + ${project.scm.developerConnection} + gh-pages + ${project.reporting.outputDirectory} + + + + scm-publish + site-deploy + + publish-scm + + + + + + org.apache.maven.plugins + maven-site-plugin + + true + + true + true + false + en + UTF-8 + UTF-8 + + + + font + coderay + style + 2 + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.3 + + + +
+
+ + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-core + ${spring.cloud.contract.verifier.version} + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-converters + ${spring.cloud.contract.verifier.version} + + + org.springframework.cloud.contract + spring-cloud-contract-stub-runner + ${spring.cloud.contract.verifier.version} + + + + org.eclipse.aether + aether-api + ${aether.version} + provided + + + + org.apache.maven.shared + maven-filtering + 3.1.0 + + + + ch.qos.logback + logback-core + 1.1.3 + + + org.slf4j + slf4j-simple + 1.7.12 + test + + + + javax.inject + javax.inject + 1 + provided + + + org.apache.maven + maven-core + ${maven.version} + provided + + + + org.apache.maven + maven-plugin-api + ${maven.version} + provided + + + org.apache.maven + maven-model + + + org.apache.maven + maven-artifact + + + org.eclipse.sisu + org.eclipse.sisu.plexus + + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.plugin.plugin.version} + provided + + + + org.apache.maven + maven-archiver + 3.0.0 + + + org.codehaus.plexus + plexus-archiver + 3.1 + + + + io.takari.maven.plugins + takari-plugin-testing + 2.9.0 + test + + + io.takari.maven.plugins + takari-plugin-integration-testing + 2.9.0 + pom + test + + + org.apache.maven + maven-compat + ${maven.version} + test + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 2.4.0 + test + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + + attach-javadocs + + jar + + + + + + + + + spring + + true + + + + spring-snapshots + Spring Snapshots + http://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + http://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + http://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + http://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + http://repo.spring.io/libs-milestone-local + + false + + + + spring-plugin-snapshots + Spring Snapshots + http://repo.spring.io/plugins-snapshot-local + + true + + + + spring-plugin-milestones + Spring Milestones + http://repo.spring.io/plugins-milestone-local + + false + + + + + +
diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/etc/header.txt b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/etc/header.txt new file mode 100644 index 0000000000..771f93dfec --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/etc/header.txt @@ -0,0 +1,14 @@ + + Copyright 2013-${year} the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/CopyContracts.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/CopyContracts.groovy new file mode 100644 index 0000000000..25f2014644 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/CopyContracts.groovy @@ -0,0 +1,63 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.maven.execution.MavenSession +import org.apache.maven.model.Resource +import org.apache.maven.plugin.MojoExecutionException +import org.apache.maven.project.MavenProject +import org.apache.maven.shared.filtering.MavenFilteringException +import org.apache.maven.shared.filtering.MavenResourcesExecution +import org.apache.maven.shared.filtering.MavenResourcesFiltering + +@CompileStatic +@Slf4j +class CopyContracts { + + private final MavenProject project + private final MavenSession mavenSession + private final MavenResourcesFiltering mavenResourcesFiltering + + CopyContracts(MavenProject project, MavenSession mavenSession, MavenResourcesFiltering mavenResourcesFiltering) { + this.project = project + this.mavenSession = mavenSession + this.mavenResourcesFiltering = mavenResourcesFiltering + } + + void copy(File contractsDirectory, File outputDirectory) { + log.info('Copying Spring Cloud Contract Verifier contracts') + Resource testResource = new Resource(directory: contractsDirectory.absolutePath) + MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution( + resources: [testResource], + outputDirectory: new File(outputDirectory, 'contracts'), + mavenProject: project, + encoding: 'UTF-8', + mavenSession: mavenSession); + mavenResourcesExecution.injectProjectBuildFilters = false + mavenResourcesExecution.overwrite = true + mavenResourcesExecution.includeEmptyDirs = false + mavenResourcesExecution.filterFilenames = false + try { + mavenResourcesFiltering.filterResources(mavenResourcesExecution); + } catch (MavenFilteringException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/ManifestCreator.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/ManifestCreator.groovy new file mode 100644 index 0000000000..e6dd688d85 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/ManifestCreator.groovy @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven + +import org.apache.maven.model.Plugin +import org.apache.maven.project.MavenProject +import org.codehaus.plexus.archiver.jar.Manifest + +class ManifestCreator { + static Manifest createManifest(MavenProject project) { + Manifest manifest = new Manifest(); + Plugin verifierMavenPlugin = project.getBuildPlugins().find { it.artifactId == 'spring-cloud-contract-verifier-maven-plugin' } + manifest.addConfiguredAttribute(new Manifest.Attribute("Spring-Cloud-Contract-Verifier-Maven-Plugin-Version", verifierMavenPlugin.version)); + if (verifierMavenPlugin.getDependencies()) { + String verifierVersion = verifierMavenPlugin.getDependencies().find { + it.artifactId == 'spring-cloud-contract-verifier-core' + }.version + manifest.addConfiguredAttribute(new Manifest.Attribute("Spring-Cloud-Contract-Verifier-Version", verifierVersion)); + } + return manifest + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/AetherStubDownloaderFactory.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/AetherStubDownloaderFactory.groovy new file mode 100644 index 0000000000..f6aa07e085 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/AetherStubDownloaderFactory.groovy @@ -0,0 +1,49 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.maven.project.MavenProject +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.springframework.cloud.contract.stubrunner.AetherStubDownloader + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Named +@Singleton +@CompileStatic +@Slf4j +class AetherStubDownloaderFactory { + + private final MavenProject project + private final RepositorySystem repoSystem + + @Inject + AetherStubDownloaderFactory(RepositorySystem repoSystem, MavenProject project) { + this.repoSystem = repoSystem + this.project = project + } + + AetherStubDownloader build(RepositorySystemSession repoSession) { + return new AetherStubDownloader(repoSystem, project.remoteProjectRepositories, repoSession) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/LocalStubRunner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/LocalStubRunner.groovy new file mode 100644 index 0000000000..687c3e1cd0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/LocalStubRunner.groovy @@ -0,0 +1,38 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.cloud.contract.stubrunner.StubRunner +import org.springframework.cloud.contract.stubrunner.StubRunnerOptions + +import javax.inject.Named + +@Named +@CompileStatic +@Slf4j +class LocalStubRunner { + + StubRunner run(String contractsDir, StubRunnerOptions options) { + log.info("Launching StubRunner with contracts from ${contractsDir}") + StubRunner stubRunner = new StubRunner(options, contractsDir, new StubConfiguration(contractsDir)) + stubRunner.runStubs() + return stubRunner + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/RemoteStubRunner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/RemoteStubRunner.groovy new file mode 100644 index 0000000000..f85e6f80a2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/maven/stubrunner/RemoteStubRunner.groovy @@ -0,0 +1,52 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.eclipse.aether.RepositorySystemSession +import org.springframework.cloud.contract.stubrunner.* + +import javax.inject.Inject +import javax.inject.Named + +@Named +@CompileStatic +@Slf4j +class RemoteStubRunner { + + private final AetherStubDownloaderFactory aetherStubDownloaderFactory + + @Inject + RemoteStubRunner(AetherStubDownloaderFactory aetherStubDownloaderFactory) { + this.aetherStubDownloaderFactory = aetherStubDownloaderFactory + } + + BatchStubRunner run(StubRunnerOptions options, RepositorySystemSession repositorySystemSession) { + AetherStubDownloader stubDownloader = aetherStubDownloaderFactory.build(repositorySystemSession) + try { + log.debug("Launching StubRunner with args: $options") + BatchStubRunner stubRunner = new BatchStubRunnerFactory(options, stubDownloader).buildBatchStubRunner() + RunningStubs runningCollaborators = stubRunner.runStubs() + log.info(runningCollaborators.toString()) + return stubRunner + } catch (Exception e) { + log.error("An exception occurred while trying to execute the stubs: ${e.message}") + throw e + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/ConvertMojo.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/ConvertMojo.java new file mode 100644 index 0000000000..cd8c5063b1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/ConvertMojo.java @@ -0,0 +1,105 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import java.io.File; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.filtering.MavenResourcesFiltering; +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; +import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter; +import org.springframework.cloud.contract.verifier.wiremock.RecursiveFilesConverter; + +/** + * Convert Spring Cloud Contract Verifier contracts into WireMock stubs mappings. + *

+ * This goal allow to generate `stubs-jar` or execute `spring-cloud-contract-verifier:run` with generated WireMock mappings. + */ +@Mojo(name = "convert", requiresProject = false, defaultPhase = LifecyclePhase.PROCESS_TEST_RESOURCES) +public class ConvertMojo extends AbstractMojo { + + /** + * Directory containing Spring Cloud Contract Verifier contracts written using the GroovyDSL + */ + @Parameter(defaultValue = "${basedir}/src/test/resources/contracts") + private File contractsDirectory; + + /** + * Directory where the generated WireMock stubs from Groovy DSL should be placed. + * You can then mention them in your packaging task to create jar with stubs + */ + @Parameter(defaultValue = "${project.build.directory}/stubs") + private File outputDirectory; + + /** + * Directory containing contracts written using the GroovyDSL + *

+ * This parameter is only used when goal is executed outside of maven project. + */ + @Parameter(property = "contractsDirectory", defaultValue = "${basedir}") + private File source; + + @Parameter(property = "stubsDirectory", defaultValue = "${basedir}") + private File destination; + + @Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false") + private boolean skip; + + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession mavenSession; + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Component(role = MavenResourcesFiltering.class, hint = "default") + private MavenResourcesFiltering mavenResourcesFiltering; + + public void execute() throws MojoExecutionException, MojoFailureException { + + if (skip) { + getLog().info(String.format("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=%s", skip)); + return; + } + + new CopyContracts(project, mavenSession, mavenResourcesFiltering).copy(contractsDirectory, outputDirectory); + + final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties(); + config.setContractsDslDir(isInsideProject() ? contractsDirectory : source); + config.setStubsOutputDir(isInsideProject() ? new File(outputDirectory, "mappings") : destination); + + getLog().info("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings"); + getLog().info(String.format(" Spring Cloud Contract Verifier contracts directory: %s", config.getContractsDslDir())); + getLog().info(String.format("WireMock stubs mappings directory: %s", config.getStubsOutputDir())); + + RecursiveFilesConverter converter = new RecursiveFilesConverter(new DslToWireMockClientConverter(), config); + converter.processFiles(); + } + + private boolean isInsideProject() { + return mavenSession.getRequest().isProjectPresent(); + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateStubsMojo.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateStubsMojo.java new file mode 100644 index 0000000000..dfb657506a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateStubsMojo.java @@ -0,0 +1,100 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import java.io.File; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.codehaus.plexus.archiver.Archiver; +import org.codehaus.plexus.archiver.jar.JarArchiver; + +@Mojo(name = "generateStubs", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true) +public class GenerateStubsMojo extends AbstractMojo { + + private static final String STUB_MAPPING_FILE_PATTERN = "**/*.json"; + private static final String CONTRACT_FILE_PATTERN = "**/*.groovy"; + + @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) + private File projectBuildDirectory; + + @Parameter(property = "stubsDirectory", defaultValue = "${project.build.directory}/stubs") + private File outputDirectory; + + /** + * Set this to "true" to bypass Verifier execution.. + */ + @Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false") + private boolean skip; + + @Component + private MavenProjectHelper projectHelper; + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Component(role = Archiver.class, hint = "jar") + private JarArchiver archiver; + + @Parameter(defaultValue = "true") + private boolean attachContracts; + + @Parameter(defaultValue = "stubs") + private String classifier; + + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=" + skip); + return; + } + File stubsJarFile = createStubJar(outputDirectory); + projectHelper.attachArtifact(project, "jar", classifier, stubsJarFile); + } + + private File createStubJar(File stubsOutputDir) throws MojoFailureException, MojoExecutionException { + if (!stubsOutputDir.exists()) { + throw new MojoExecutionException("Stubs could not be found: " + stubsOutputDir.getAbsolutePath() + ".\nPlease make sure that spring-cloud-contract-verifier:convert was invoked"); + } + String stubArchiveName = project.getBuild().getFinalName() + "-" + classifier + ".jar"; + File stubsJarFile = new File(projectBuildDirectory, stubArchiveName); + try { + if (attachContracts) { + archiver.addDirectory(stubsOutputDir, new String[]{STUB_MAPPING_FILE_PATTERN, + CONTRACT_FILE_PATTERN }, new String[0]); + } else { + getLog().info("Skipping attaching Spring Cloud Contract Verifier contracts"); + archiver.addDirectory(stubsOutputDir, new String[]{STUB_MAPPING_FILE_PATTERN}, new String[]{ + CONTRACT_FILE_PATTERN }); + } + archiver.setCompress(true); + archiver.setDestFile(stubsJarFile); + archiver.addConfiguredManifest(ManifestCreator.createManifest(project)); + archiver.createArchive(); + } catch (Exception e) { + throw new MojoFailureException("Exception while packaging " + classifier + " jar.", e); + } + return stubsJarFile; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateTestsMojo.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateTestsMojo.java new file mode 100644 index 0000000000..ddd78f7c2d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/GenerateTestsMojo.java @@ -0,0 +1,142 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import java.io.File; +import java.util.List; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.springframework.cloud.contract.spec.ContractVerifierException; +import org.springframework.cloud.contract.verifier.TestGenerator; +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; +import org.springframework.cloud.contract.verifier.config.TestFramework; +import org.springframework.cloud.contract.verifier.config.TestMode; + +@Mojo(name = "generateTests", defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES, requiresDependencyResolution = ResolutionScope.TEST) +public class GenerateTestsMojo extends AbstractMojo { + + @Parameter(property = "spring.cloud.contract.verifier.contractsDirectory", defaultValue = "${project.basedir}/src/test/resources/contracts") + private File contractsDirectory; + + @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/contracts") + private File generatedTestSourcesDir; + + @Parameter(defaultValue = "org.springframework.cloud.contract.verifier.tests") + private String basePackageForTests; + + @Parameter + private String baseClassForTests; + + @Parameter(defaultValue = "MOCKMVC") + private TestMode testMode; + + @Parameter(defaultValue = "JUNIT") + private TestFramework testFramework; + + @Parameter + private String ruleClassForTests; + + @Parameter + private String nameSuffixForTests; + + /** + * Imports that should be added to generated tests + */ + @Parameter + private String[] imports; + + /** + * Static imports that should be added to generated tests + */ + @Parameter + private String[] staticImports; + + /** + * Patterns that should not be taken into account for processing + */ + @Parameter + private List excludedFiles; + + /** + * Patterns for which Spring Cloud Contract Verifier should generate @Ignored tests + */ + @Parameter + private List ignoredFiles; + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false") + private boolean skip; + + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=" + skip); + return; + } + getLog().info("Generating server tests source code for Spring Cloud Contract Verifier contract verification"); + final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties(); + config.setContractsDslDir(contractsDirectory); + config.setGeneratedTestSourcesDir(generatedTestSourcesDir); + config.setTargetFramework(testFramework); + config.setTestMode(testMode); + config.setBasePackageForTests(basePackageForTests); + config.setBaseClassForTests(baseClassForTests); + config.setRuleClassForTests(ruleClassForTests); + config.setNameSuffixForTests(nameSuffixForTests); + config.setImports(imports); + config.setStaticImports(staticImports); + config.setIgnoredFiles(ignoredFiles); + config.setExcludedFiles(excludedFiles); + project.addTestCompileSourceRoot(generatedTestSourcesDir.getAbsolutePath()); + if (getLog().isInfoEnabled()) { + getLog().info("Test Source directory: " + generatedTestSourcesDir.getAbsolutePath() + " added."); + getLog().info("Using " + config.getBaseClassForTests() + " as base class for test classes"); + } + try { + TestGenerator generator = new TestGenerator(config); + int generatedClasses = generator.generate(); + getLog().info("Generated " + generatedClasses + " test classes."); + } catch (ContractVerifierException e) { + throw new MojoExecutionException(String.format("Spring Cloud Contract Verifier Plugin exception: %s", e.getMessage()), e); + } + } + + public List getExcludedFiles() { + return excludedFiles; + } + + public void setExcludedFiles(List excludedFiles) { + this.excludedFiles = excludedFiles; + } + + public List getIgnoredFiles() { + return ignoredFiles; + } + + public void setIgnoredFiles(List ignoredFiles) { + this.ignoredFiles = ignoredFiles; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/RunMojo.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/RunMojo.java new file mode 100644 index 0000000000..c8b4b42f0f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/java/org/springframework/cloud/contract/verifier/maven/RunMojo.java @@ -0,0 +1,151 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.eclipse.aether.RepositorySystemSession; +import org.springframework.cloud.contract.stubrunner.BatchStubRunner; +import org.springframework.cloud.contract.stubrunner.StubRunner; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptions; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder; +import org.springframework.cloud.contract.verifier.maven.stubrunner.LocalStubRunner; + +import org.springframework.cloud.contract.verifier.maven.stubrunner.RemoteStubRunner; + +import static com.google.common.base.Strings.isNullOrEmpty; + +@Mojo(name = "run", requiresProject = false, requiresDependencyResolution = ResolutionScope.RUNTIME) +public class RunMojo extends AbstractMojo { + + @Parameter(defaultValue = "${repositorySystemSession}", readonly = true) + private RepositorySystemSession repoSession; + + @Parameter(defaultValue = "${project.build.directory}/contracts/mappings") + private File stubsDirectory; + + @Parameter(property = "stubsDirectory", defaultValue = "${basedir}") + private File destination; + + /** + * HTTP port for WireMock server + */ + @Parameter(property = "spring.cloud.contract.verifier.http.port", defaultValue = "8080") + private int httpPort; + + /** + * Set this to "true" to bypass verifier execution. + */ + @Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false") + private boolean skip; + + /** + * Set this to "true" to bypass verifier test generation. + */ + @Parameter(property = "spring.cloud.contract.verifier.skipTestOnly", defaultValue = "false") + private boolean skipTestOnly; + + @Parameter(property = "spring.cloud.contract.verifier.stubs") + private String stubs; + + @Parameter(property = "spring.cloud.contract.verifier.http.minPort", defaultValue = "10000") + private int minPort; + + @Parameter(property = "spring.cloud.contract.verifier.http.maxPort", defaultValue = "15000") + private int maxPort; + + /** + * Classifier used by stubs artifacts. + */ + @Parameter(defaultValue = "stubs") + private String stubsClassifier = "stubs"; + + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession mavenSession; + + private final LocalStubRunner localStubRunner; + + private final RemoteStubRunner remoteStubRunner; + + @Inject + public RunMojo(LocalStubRunner localStubRunner, RemoteStubRunner remoteStubRunner) { + this.localStubRunner = localStubRunner; + this.remoteStubRunner = remoteStubRunner; + } + + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip || skipTestOnly) { + getLog().info("Skipping verifier execution: spring.cloud.contract.verifier.skip=" + String.valueOf(skip)); + return; + } + BatchStubRunner batchStubRunner = null; + StubRunnerOptionsBuilder optionsBuilder = new StubRunnerOptionsBuilder() + .withStubsClassifier(stubsClassifier); + if (isNullOrEmpty(stubs)) { + StubRunnerOptions options = optionsBuilder + .withPort(httpPort) + .build(); + StubRunner stubRunner = localStubRunner.run(resolveStubsDirectory().getAbsolutePath(), options); + batchStubRunner = new BatchStubRunner(Collections.singleton(stubRunner)); + } else { + StubRunnerOptions options = optionsBuilder + .withStubs(stubs) + .withMinMaxPort(minPort, maxPort) + .build(); + batchStubRunner = remoteStubRunner.run(options, repoSession); + } + pressAnyKeyToContinue(); + if (batchStubRunner != null) { + try { + batchStubRunner.close(); + } catch (IOException e) { + throw new MojoExecutionException("Fail to close batch stub runner", e); + } + } + } + + private File resolveStubsDirectory() { + if (isInsideProject()) { + return stubsDirectory; + } else { + return destination; + } + } + + private void pressAnyKeyToContinue() { + getLog().info("Press ENTER to continue..."); + try { + System.in.read(); + } catch (Exception ignored) { + } + } + + private boolean isInsideProject() { + return mavenSession.getRequest().isProjectPresent(); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 0000000000..19a83714fc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,45 @@ + + + + + + org.apache.maven.lifecycle.Lifecycle + org.apache.maven.lifecycle.Lifecycle + spring-cloud-contract-verifier + + spring-cloud-contract-verifier + + spring-cloud-contract-verifier-not-used-phase + + + + org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:${project.version}:convert + + + org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:${project.version}:generateTests + + + org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:${project.version}:generateStubs + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex-content.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex-content.adoc new file mode 100644 index 0000000000..fad117ee37 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex-content.adoc @@ -0,0 +1,38 @@ +== More Complex Plugin Configuration + +Sample more complex configuration for Java Project with JUnit tests. + + +=== Project configuration for Accurest with JUnit tests and stub publishing + +[source,xml] +---- +include::{root_folder}/src/test/projects/complex-configuration/pom.xml[tags=plugin] +---- + +=== Base Test class + +[source,java] +---- +include::{root_folder}/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/BaseAccurest.java[] +---- + + +=== Sample additional matcher + +[source,java] +---- +include::{root_folder}/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/matchers/CustomMatchers.java[] +---- + +=== Sample contract using matcher + +[source,java] +---- +include::{root_folder}/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsNotFraud.groovy[] +---- + + +=== Project source code + +https://github.com/Codearte/accurest-maven-plugin/tree/master/src/test/projects/complex-configuration \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex.adoc new file mode 100644 index 0000000000..e29ea2b549 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/complex.adoc @@ -0,0 +1,3 @@ +:root_folder: ../../.. + +include::complex.adoc[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/index.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/index.adoc new file mode 100644 index 0000000000..f73fbb3dc9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/index.adoc @@ -0,0 +1,10 @@ += Spring Cloud Contract Verifier Maven Plugin + +Just to make long story short - Spring Cloud Contract Verifier is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. + + * Stubs mappings to be used by WireMock when doing integration testing on the client code (client tests). + + * Acceptance tests used to verify if server-side implementation of the API is compliant with the contract (server tests). + +Spring Cloud Contract Verifier moves TDD to the level of software architecture. + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/junit.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/junit.adoc new file mode 100644 index 0000000000..ad56a34c79 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/junit.adoc @@ -0,0 +1,29 @@ +== Java Project with JUnit Tests + +Sample minimal configuration for Java Project with JUnit tests. + +=== Test dependencies for generated contract verification tests + +[source,xml] +---- +include::../../../src/test/projects/spring-boot-java/pom.xml[tags=dependencies] +---- + +=== Project configuration for Accurest with JUnit tests and stub publishing + +[source,xml] +---- +include::../../../src/test/projects/spring-boot-java/pom.xml[tags=plugin] +---- + +=== Base Test class + +[source,java] +---- +include::../../../src/test/projects/spring-boot-java/src/test/java/hello/BaseAccurest.java[] +---- + + +=== Project source code + +https://github.com/Codearte/accurest-maven-plugin/tree/master/src/test/projects/spring-boot-java \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/spock.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/spock.adoc new file mode 100644 index 0000000000..6e262ede51 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/spock.adoc @@ -0,0 +1,29 @@ +== Groovy Project with Spock Specifications + +Sample minimal configuration for Groovy Project with Spock Specification + +=== Test dependencies for generated contract verification tests + +[source,xml] +---- +include::../../../src/test/projects/spring-boot-groovy/pom.xml[tags=dependencies] +---- + +=== Project configuration for Accurest, Groovy, Spock specifications and stub publishing + + +[source,xml] +---- +include::../../../src/test/projects/spring-boot-groovy/pom.xml[tags=plugin] +---- + +=== Base Specification class + +[source,java] +---- +include::../../../src/test/projects/spring-boot-groovy/src/test/groovy/hello/BaseAccurest.groovy[] +---- + +=== Project source code + +https://github.com/Codearte/accurest-maven-plugin/tree/master/src/test/projects/spring-boot-groovy diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/usage.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/usage.adoc new file mode 100644 index 0000000000..825f738e45 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/asciidoc/usage.adoc @@ -0,0 +1,35 @@ += Usage + +== Converting Accurest GroovyDSL into WireMock stub mappings + + mvn org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:convert + +or shortly footnoteref:[prefix,Additional configuration inside `~/.m2/settings.xml` is required: `io.codearte.accurest`.] + + mvn spring-cloud-contract-verifier:convert + +For more information please go to the Accurest Wiki or Plugin Documentation Site. + +== Accurest Stub Runner + +Run stubs mappings from current directory: + + mvn org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:run + +or shortly footnoteref:[prefix] + + mvn spring-cloud-contract-verifier:run + +== Running stubs from repository + + mvn spring-cloud-contract-verifier:run -Dstubs="org.springframework:gs-rest-service" + +where `org.springframework:gs-rest-service` is artifact with stubs classifier contains wiremock mappings. + +== Project configuration + +[source,xml] +---- +include::../../../src/test/projects/spring-boot-java/pom.xml[tags=plugin] +---- + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/site.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/site.xml new file mode 100644 index 0000000000..8d23c5f4d2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/site/site.xml @@ -0,0 +1,58 @@ + + + + ${project.name} + + + + + + org.apache.maven.skins + maven-fluido-skin + 1.5 + + + + true + true + + codearte/accurest-maven-plugin + right + orange + + » + + + + +

+ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginIT.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginIT.java new file mode 100644 index 0000000000..cc2c878333 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginIT.java @@ -0,0 +1,113 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import java.io.File; + +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.takari.maven.testing.TestProperties; +import io.takari.maven.testing.TestResources; +import io.takari.maven.testing.executor.MavenRuntime; +import io.takari.maven.testing.executor.MavenVersions; +import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner; + +@RunWith(MavenJUnitTestRunner.class) +@MavenVersions({ "3.3.3" }) +public class PluginIT { + + @Rule + public final TestResources resources = new TestResources(); + + private final TestProperties properties = new TestProperties(); + + private final MavenRuntime maven; + + public PluginIT(MavenRuntime.MavenRuntimeBuilder mavenBuilder) throws Exception { + this.maven = mavenBuilder.withCliOptions("-B", "-U").build(); + } + + @Test + public void should_build_project_Spring_Boot_Groovy_with_Accurest() throws Exception { + File basedir = resources.getBasedir("spring-boot-groovy"); + maven.forProject(basedir) + .execute("package") + .assertErrorFreeLog() + .assertLogText("Generating server tests source code for Spring Cloud Contract Verifier contract verification") + .assertLogText("Generated 1 test classes.") + .assertLogText("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings") + .assertLogText("Creating new json") + .assertLogText("Running org.springframework.cloud.contract.verifier.tests.ContractVerifierSpec") + .assertErrorFreeLog(); + } + + @Test + public void should_build_project_Spring_Boot_Java_with_Accurest() throws Exception { + File basedir = resources.getBasedir("spring-boot-java"); + maven.forProject(basedir) + .execute("package") + .assertErrorFreeLog() + .assertLogText("Generating server tests source code for Spring Cloud Contract Verifier contract verification") + .assertLogText("Generated 1 test classes.") + .assertLogText("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings") + .assertLogText("Creating new json") + .assertLogText("Running org.springframework.cloud.contract.verifier.tests.ContractVerifierTest") + .assertErrorFreeLog(); + } + + @Test + public void should_build_project_with_plugin_extension() throws Exception { + File basedir = resources.getBasedir("plugin-extension"); + maven.forProject(basedir) + .execute("package") + .assertErrorFreeLog() + .assertLogText("Generating server tests source code for Spring Cloud Contract Verifier contract verification") + .assertLogText("Generated 1 test classes.") + .assertLogText("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings") + .assertLogText("Creating new json") + .assertLogText("Running org.springframework.cloud.contract.verifier.tests.ContractVerifierTest") + .assertErrorFreeLog(); + } + + @Test + @Ignore("Ignored, because of bug accurest#245") + public void should_build_project_project_with_complex_configuration() throws Exception { + File basedir = resources.getBasedir("complex-configuration"); + maven.forProject(basedir) + .execute("package") + .assertErrorFreeLog() + .assertLogText("Tests run: 2, Failures: 0, Errors: 0, Skipped: 1") + .assertLogText("Running com.blogspot.toomuchcoding.frauddetection.ContractVerifierTest") + .assertErrorFreeLog(); + } + + @Test + public void should_convert_Accurest_Contracts_to_WireMock_Stubs_mappings() throws Exception { + File basedir = resources.getBasedir("pomless"); + properties.getPluginVersion(); + maven.forProject(basedir) + .withCliOption("-X") + .execute(String.format("org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:%s:convert", + properties.getPluginVersion())) + .assertLogText("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings") + .assertLogText("Creating new json") + .assertErrorFreeLog(); + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginUnitTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginUnitTest.java new file mode 100644 index 0000000000..5484243411 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/java/org/springframework/cloud/contract/verifier/maven/PluginUnitTest.java @@ -0,0 +1,115 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.contract.verifier.maven; + +import java.io.File; + +import org.junit.Rule; +import org.junit.Test; + +import io.takari.maven.testing.TestMavenRuntime; +import io.takari.maven.testing.TestResources; + +import static io.takari.maven.testing.TestMavenRuntime.newParameter; +import static io.takari.maven.testing.TestResources.assertFilesNotPresent; +import static io.takari.maven.testing.TestResources.assertFilesPresent; + +public class PluginUnitTest { + + @Rule + public final TestResources resources = new TestResources(); + + @Rule + public final TestMavenRuntime maven = new TestMavenRuntime(); + + @Test + public void shouldGenerateWireMockStubsInDefaultLocation() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "convert"); + assertFilesPresent(basedir, "target/stubs/mappings/Sample.json"); + assertFilesNotPresent(basedir, "target/stubs/mappings/Messaging.json"); + } + + @Test + public void shouldGenerateWireMockFromStubsDirectory() throws Exception { + File basedir = resources.getBasedir("withStubs"); + maven.executeMojo(basedir, "convert", newParameter("contractsDirectory", "src/test/resources/stubs")); + assertFilesPresent(basedir, "target/stubs/mappings/Sample.json"); + } + + @Test + public void shouldCopyContracts() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "convert"); + assertFilesPresent(basedir, "target/stubs/contracts/Sample.groovy"); + assertFilesPresent(basedir, "target/stubs/contracts/Messaging.groovy"); + } + @Test + public void shouldGenerateWireMockStubsInSelectedLocation() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "convert", newParameter("outputDirectory", "target/foo")); + assertFilesPresent(basedir, "target/foo/mappings/Sample.json"); + } + + @Test + public void shouldGenerateContractSpecificationInDefaultLocation() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "generateTests", newParameter("testFramework", "SPOCK")); + assertFilesPresent(basedir, + "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierSpec.groovy"); + } + + @Test + public void shouldGenerateContractTestsInDefaultLocation() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "generateTests"); + assertFilesPresent(basedir, + "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java"); + } + + @Test + public void shouldGenerateContractTestsWithCustomImports() throws Exception { + File basedir = resources.getBasedir("basic"); + maven.executeMojo(basedir, "generateTests", newParameter("imports", "")); + assertFilesPresent(basedir, + "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java"); + } + + @Test + public void shouldGenerateStubs() throws Exception { + File basedir = resources.getBasedir("generatedStubs"); + maven.executeMojo(basedir, "generateStubs"); + assertFilesPresent(basedir, "target/sample-project-0.1-stubs.jar"); + } + + @Test + public void shouldGenerateStubsWithMappingsOnly() throws Exception { + File basedir = resources.getBasedir("generatedStubs"); + maven.executeMojo(basedir, "generateStubs", newParameter("attachContracts", "false")); + assertFilesPresent(basedir, "target/sample-project-0.1-stubs.jar"); + // FIXME: add assertion for jar content + } + + @Test + public void shouldGenerateStubsWithCustomClassifier() throws Exception { + File basedir = resources.getBasedir("generatedStubs"); + maven.executeMojo(basedir, "generateStubs", newParameter("classifier", "foo")); + assertFilesPresent(basedir, "target/sample-project-0.1-foo.jar"); + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/pom.xml new file mode 100644 index 0000000000..e7ee761ca9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/pom.xml @@ -0,0 +1,37 @@ + + + + 4.0.0 + + io.codearte.accurest.sample + sample-project + 0.1 + + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Messaging.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Messaging.groovy new file mode 100644 index 0000000000..b21f448049 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Messaging.groovy @@ -0,0 +1,37 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Sample.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Sample.groovy new file mode 100644 index 0000000000..094820225b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/basic/src/test/resources/contracts/Sample.groovy @@ -0,0 +1,34 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url('/users') { + + } + headers { + header 'Content-Type': 'application/json' + } + body '''{ "login" : "john", "name": "John The Contract" }''' + } + response { + status 200 + headers { + header 'Location': '/users/john' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/.gitignore b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/.gitignore new file mode 100644 index 0000000000..1de565933b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/pom.xml new file mode 100644 index 0000000000..efebefaf10 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/pom.xml @@ -0,0 +1,131 @@ + + + + 4.0.0 + + com.blogspot.toomuchcoding.frauddetection + frauddetection + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + ch.qos.logback + logback-classic + + + + + + com.jayway.restassured + rest-assured + 2.9.0 + test + + + com.jayway.restassured + spring-mock-mvc + 2.9.0 + test + + + com.toomuchcoding.jsonassert + jsonassert + 0.4.7 + test + + + org.assertj + assertj-core + 2.4.0 + test + + + + + + 1.8 + ${it-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + ${accurest-plugin.version} + + + + convert + generateStubs + generateTests + + + src/test/accurest + com.blogspot.toomuchcoding.frauddetection + MOCKMVC + JUNIT + stubs + Test + org.junit.rules.ErrorCollector + + com.blogspot.toomuchcoding.frauddetection.matchers.CustomMatchers.* + + + com.blogspot.toomuchcoding.frauddetection.matchers.CustomMatchers + + + broken** + + + shouldMarkClientAsFraud.groovy + + + + + + com.blogspot.toomuchcoding.frauddetection.BaseAccurest + + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/Application.java new file mode 100644 index 0000000000..bedd785957 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/Application.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/FraudDetectionController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/FraudDetectionController.java new file mode 100644 index 0000000000..ce53beb419 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/FraudDetectionController.java @@ -0,0 +1,56 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection; + +import java.math.BigDecimal; + +import com.blogspot.toomuchcoding.frauddetection.model.FraudCheck; +import com.blogspot.toomuchcoding.frauddetection.model.FraudCheckResult; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.blogspot.toomuchcoding.frauddetection.model.FraudCheckStatus.FRAUD; +import static com.blogspot.toomuchcoding.frauddetection.model.FraudCheckStatus.OK; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +@RestController +public class FraudDetectionController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + private static final String NO_REASON = null; + private static final String AMOUNT_TOO_HIGH = "Amount too high"; + private static final BigDecimal MAX_AMOUNT = new BigDecimal("5000"); + + @RequestMapping( + value = "/fraudcheck", + method = PUT, + consumes = FRAUD_SERVICE_JSON_VERSION_1, + produces = FRAUD_SERVICE_JSON_VERSION_1) + public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FRAUD, AMOUNT_TOO_HIGH); + } + return new FraudCheckResult(OK, NO_REASON); + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheck.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheck.java new file mode 100644 index 0000000000..67f51b8a39 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheck.java @@ -0,0 +1,45 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudCheck { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckResult.java new file mode 100644 index 0000000000..a5fba329aa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckResult.java @@ -0,0 +1,48 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection.model; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudCheckResult() { + } + + public FraudCheckResult(FraudCheckStatus fraudCheckStatus, String rejectionReason) { + this.fraudCheckStatus = fraudCheckStatus; + this.rejectionReason = rejectionReason; + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..8ea884adb3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/main/java/com/blogspot/toomuchcoding/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/brokenContract.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/brokenContract.groovy new file mode 100644 index 0000000000..48435cb1a2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/brokenContract.groovy @@ -0,0 +1,44 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 999 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..9cd6dec8b1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsFraud.groovy @@ -0,0 +1,43 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":99999} + """ + ) + headers { + header("""Content-Type""", """application/vnd.fraud.v1+json""") + } + + } + response { + status 200 + body( """{ + "fraudCheckStatus": "${value(client('FRAUD'), server(regex('[A-Z]{5}')))}", + "rejectionReason": "Amount too high" +}""") + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsNotFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..92b2646567 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/contracts/shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,44 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/BaseAccurest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/BaseAccurest.java new file mode 100644 index 0000000000..0bc555db19 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/BaseAccurest.java @@ -0,0 +1,30 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + +import org.junit.Before; + +public class BaseAccurest { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController()); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/matchers/CustomMatchers.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/matchers/CustomMatchers.java new file mode 100644 index 0000000000..44d5331c9e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/complex-configuration/src/test/java/com/blogspot/toomuchcoding/frauddetection/matchers/CustomMatchers.java @@ -0,0 +1,27 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blogspot.toomuchcoding.frauddetection.matchers; + +import org.junit.Assert; + +public class CustomMatchers { + + public static void assertThatRejectionReasonIsNull(String rejectionReason) { + Assert.assertNull(rejectionReason); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/pom.xml new file mode 100644 index 0000000000..e7ee761ca9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/pom.xml @@ -0,0 +1,37 @@ + + + + 4.0.0 + + io.codearte.accurest.sample + sample-project + 0.1 + + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/pom.xml new file mode 100644 index 0000000000..7d5f62d156 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + org.springframework + gs-rest-service + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + ch.qos.logback + logback-classic + + + + + + com.jayway.restassured + rest-assured + 2.9.0 + test + + + com.jayway.restassured + spring-mock-mvc + 2.9.0 + test + + + com.toomuchcoding.jsonassert + jsonassert + 0.4.7 + test + + + org.assertj + assertj-core + 2.4.1 + test + + + + + + 1.8 + ${it-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + ${accurest-plugin.version} + true + + hello.BaseAccurest + + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Application.java new file mode 100644 index 0000000000..bb79fe49da --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Application.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Greeting.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Greeting.java new file mode 100644 index 0000000000..f22f746a8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/Greeting.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +public class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/GreetingController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/GreetingController.java new file mode 100644 index 0000000000..f6db9e06b9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/main/java/hello/GreetingController.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), + String.format(template, name)); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/java/hello/BaseAccurest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/java/hello/BaseAccurest.java new file mode 100644 index 0000000000..5d04fddb2f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/java/hello/BaseAccurest.java @@ -0,0 +1,30 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + +import org.junit.Before; + +public class BaseAccurest { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new GreetingController()); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_ok.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_ok.groovy new file mode 100644 index 0000000000..4a5d9a792f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_ok.groovy @@ -0,0 +1,37 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + urlPath('/greeting') { + queryParameters { + parameter 'name': 'Something' + } + } + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body( + id: 1, + content: "Hello, Something!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_with_default_value.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_with_default_value.groovy new file mode 100644 index 0000000000..5b65b65ca7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/plugin-extension/src/test/resources/contracts/greetings_with_default_value.groovy @@ -0,0 +1,33 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + url('/greeting') + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body( + id: 1, + content: "Hello, World!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/pomless/Sample.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/pomless/Sample.groovy new file mode 100644 index 0000000000..094820225b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/pomless/Sample.groovy @@ -0,0 +1,34 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url('/users') { + + } + headers { + header 'Content-Type': 'application/json' + } + body '''{ "login" : "john", "name": "John The Contract" }''' + } + response { + status 200 + headers { + header 'Location': '/users/john' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml new file mode 100644 index 0000000000..b925aca3d1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml @@ -0,0 +1,145 @@ + + + + 4.0.0 + + org.springframework + gs-rest-service + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.spockframework + spock-core + test + + + com.jayway.restassured + rest-assured + 2.9.0 + test + + + com.jayway.restassured + spring-mock-mvc + 2.9.0 + test + + + com.toomuchcoding.jsonassert + jsonassert + 0.4.7 + test + + + + + + 1.8 + ${it-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + ${accurest-plugin.version} + + + + convert + generateStubs + generateTests + + + + + hello.BaseAccurest + SPOCK + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + + testCompile + + + + + + + ${project.basedir}/src/test/groovy + + **/*.groovy + + + + ${project.build.directory}/generated-test-sources/contracts + + **/*.groovy + + + + + + + maven-surefire-plugin + 2.19 + + + **/*Spec.java + + + + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Application.java new file mode 100644 index 0000000000..bb79fe49da --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Application.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Greeting.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Greeting.java new file mode 100644 index 0000000000..f22f746a8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/Greeting.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +public class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/GreetingController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/GreetingController.java new file mode 100644 index 0000000000..f6db9e06b9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/main/java/hello/GreetingController.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), + String.format(template, name)); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/groovy/hello/BaseAccurest.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/groovy/hello/BaseAccurest.groovy new file mode 100644 index 0000000000..82707cf2ef --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/groovy/hello/BaseAccurest.groovy @@ -0,0 +1,28 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import spock.lang.Specification + +public class BaseAccurest extends Specification { + + def setup() { + RestAssuredMockMvc.standaloneSetup(new GreetingController()) + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_ok.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_ok.groovy new file mode 100644 index 0000000000..e782af7b50 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_ok.groovy @@ -0,0 +1,37 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + urlPath('/greeting') { + queryParameters { + parameter 'name': 'Something' + } + } + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body ( + id: 1, + content: "Hello, Something!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_with_default_value.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_with_default_value.groovy new file mode 100644 index 0000000000..ad5e45965c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-groovy/src/test/resources/contracts/greetings_with_default_value.groovy @@ -0,0 +1,33 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + url('/greeting') + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body ( + id: 1, + content: "Hello, World!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/pom.xml new file mode 100644 index 0000000000..26dd0112b4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/pom.xml @@ -0,0 +1,110 @@ + + + + 4.0.0 + + org.springframework + gs-rest-service + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + ch.qos.logback + logback-classic + + + + + + com.jayway.restassured + rest-assured + 2.9.0 + test + + + com.jayway.restassured + spring-mock-mvc + 2.9.0 + test + + + com.toomuchcoding.jsonassert + jsonassert + 0.4.7 + test + + + org.assertj + assertj-core + 2.4.1 + test + + + + + + 1.8 + ${it-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + ${accurest-plugin.version} + + + + convert + generateStubs + generateTests + + + + + hello.BaseAccurest + + + + + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Application.java new file mode 100644 index 0000000000..bb79fe49da --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Application.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Greeting.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Greeting.java new file mode 100644 index 0000000000..f22f746a8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/Greeting.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +public class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/GreetingController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/GreetingController.java new file mode 100644 index 0000000000..f6db9e06b9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/main/java/hello/GreetingController.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), + String.format(template, name)); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/java/hello/BaseAccurest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/java/hello/BaseAccurest.java new file mode 100644 index 0000000000..5d04fddb2f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/java/hello/BaseAccurest.java @@ -0,0 +1,30 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package hello; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + +import org.junit.Before; + +public class BaseAccurest { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new GreetingController()); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_ok.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_ok.groovy new file mode 100644 index 0000000000..4a5d9a792f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_ok.groovy @@ -0,0 +1,37 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + urlPath('/greeting') { + queryParameters { + parameter 'name': 'Something' + } + } + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body( + id: 1, + content: "Hello, Something!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_with_default_value.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_with_default_value.groovy new file mode 100644 index 0000000000..5b65b65ca7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/spring-boot-java/src/test/resources/contracts/greetings_with_default_value.groovy @@ -0,0 +1,33 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + priority 2 + request { + method 'GET' + url('/greeting') + headers { + header 'Content-Type': 'application/json' + } + } + response { + status 200 + body( + id: 1, + content: "Hello, World!" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/pom.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/pom.xml new file mode 100644 index 0000000000..e7ee761ca9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/pom.xml @@ -0,0 +1,37 @@ + + + + 4.0.0 + + io.codearte.accurest.sample + sample-project + 0.1 + + + + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/src/test/resources/stubs/Sample.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/src/test/resources/stubs/Sample.groovy new file mode 100644 index 0000000000..094820225b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/withStubs/src/test/resources/stubs/Sample.groovy @@ -0,0 +1,34 @@ +/** + * + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url('/users') { + + } + headers { + header 'Content-Type': 'application/json' + } + body '''{ "login" : "john", "name": "John The Contract" }''' + } + response { + status 200 + headers { + header 'Location': '/users/john' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/.gitignore b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/.gitignore new file mode 100644 index 0000000000..05f150069a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/.gitignore @@ -0,0 +1,11 @@ +*.class +*.iml + +.idea/ +.gradle/ +target/ +build/ + +hs_err_pid* +.DS_Store +*.log \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/LICENSE b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/LICENSE new file mode 100644 index 0000000000..e06d208186 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/README.adoc new file mode 100644 index 0000000000..9bd94b36f0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/README.adoc @@ -0,0 +1,221 @@ +// Do not edit this file (e.g. go instead to src/main/asciidoc) + +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core +:stubrunner_core_path: {core_path}/spring-cloud-contract-stub-runner +:documentation_url: http://codearte.github.io/accurest + += Spring Cloud Contract Verifier + +:images_url: https://raw.githubusercontent.com/Codearte/accurest/master/docs/src/main/asciidoc/images + +== Introduction + +IMPORTANT: http://codearte.github.io/accurest/deprecated[The documentation to the deprecated Accurest project in version 1.1.0 is available here.] + +TIP: The Accurest project was initially started by Marcin Grzejszczak and Jakub Kubrynski (http://codearte.io[codearte.io]) + +Just to make long story short - Spring Cloud Contract Verifier is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped +with __Contract Definition Language__ (DSL). Contract definitions are used to produce following resources: + +* JSON stub definitions to be used by Wiremock when doing integration testing on the client code (__client tests__). +Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier. +* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to +* Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). +Full test is generated by Spring Cloud Contract Verifier. + +Spring Cloud Contract Verifier moves TDD to the level of software architecture. + +=== Why? + +Let us assume that we have a system comprising of multiple microservices: + +image::{images_url}/Deps.png[Microservices Architecture] + +==== Testing issues + +If we wanted to test the application in top left corner if it can communicate with other services then we could do one of two things: + +- deploy all microservices and perform end to end tests +- mock other microservices in unit / integration tests + +Both have their advantages but also a lot of disadvantages. Let's focus on the latter. + +*Deploy all microservices and perform end to end tests* + +Advantages: + +- simulates production +- tests real communication between services + +Disadvantages: + +- to test one microservice we would have to deploy 6 microservices, a couple of databases etc. +- the environment where the tests would be conducted would be locked for a single suite of tests (i.e. nobody else would be able to run the tests in the meantime). +- long to run +- very late feedback +- extremely hard to debug + +*Mock other microservices in unit / integration tests* + +Advantages: + +- very fast feedback +- no infrastructure requirements + +Disadvantages: + +- the implementor of the service creates stubs thus they might have nothing to do with the reality +- you can go to production with passing tests and failing production + +To solve the aforementioned issues Spring Cloud Contract Verifier with Stub Runner were created. Their main idea is to give you very fast feedback, without the need +to set up the whole world of microservices. + +image::{images_url}/Stubs1.png[Stubbed Services] + +If you work on stubs then the only applications you need are those that your application is using directly. + +image::{images_url}/Stubs2.png[Stubbed Services] + +Spring Cloud Contract Verifier gives you the certainty that the stubs that you're using were created by the service that you're calling. Also if you can use them it means that they were +tested against the producer's side. In other words - you can trust those stubs. + + +=== Purposes + +The main purposes of Spring Cloud Contract Verifier with Stub Runner are: + + - to ensure that WireMock / Messaging stubs (used when developing the client) are doing exactly what actual server-side implementation will do, + - to promote ATDD method and Microservices architectural style, + - to provide a way to publish changes in contracts that are immediately visible on both sides, + - to generate boilerplate test code used on the server side. + +=== Client Side + +During the tests you want to have a Wiremock instance / Messaging route up and running that simulates the service Y. +You would like to feed that instance with a proper stub definition. That stub definition would need +to be valid and should also be reusable on the server side. + +__Summing it up:__ On this side, in the stub definition, you can use patterns for request stubbing and you need exact +values for responses. + +=== Server Side + +Being a service Y since you are developing your stub, you need to be sure that it's actually resembling your +concrete implementation. You can't have a situation where your stub acts in one way and your application on +production behaves in a different way. + +That's why from the provided stub acceptance tests will be generated that will ensure +that your application behaves in the same way as you define in your stub. + +__Summing it up:__ On this side, in the stub definition, you need exact values as request and can use patterns/methods +for response verification. + +=== Dependencies + +Spring Cloud Contract Verifier and Stub Runner are using the following libraries + +- http://wiremock.org/[WireMock] +- https://github.com/jayway/JsonPath[Jayway JSONPath] +- https://github.com/marcingrzejszczak/jsonassert[JSONAssert from Marcin Grzejszczak] + +=== Additional links + +Below you can find some resources related to Spring Cloud Contract Verifier and Stub Runner. Note that some can be outdated since the Spring Cloud Contract Verifier project +is under constant development. + +==== Videos + +*Olga Maciaszek-Sharma talking about Accurest (Spring Cloud Contract Verifier predecessor)* + +video::daafmTYFoDU[youtube] + +*Marcin Grzejszczak and Jakub Kubryński talking about Accurest (Spring Cloud Contract Verifier predecessor)* + +video::130779882[vimeo] + +==== Readings + +- http://www.slideshare.net/MarcinGrzejszczak/stick-to-the-rules-consumer-driven-contracts-201507-confitura[Slides from Marcin Grzejszczak's talk about Accurest] +- http://toomuchcoding.com/blog/categories/accurest/[Accurest related articles from Marcin Grzejszczak's blog] + +=== Samples + +Here you can find some https://github.com/Codearte/accurest-samples[working samples]. Check the readme of each project for more information. + +=== Why use Spring Cloud Contract Verifier and not X ? + +For the time being Spring Cloud Contract Verifier is a JVM based tool. So it could be your first pick when you're already creating +software for the JVM. This project has a lot of really interesting features but especially quite a few of them definitely make +Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Contract (CDC) tooling. Out of many the most interesting are: + +- Possibility to do CDC with messaging +- Clear and easy to use, statically typed DSL +- Possibility to copy paste your current JSON file to the contract and only edit its elements +- Automatic generation of tests from the defined Contract +- Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory +- Spring Cloud integration - no discovery service is needed for integration tests + +== Links + +Here you can find interesting links related to Spring Cloud Contract Verifier: + +- https://github.com/Codearte/accurest[Spring Cloud Contract Verifier Github Repository] +- http://codearte.github.io/accurest[Spring Cloud Contract Verifier Documentation] +- http://codearte.github.io/accurest/deprecated[Accurest Legacy Documentation] +- https://codearte.github.io/accurest/#spring-cloud-contract-stub-runner[Spring Cloud Contract Stub Runner Documentation] +- http://codearte.github.io/accurest/#stub-runner-for-messaging[Spring Cloud Contract Stub Runner Messaging Documentation] +- https://gitter.im/Codearte/accurest[Spring Cloud Contract Verifier Gitter] +- https://github.com/Codearte/accurest-maven-plugin[Spring Cloud Contract Verifier Maven Plugin] + +== Documentation + +You can read more about Spring Cloud Contract Verifier by reading the {documentation_url}[docs] + +== Contributing + +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. + +=== Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +https://support.springsource.com/spring_committer_signup[contributor's agreement]. +Signing the contributor's agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. + +=== Code of Conduct +This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of +conduct]. By participating, you are expected to uphold this code. Please report +unacceptable behavior to spring-code-of-conduct@pivotal.io. + +=== Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + +* Use the Spring Framework code format conventions. If you use Eclipse + you can import formatter settings using the + `eclipse-code-formatter.xml` file from the + https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring + Cloud Build] project. If using IntelliJ, you can use the + http://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter + Plugin] to import the same file. +* Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +* Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +* Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +* Add some Javadocs and, if you change the namespace, some XSD doc elements. +* A few unit tests would help a lot as well -- someone has to do it. +* If no-one else is using your branch, please rebase it against the current master (or + other target branch in the main project). +* When writing a commit message please follow http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], + if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit + message (where XXXX is the issue number). \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/asciidoctor.css b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/asciidoctor.css new file mode 100644 index 0000000000..06d52e688a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/asciidoctor.css @@ -0,0 +1,399 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ +/* Remove the comments around the @import statement below when using this as a custom stylesheet */ +/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400";*/ +article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} +audio,canvas,video{display:inline-block} +audio:not([controls]){display:none;height:0} +[hidden],template{display:none} +script{display:none!important} +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +body{margin:0} +a{background:transparent} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +abbr[title]{border-bottom:1px dotted} +b,strong{font-weight:bold} +dfn{font-style:italic} +hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:monospace;font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} +input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} +input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} +html,body{font-size:100%} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +#map_canvas img,#map_canvas embed,#map_canvas object,.map_canvas img,.map_canvas embed,.map_canvas object{max-width:none!important} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +.antialiased,body{-webkit-font-smoothing:antialiased} +img{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6} +.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} +div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} +a{color:#2156a5;text-decoration:underline;line-height:inherit} +a:hover,a:focus{color:#1d4b8f} +a img{border:none} +p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} +p aside{font-size:.875em;line-height:1.35;font-style:italic} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} +h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} +h1{font-size:2.125em} +h2{font-size:1.6875em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} +h4,h5{font-size:1.125em} +h6{font-size:1em} +hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} +em,i{font-style:italic;line-height:inherit} +strong,b{font-weight:bold;line-height:inherit} +small{font-size:60%;line-height:inherit} +code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} +ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} +ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em} +ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} +ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} +ul.square{list-style-type:square} +ul.circle{list-style-type:circle} +ul.disc{list-style-type:disc} +ul.no-bullet{list-style:none} +ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} +dl dt{margin-bottom:.3125em;font-weight:bold} +dl dd{margin-bottom:1.25em} +abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} +abbr{text-transform:none} +blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} +blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} +blockquote cite:before{content:"\2014 \0020"} +blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} +blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} +@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} +h1{font-size:2.75em} +h2{font-size:2.3125em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} +h4{font-size:1.4375em}}table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} +table thead,table tfoot{background:#f7f8f7;font-weight:bold} +table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} +table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} +table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} +table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} +h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} +.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} +.clearfix:after,.float-group:after{clear:both} +*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} +pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} +.keyseq{color:rgba(51,51,51,.8)} +kbd{display:inline-block;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.4;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:-.15em .15em 0 .15em;padding:.2em .6em .2em .5em;vertical-align:middle;white-space:nowrap} +.keyseq kbd:first-child{margin-left:0} +.keyseq kbd:last-child{margin-right:0} +.menuseq,.menu{color:rgba(0,0,0,.8)} +b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} +b.button:before{content:"[";padding:0 3px 0 2px} +b.button:after{content:"]";padding:0 2px 0 3px} +p a>code:hover{color:rgba(0,0,0,.9)} +#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} +#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} +#header:after,#content:after,#footnotes:after,#footer:after{clear:both} +#content{margin-top:1.25em} +#content:before{content:none} +#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} +#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8} +#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px} +#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} +#header .details span:first-child{margin-left:-.125em} +#header .details span.email a{color:rgba(0,0,0,.85)} +#header .details br{display:none} +#header .details br+span:before{content:"\00a0\2013\00a0"} +#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} +#header .details br+span#revremark:before{content:"\00a0|\00a0"} +#header #revnumber{text-transform:capitalize} +#header #revnumber:after{content:"\00a0"} +#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} +#toc{border-bottom:1px solid #efefed;padding-bottom:.5em} +#toc>ul{margin-left:.125em} +#toc ul.sectlevel0>li>a{font-style:italic} +#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} +#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} +#toc a{text-decoration:none} +#toc a:active{text-decoration:underline} +#toctitle{color:#7a2518;font-size:1.2em} +@media only screen and (min-width:768px){#toctitle{font-size:1.375em} +body.toc2{padding-left:15em;padding-right:0} +#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} +#toc.toc2 #toctitle{margin-top:0;font-size:1.2em} +#toc.toc2>ul{font-size:.9em;margin-bottom:0} +#toc.toc2 ul ul{margin-left:0;padding-left:1em} +#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} +body.toc2.toc-right{padding-left:0;padding-right:15em} +body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} +#toc.toc2{width:20em} +#toc.toc2 #toctitle{font-size:1.375em} +#toc.toc2>ul{font-size:.95em} +#toc.toc2 ul ul{padding-left:1.25em} +body.toc2.toc-right{padding-left:0;padding-right:20em}}#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +#content #toc>:first-child{margin-top:0} +#content #toc>:last-child{margin-bottom:0} +#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} +#footer-text{color:rgba(255,255,255,.8);line-height:1.44} +.sect1{padding-bottom:.625em} +@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}.sect1+.sect1{border-top:1px solid #efefed} +#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} +#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} +#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} +#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} +#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} +.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} +.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} +table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0} +.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)} +table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit} +.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} +.admonitionblock>table td.icon{text-align:center;width:80px} +.admonitionblock>table td.icon img{max-width:none} +.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} +.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} +.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} +.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} +.exampleblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child{margin-bottom:0} +.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +.sidebarblock>:first-child{margin-top:0} +.sidebarblock>:last-child{margin-bottom:0} +.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} +.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} +.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} +.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em} +.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal} +@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} +.listingblock pre.highlightjs{padding:0} +.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} +.listingblock pre.prettyprint{border-width:0} +.listingblock>.content{position:relative} +.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} +.listingblock:hover code[data-lang]:before{display:block} +.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} +.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} +table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} +table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0} +table.pyhltable td.code{padding-left:.75em;padding-right:0} +pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} +pre.pygments .lineno{display:inline-block;margin-right:.25em} +table.pyhltable .linenodiv{background:none!important;padding-right:0!important} +.quoteblock{margin:0 1em 1.25em 1.5em;display:table} +.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} +.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} +.quoteblock blockquote{margin:0;padding:0;border:0} +.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} +.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} +.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right} +.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} +.quoteblock .quoteblock blockquote{padding:0 0 0 .75em} +.quoteblock .quoteblock blockquote:before{display:none} +.verseblock{margin:0 1em 1.25em 1em} +.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} +.verseblock pre strong{font-weight:400} +.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} +.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} +.quoteblock .attribution br,.verseblock .attribution br{display:none} +.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.05em;color:rgba(0,0,0,.6)} +.quoteblock.abstract{margin:0 0 1.25em 0;display:block} +.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} +.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} +table.tableblock{max-width:100%;border-collapse:separate} +table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0} +table.spread{width:100%} +table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} +table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} +table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0} +table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} +table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0} +table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0} +table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0} +table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0} +table.frame-all{border-width:1px} +table.frame-sides{border-width:0 1px} +table.frame-topbot{border-width:1px 0} +th.halign-left,td.halign-left{text-align:left} +th.halign-right,td.halign-right{text-align:right} +th.halign-center,td.halign-center{text-align:center} +th.valign-top,td.valign-top{vertical-align:top} +th.valign-bottom,td.valign-bottom{vertical-align:bottom} +th.valign-middle,td.valign-middle{vertical-align:middle} +table thead th,table tfoot th{font-weight:bold} +tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} +tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} +p.tableblock>code:only-child{background:none;padding:0} +p.tableblock{font-size:1em} +td>div.verse{white-space:pre} +ol{margin-left:1.75em} +ul li ol{margin-left:1.5em} +dl dd{margin-left:1.125em} +dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} +ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} +ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} +ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} +ul.checklist li>p:first-child>.fa-check-square-o:first-child,ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em} +ul.checklist li>p:first-child>input[type="checkbox"]:first-child{position:relative;top:1px} +ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} +ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block} +ul.inline>li>*{display:block} +.unstyled dl dt{font-weight:400;font-style:normal} +ol.arabic{list-style-type:decimal} +ol.decimal{list-style-type:decimal-leading-zero} +ol.loweralpha{list-style-type:lower-alpha} +ol.upperalpha{list-style-type:upper-alpha} +ol.lowerroman{list-style-type:lower-roman} +ol.upperroman{list-style-type:upper-roman} +ol.lowergreek{list-style-type:lower-greek} +.hdlist>table,.colist>table{border:0;background:none} +.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} +td.hdlist1{padding-right:.75em;font-weight:bold} +td.hdlist1,td.hdlist2{vertical-align:top} +.literalblock+.colist,.listingblock+.colist{margin-top:-.5em} +.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} +.colist>table tr>td:last-of-type{padding:.25em 0} +.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} +.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} +.imageblock>.title{margin-bottom:0} +.imageblock.thumb,.imageblock.th{border-width:6px} +.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} +.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} +.image.left{margin-right:.625em} +.image.right{margin-left:.625em} +a.image{text-decoration:none} +span.footnote,span.footnoteref{vertical-align:super;font-size:.875em} +span.footnote a,span.footnoteref a{text-decoration:none} +span.footnote a:active,span.footnoteref a:active{text-decoration:underline} +#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} +#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0} +#footnotes .footnote{padding:0 .375em;line-height:1.3;font-size:.875em;margin-left:1.2em;text-indent:-1.2em;margin-bottom:.2em} +#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none} +#footnotes .footnote:last-of-type{margin-bottom:0} +#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} +.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} +.gist .file-data>table td.line-data{width:99%} +div.unbreakable{page-break-inside:avoid} +.big{font-size:larger} +.small{font-size:smaller} +.underline{text-decoration:underline} +.overline{text-decoration:overline} +.line-through{text-decoration:line-through} +.aqua{color:#00bfbf} +.aqua-background{background-color:#00fafa} +.black{color:#000} +.black-background{background-color:#000} +.blue{color:#0000bf} +.blue-background{background-color:#0000fa} +.fuchsia{color:#bf00bf} +.fuchsia-background{background-color:#fa00fa} +.gray{color:#606060} +.gray-background{background-color:#7d7d7d} +.green{color:#006000} +.green-background{background-color:#007d00} +.lime{color:#00bf00} +.lime-background{background-color:#00fa00} +.maroon{color:#600000} +.maroon-background{background-color:#7d0000} +.navy{color:#000060} +.navy-background{background-color:#00007d} +.olive{color:#606000} +.olive-background{background-color:#7d7d00} +.purple{color:#600060} +.purple-background{background-color:#7d007d} +.red{color:#bf0000} +.red-background{background-color:#fa0000} +.silver{color:#909090} +.silver-background{background-color:#bcbcbc} +.teal{color:#006060} +.teal-background{background-color:#007d7d} +.white{color:#bfbfbf} +.white-background{background-color:#fafafa} +.yellow{color:#bfbf00} +.yellow-background{background-color:#fafa00} +span.icon>.fa{cursor:default} +.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} +.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c} +.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} +.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900} +.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} +.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} +.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} +.conum[data-value] *{color:#fff!important} +.conum[data-value]+b{display:none} +.conum[data-value]:after{content:attr(data-value)} +pre .conum[data-value]{position:relative;top:-.125em} +b.conum *{color:inherit!important} +.conum:not([data-value]):empty{display:none} +h1,h2{letter-spacing:-.01em} +dt,th.tableblock,td.content{text-rendering:optimizeLegibility} +p,td.content{letter-spacing:-.01em} +p strong,td.content strong{letter-spacing:-.005em} +p,blockquote,dt,td.content{font-size:1.0625rem} +p{margin-bottom:1.25rem} +.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} +.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} +.print-only{display:none!important} +@media print{@page{margin:1.25cm .75cm} +*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} +a{color:inherit!important;text-decoration:underline!important} +a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} +a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} +abbr[title]:after{content:" (" attr(title) ")"} +pre,blockquote,tr,img{page-break-inside:avoid} +thead{display:table-header-group} +img{max-width:100%!important} +p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} +h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} +#toc,.sidebarblock,.exampleblock>.content{background:none!important} +#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important} +.sect1{padding-bottom:0!important} +.sect1+.sect1{border:0!important} +#header>h1:first-child{margin-top:1.25rem} +body.book #header{text-align:center} +body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0} +body.book #header .details{border:0!important;display:block;padding:0!important} +body.book #header .details span:first-child{margin-left:0!important} +body.book #header .details br{display:block} +body.book #header .details br+span:before{content:none!important} +body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} +body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} +.listingblock code[data-lang]:before{display:block} +#footer{background:none!important;padding:0 .9375em} +#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em} +.hide-on-print{display:none!important} +.print-only{display:block!important} +.hide-for-print{display:none!important} +.show-for-print{display:inherit!important}} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle new file mode 100644 index 0000000000..82570396cb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle @@ -0,0 +1,88 @@ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url 'https://repo.spring.io/plugins-release' } + if (project.hasProperty('fatJar')) jcenter() + } + dependencies { + classpath "com.bmuschko:gradle-nexus-plugin:2.3" + classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" + if (project.hasProperty('fatJar')) classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.3' + } +} + +ext { + repoUser = System.getenv('REPO_USERNAME') ?: project.findProperty('REPO_USERNAME') ?: '' + repoPass = System.getenv('REPO_PASSWORD') ?: project.findProperty('REPO_PASSWORD') ?: '' + projectsToSkipPublication = ['parent', 'samples', 'docs', 'root'] +} + +allprojects { + project.version = findProperty('contractVerifierVersion') ?: '1.0.0.BUILD-SNAPSHOT' + apply from: "$rootDir/gradle/release.gradle" +} + +subprojects { + apply plugin: 'groovy' + + group = 'org.springframework.cloud.contract' + + sourceCompatibility = 1.7 + targetCompatibility = 1.7 + + repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/libs-release-local" } + maven { url "http://repo.spring.io/libs-staging-local/" } + } + + //Dependencies in all subprojects - http://solidsoft.wordpress.com/2014/11/13/gradle-tricks-display-dependencies-for-all-subprojects-in-multi-project-build/ + task allDeps(type: DependencyReportTask) {} + task allInsight(type: DependencyInsightReportTask) {} + + dependencies { + compile localGroovy() + testCompile('org.spockframework:spock-core:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'info.solidsoft.spock:spock-global-unroll:0.5.0' + } + + test { + testLogging { + exceptionFormat = 'full' + } + } + + // fixing the groovydoc issue http://stackoverflow.com/questions/20618857/gradle-task-groovydoc-failing-with-noclassdeffounderror + configurations { + jansi.extendsFrom(runtime) + } + groovydoc { + def title = "IPDS ${version}" + groovyClasspath = project.configurations.jansi + } + dependencies { + jansi 'org.fusesource.jansi:jansi:1.11' + } +} + +configurations { + all { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + // To prevent an accidental usage of groovy-all.jar and groovy.jar in different versions + // all modularized Groovy jars are replaced with groovy-all.jar by default. + if (details.requested.group == 'org.codehaus.groovy' && details.requested.name != "groovy-all") { + details.useTarget("org.codehaus.groovy:groovy-all:${details.requested.version}") + } + } + } + } +} + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.sh new file mode 100755 index 0000000000..0a16b0b0e9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew clean build --parallel \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/deploy.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/deploy.sh new file mode 100755 index 0000000000..8c061c8b65 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/deploy.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew clean build install && ./scripts/runTests.sh && ./gradlew uploadArchives -x test \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/build.gradle new file mode 100644 index 0000000000..2a79e0ef8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/build.gradle @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'org.ajoberstar.github-pages' version '1.1.0' + id 'org.asciidoctor.gradle.asciidoctor' version '1.5.1' + id 'org.asciidoctor.convert' version '1.5.3' + id 'com.github.jruby-gradle.base' version '1.2.1' +} + + +apply plugin: 'org.asciidoctor.gradle.asciidoctor' +apply plugin: 'com.github.jruby-gradle.base' + +dependencies { + gems group: 'rubygems', name: 'asciidoctor', version: '1.5.3' +} + +asciidoctorj { + version = '1.5.4' +} + +task generateReadme(type: com.github.jrubygradle.JRubyExec) { + dependsOn jrubyPrepare + + description "Generates an output README.adoc" + script "${projectDir}/src/main/ruby/generate_readme.sh" + scriptArgs "-o${new File(project.rootDir, 'README.adoc')}" + gemWorkDir jrubyPrepare.outputDir +} + +asciidoctor { + String backend = 'html' + dependsOn jrubyPrepare + + resources { + from("src/main/asciidoc/") { + include '**/*.png' + include '**/*.jpg' + include '**/*.html' + } + into './' + } + + sourceDir 'src/main/asciidoc' + backends backend + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://Codearte.github.io/accurest', + 'source-highlighter': 'coderay', + 'imagesdir': 'images', + 'toc': 'left', + 'icons': 'font', + 'setanchors': 'true', + 'idprefix': '', + 'idseparator': '-', + 'docinfo1': 'true', + 'doctype' : 'article' + + gemPath = jrubyPrepare.outputDir +} + +generateReadme.dependsOn asciidoctor +publishGhPages.dependsOn asciidoctor + +githubPages { + repoUri = 'https://github.com/Codearte/accurest' + credentials { + username = project.hasProperty('githubToken') ? project.githubToken : '' + password = project.hasProperty('githubPass') ? project.githubPass : '' + } + + pages { + from('build/asciidoc/html') { + include '**/' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/README.adoc new file mode 100644 index 0000000000..cc85698563 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/README.adoc @@ -0,0 +1,19 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core +:stubrunner_core_path: {core_path}/spring-cloud-contract-stub-runner +:documentation_url: http://codearte.github.io/accurest + += Spring Cloud Contract Verifier + +include::introduction.adoc[] + +include::links.adoc[] + +== Documentation + +You can read more about Spring Cloud Contract Verifier by reading the {documentation_url}[docs] + +== Contributing + +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/contract.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/contract.adoc new file mode 100644 index 0000000000..a846910959 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/contract.adoc @@ -0,0 +1,220 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core + +== Contract DSL + +IMPORTANT: Remember that inside the contract file you have to provide the fully qualified name to +the `Contract` class and the `make` static import i.e. `org.springframework.cloud.contract.spec.Contract.make { ... }`. +You can also provide an import to the `Contract` class `import org.springframework.cloud.contract.spec.Contract` and then call + `Contract.make { ... }` + +Contract DSL is written in Groovy, but don't be alarmed if you didn't use Groovy before. Knowledge of the language is not really needed as our DSL uses only +a tiny subset of it (namely literals, method calls and closures). What's more the DSL is designed to be programmer-readable without any knowledge of the DSL itself - + it's statically typed. + +The Contract is present in the `spring-cloud-contract-spec` module of the Spring Cloud Contract Verifier repository. + +Let's look at full example of a contract definition. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=dsl_example,indent=0] +---- + +Not all features of the DSL are used in example above. If you didn't find what you are looking for, please check next paragraphs on this page. + +> You can easily compile Contracts to WireMock stubs mapping using standalone maven command: `mvn org.springframework.cloud.contract:spring-cloud-contract-verifier-maven-plugin:convert`. + +=== Limitations + +WARNING: Spring Cloud Contract Verifier doesn't support XML properly. Please use JSON or help us implement this feature. + +WARNING: Spring Cloud Contract Verifier supports equality check on text response. Regular expressions are not yet available. + +WARNING: The support for the verification of size of JSON arrays is experimental. If you want to turn it on please provide +the value of a system property `spring.cloud.contract.verifier.assert.size` equal to `true`. By default this feature is set to +`false`. You can also provide the `assertJsonSize` property in the plugin configuration. + +=== HTTP Top-Level Elements + +Following methods can be called in the top-level closure of a contract definition. Request and response are mandatory, priority is optional. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=http_dsl,indent=0] +---- + +=== Request + +HTTP protocol requires only **method and address** to be specified in a request. The same information is mandatory in request definition of the Contract. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=request,indent=0] +---- + +It is possible to specify whole `url` instead of just path, but `urlPath` is the recommended way as it makes the tests **host-independent**. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=url,indent=0] + +---- + +Request may contain **query parameters**, which are specified in a closure nested in a call to `urlPath` or `url`. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=urlpath,indent=0] +---- + +It may contain additional **request headers**... + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=headers,indent=0] +---- + +...and a **request body**. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=body,indent=0] +---- + +**Body's format** can also be specified explicitly by invoking one of format functions. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=bodyAsXml,indent=0] +---- + +=== Response + +Minimal response must contain **HTTP status code**. + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=response,indent=0] +---- + +Besides status response may contain **headers** and **body**, which are specified the same way as in the request (see previous paragraph). + +=== Regular expressions +You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response +should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both +for your test and your server side tests. + +Please see the example below: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=regex,indent=0] +---- + +=== Passing optional parameters + +It is possible to provide optional parameters in your contract. It's only possible to have optional parameter for the: + +- __STUB__ side of the Request +- __TEST__ side of the Response + +Example: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals,indent=0] +---- + +By wrapping a part of the body with the `optional()` method you are in fact creating a regular expression that should be present 0 or more times. + +That way for the example above the following test would be generated if you pick Spock: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals_test,indent=0] +---- + +and the following stub: + +[source,javascript,indent=0] +---- +include::{verifier_root_path}/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[tags=wiremock,indent=0] +---- + +=== Executing custom methods on server side +It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" +in the configuration. Please see the examples below: + +==== Contract DSL + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=method,indent=0] +---- + +==== Base Mock Spec + +[source,groovy,indent=0] +---- +include::{verifier_root_path}/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy[tags=base_class,indent=0] +---- + +=== JAX-RS support +Starting with release 0.8.0 we support JAX-RS 2 Client API. Base class needs to define `protected WebTarget webTarget` and server initialization, right now the only option how to test JAX-RS API is to start a web server. + +Request with a body needs to have a content type set otherwise `application/octet-stream` is going to be used. + +In order to use JAX-RS mode, use the following settings: + +[source,groovy,indent=0] +---- +testMode == 'JAXRSCLIENT' +---- + +Example of a test API generated: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy[tags=jaxrs,indent=0] +---- + +=== Messaging Top-Level Elements + +The DSL for messaging looks a little bit different than the one that focuses on HTTP. + +==== Output triggered by a method + +The output message can be triggered by calling a method (e.g. a Scheduler was started and a message was sent) + +[source,groovy] +---- +include::../../../../samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy[tags=method_trigger,indent=0] +---- + +In this case the output message will be sent to `output` if a method called `bookReturnedTriggered` will be executed. In the message *publisher's* side +we will generate a test that will call that method to trigger the message. On the *consumer* side you can use the `some_label` to trigger the message. + +==== Output triggered by a message + +The output message can be triggered by receiving a message. + +[source,groovy] +---- +include::../../../../samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy[tags=message_trigger,indent=0] +---- + +In this case the output message will be sent to `output` if a proper message will be received on the `input` destination. In the message *publisher's* side +we will generate a test that will send the input message to the defined destination. On the *consumer* side you can either send a message to the input +destination or use the `some_label` to trigger the message. + +==== Consumer / Producer + +In HTTP you have a notion of `client`/`stub and `server`/`test` notation. You can use them also in messaging but we're providing also the `consumer` and `produer` methods +as presented below (note you can use either `$` or `value` methods to provide `consumer` and `producer` parts) + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=consumer_producer] +---- \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/contract.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/contract.html new file mode 100644 index 0000000000..a210b8ade0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/contract.html @@ -0,0 +1,1232 @@ + + + + + + + +Contract DSL + + + + + + + +
+
+

Contract DSL

+
+
+

Contract DSL in Accurest is written in Groovy, but don’t be alarmed if you didn’t use Groovy before. Knowledge of the language is not really needed as our DSL uses only +a tiny subset of it (namely literals, method calls and closures). What’s more, Accurest’s DSL is designed to be programmer-readable without any knowledge of the DSL itself - + it’s statically typed.

+
+
+ + + + + +
+ + +Since {messaging_version} you can use the io.codearte.accurest.dsl.Accurest class in your DSL files. +
+
+
+

Let’s look at full example of a contract definition.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'PUT'
+                url '/api/12'
+                headers {
+                        header 'Content-Type': 'application/vnd.com.ofg.twitter-places-analyzer.v1+json'
+                }
+                body '''\
+                [{
+                        "created_at": "Sat Jul 26 09:38:57 +0000 2014",
+                        "id": 492967299297845248,
+                        "id_str": "492967299297845248",
+                        "text": "Gonna see you at Warsaw",
+                        "place":
+                        {
+                                "attributes":{},
+                                "bounding_box":
+                                {
+                                        "coordinates":
+                                                [[
+                                                        [-77.119759,38.791645],
+                                                        [-76.909393,38.791645],
+                                                        [-76.909393,38.995548],
+                                                        [-77.119759,38.995548]
+                                                ]],
+                                        "type":"Polygon"
+                                },
+                                "country":"United States",
+                                "country_code":"US",
+                                "full_name":"Washington, DC",
+                                "id":"01fbe706f872cb32",
+                                "name":"Washington",
+                                "place_type":"city",
+                                "url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
+                        }
+                }]
+        '''
+        }
+        response {
+                status 200
+        }
+}
+
+
+
+

Not all features of the DSL are used in example above. If you didn’t find what you are looking for, please check next paragraphs on this page.

+
+
+
+
+

You can easily compile Accurest Contracts to WireMock stubs mapping using standalone maven command: mvn io.codearte.accurest:accurest-maven-plugin:convert.

+
+
+
+
+

Limitations

+
+ + + + + +
+ + +Accurest doesn’t support XML properly. Please use JSON or help us implement this feature. +
+
+
+ + + + + +
+ + +Accurest supports equality check on text response. Regular expressions are not yet available. +
+
+
+
+

HTTP Top-Level Elements

+
+

Following methods can be called in the top-level closure of a contract definition. Request and response are mandatory, priority is optional.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        // Definition of HTTP request part of the contract
+        // (this can be a valid request or invalid depending
+        // on type of contract being specified).
+        request {
+                //...
+        }
+
+        // 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).
+        response {
+                //...
+        }
+
+        // Contract priority, which can be used for overriding
+        // contracts (1 is highest). Priority is optional.
+        priority 1
+}
+
+
+
+
+

Request

+
+

HTTP protocol requires only method and address to be specified in a request. The same information is mandatory in request definition of Accurest contract.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                // HTTP request method (GET/POST/PUT/DELETE).
+                method 'GET'
+
+                // Path component of request URL is specified as follows.
+                urlPath('/users')
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

It is possible to specify whole url instead of just path, but urlPath is the recommended way as it makes the tests host-independent.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'GET'
+
+                // Specifying `url` and `urlPath` in one contract is illegal.
+                url('http://localhost:8888/users')
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

Request may contain query parameters, which are specified in a closure nested in a call to urlPath or url.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                urlPath('/users') {
+
+                        // 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.
+                        queryParameters {
+
+                                // If a simple literal is used as value
+                                // default matcher function is used (equalTo)
+                                parameter 'limit': 100
+
+                                // `equalTo` function simply compares passed value
+                                // using identity operator (==).
+                                parameter 'filter': equalTo("email")
+
+                                // `containing` function matches strings
+                                // that contains passed substring.
+                                parameter 'gender': value(stub(containing("[mf]")), server('mf'))
+
+                                // `matching` function tests parameter
+                                // against passed regular expression.
+                                parameter 'offset': value(stub(matching("[0-9]+")), server(123))
+
+                                // `notMatching` functions tests if parameter
+                                // does not match passed regular expression.
+                                parameter 'loginStartsWith': value(stub(notMatching(".{0,2}")), server(3))
+                        }
+                }
+
+                //...
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

It may contain additional request headers…​

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // Each header is added in form `'Header-Name' : 'Header-Value'`.
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+
+                //...
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

…​and a request body.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // JSON and XML formats of request body are supported.
+                // Format will be determined from a header or body's content.
+                body '''{ "login" : "john", "name": "John The Contract" }'''
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

Body’s format can also be specified explicitly by invoking one of format functions.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // In this case body will be formatted as XML.
+                body equalToXml(
+                                '''<user><login>john</login><name>John The Contract</name></user>'''
+                )
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+
+

Response

+
+

Minimal response must contain HTTP status code.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+        }
+        response {
+                // Status code sent by the server
+                // in response to request specified above.
+                status 200
+        }
+}
+
+
+
+

Besides status response may contain headers and body, which are specified the same way as in the request (see previous paragraph).

+
+
+
+

Regular expressions

+
+

You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response +should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both +for your test and your server side tests.

+
+
+

Please see the example below:

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method('GET')
+                url $(client(~/\/[0-9]{2}/), server('/12'))
+        }
+        response {
+                status 200
+                body(
+                                id: value(
+                                                client('123'),
+                                                server(regex('[0-9]+'))
+                                ),
+                                surname: $(
+                                                client('Kowalsky'),
+                                                server('Lewandowski')
+                                ),
+                                name: 'Jan',
+                                created: $(client('2014-02-02 12:23:43'), server(execute('currentDate(it)'))),
+                                correlationId: value(client('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
+                                                server(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
+                                )
+                )
+                headers {
+                        header 'Content-Type': 'text/plain'
+                }
+        }
+}
+
+
+
+
+

Passing optional parameters

+
+

It is possible to provide optional parameters in your contract. It’s only possible to have optional parameter for the:

+
+
+
    +
  • +

    STUB side of the Request

    +
  • +
  • +

    TEST side of the Response

    +
  • +
+
+
+

Example:

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        priority 1
+        request {
+                method 'POST'
+                url '/users/password'
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body(
+                                email: $(stub(optional(regex(email()))), test('abc@abc.com')),
+                                callback_url: $(stub(regex(hostname())), test('http://partners.com'))
+                )
+        }
+        response {
+                status 404
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body(
+                                code: value(stub("123123"), test(optional("123123")))
+                )
+        }
+}
+
+
+
+

By wrapping a part of the body with the optional() method you are in fact creating a regular expression that should be present 0 or more times.

+
+
+

That way for the example above the following test would be generated if you pick Spock:

+
+
+
+
"""
+ given:
+  def request = given()
+    .header('Content-Type', 'application/json')
+    .body('''{"email":"abc@abc.com","callback_url":"http://partners.com"}''')
+
+ when:
+  def response = given().spec(request)
+    .post("/users/password")
+
+ then:
+  response.statusCode == 404
+  response.header('Content-Type')  == 'application/json'
+ and:
+  DocumentContext parsedJson = JsonPath.parse(response.body.asString())
+  assertThatJson(parsedJson).field("code").matches("(123123)?")
+"""
+
+
+
+

and the following stub:

+
+
+
+
'''
+{
+  "request" : {
+    "url" : "/users/password",
+    "method" : "POST",
+    "bodyPatterns" : [ {
+      "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]"
+    }, {
+      "matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
+    } ],
+    "headers" : {
+      "Content-Type" : {
+        "equalTo" : "application/json"
+      }
+    }
+  },
+  "response" : {
+    "status" : 404,
+    "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}",
+    "headers" : {
+      "Content-Type" : "application/json"
+    }
+  },
+  "priority" : 1
+}
+'''
+
+
+
+
+

Executing custom methods on server side

+
+

It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" +in the configuration. Please see the examples below:

+
+
+

Groovy DSL

+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'PUT'
+                url $(client(regex('^/api/[0-9]{2}$')), server('/api/12'))
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body '''\
+                                [{
+                                        "text": "Gonna see you at Warsaw"
+                                }]
+                        '''
+        }
+        response {
+                body (
+                                path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))),
+                                correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)')))
+                )
+                status 200
+        }
+}
+
+
+
+
+

Base Mock Spec

+
+
+
abstract class BaseMockMvcSpec extends Specification {
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new PairIdController())
+        }
+
+        void isProperCorrelationId(Integer correlationId) {
+                assert correlationId == 123456
+        }
+
+        void isEmpty(String value) {
+                assert value == null
+        }
+
+}
+
+
+
+
+
+

JAX-RS support

+
+

Starting with release 0.8.0 we support JAX-RS 2 Client API. Base class needs to define protected WebTarget webTarget and server initialization, right now the only option how to test JAX-RS API is to start a web server.

+
+
+

Request with a body needs to have a content type set otherwise application/octet-stream is going to be used.

+
+
+

In order to use JAX-RS mode, use the following settings:

+
+
+
+
testMode == 'JAXRSCLIENT'
+
+
+
+

Example of a test API generated:

+
+
+
+
'''
+ // when:
+  Response response = webTarget
+    .path("/users")
+    .queryParam("limit", "10")
+    .queryParam("offset", "20")
+    .queryParam("filter", "email")
+    .queryParam("sort", "name")
+    .queryParam("search", "55")
+    .queryParam("age", "99")
+    .queryParam("name", "Denis.Stepanov")
+    .queryParam("email", "bob@email.com")
+    .request()
+    .method("GET");
+
+  String responseAsString = response.readEntity(String.class);
+
+ // then:
+  assertThat(response.getStatus()).isEqualTo(200);
+ // and:
+  DocumentContext parsedJson = JsonPath.parse(responseAsString);
+  assertThatJson(parsedJson).field("property1").isEqualTo("a");
+'''
+
+
+
+
+

Messaging Top-Level Elements

+
+ + + + + +
+ + +Feature available since {messaging_version} +
+
+
+

The DSL for messaging looks a little bit different than the one that focuses on HTTP.

+
+
+

Output triggered by a method

+
+

The output message can be triggered by calling a method (e.g. a Scheduler was started and a message was sent)

+
+
+
+
def dsl = GroovyDsl.make {
+        // Human readable description
+        description 'Some description'
+        // Label by means of which the output message can be triggered
+        label 'some_label'
+        // input to the contract
+        input {
+                // the contract will be triggered by a method
+                triggeredBy('bookReturnedTriggered()')
+        }
+        // output message of the contract
+        outputMessage {
+                // destination to which the output message will be sent
+                sentTo('output')
+                // the body of the output message
+                body('''{ "bookName" : "foo" }''')
+                // the headers of the output message
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

In this case the output message will be sent to output if a method called bookReturnedTriggered will be executed. In the message publisher’s side +we will generate a test that will call that method to trigger the message. On the consumer side you can use the some_label to trigger the message.

+
+
+
+

Output triggered by a message

+
+

The output message can be triggered by receiving a message.

+
+
+
+
def dsl = GroovyDsl.make {
+        description 'Some Description'
+        label 'some_label'
+        // input is a message
+        input {
+                // the message was received from this destination
+                messageFrom('input')
+                // has the following body
+                messageBody([
+                        bookName: 'foo'
+                ])
+                // and the following headers
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('output')
+                body([
+                        bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

In this case the output message will be sent to output if a proper message will be received on the input destination. In the message publisher’s side +we will generate a test that will send the input message to the defined destination. On the consumer side you can either send a message to the input +destination or use the some_label to trigger the message.

+
+
+
+

Consumer / Producer

+
+

In HTTP you have a notion of client/stub and `server/test notation. You can use them also in messaging but we’re providing also the consumer and produer methods +as presented below (note you can use either $ or value methods to provide consumer and producer parts)

+
+
+
+
Accurest.make {
+        label 'some_label'
+        input {
+                messageFrom value(consumer('jms:output'), producer('jms:input'))
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo $(consumer('jms:input'), producer('jms:output'))
+                body([
+                                bookName: 'foo'
+                ])
+        }
+}
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Deps.png b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Deps.png new file mode 100644 index 0000000000000000000000000000000000000000..1426814308101b8ccf6ef95fd195e37c0a180392 GIT binary patch literal 37192 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 A(f|Me literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Stubs1.png b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/images/Stubs1.png new file mode 100644 index 0000000000000000000000000000000000000000..ebadfdb91014284b917367e3d9c9c8a137da3993 GIT binary patch literal 35170 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 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/index.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/index.html new file mode 100644 index 0000000000..a0ed7a1410 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/index.html @@ -0,0 +1,3914 @@ + + + + + + + +Accurest + + + + + + + +
+
+
+
+

Adam Dudczak, Marcin Grzejszczak, Jakub Kubryński, Karol Lassak, Olga Maciaszek-Sharma, Mariusz Smykuła

+
+
+
+
+

Introduction

+
+
+

Just to make long story short - Accurest is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped +with Contract Definition Language (DSL). Contract definitions are used by Accurest to produce following resources:

+
+
+
    +
  • +

    JSON stub definitions to be used by Wiremock when doing integration testing on the client code (client tests). +Test code must still be written by hand, test data is produced by Accurest.

    +
  • +
  • +

    Messaging routes if you’re using one. We’re integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to

    +
  • +
  • +

    Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (server tests). Full test is generated by Accurest.

    +
  • +
+
+
+

Accurest moves TDD to the level of software architecture.

+
+
+

Why?

+
+

Let us assume that we have a system comprising of multiple microservices:

+
+
+
+Microservices Architecture +
+
+
+

Testing issues

+
+

If we wanted to test the application in top left corner if it can communicate with other services then we could do one of two things:

+
+
+
    +
  • +

    deploy all microservices and perform end to end tests

    +
  • +
  • +

    mock other microservices in unit / integration tests

    +
  • +
+
+
+

Both have their advantages but also a lot of disadvantages. Let’s focus on the latter.

+
+
+

Deploy all microservices and perform end to end tests

+
+
+

Advantages:

+
+
+
    +
  • +

    simulates production

    +
  • +
  • +

    tests real communication between services

    +
  • +
+
+
+

Disadvantages:

+
+
+
    +
  • +

    to test one microservice we would have to deploy 6 microservices, a couple of databases etc.

    +
  • +
  • +

    the environment where the tests would be conducted would be locked for a single suite of tests (i.e. nobody else would be able to run the tests in the meantime).

    +
  • +
  • +

    long to run

    +
  • +
  • +

    very late feedback

    +
  • +
  • +

    extremely hard to debug

    +
  • +
+
+
+

Mock other microservices in unit / integration tests

+
+
+

Advantages:

+
+
+
    +
  • +

    very fast feedback

    +
  • +
  • +

    no infrastructure requirements

    +
  • +
+
+
+

Disadvantages:

+
+
+
    +
  • +

    the implementor of the service creates stubs thus they might have nothing to do with the reality

    +
  • +
  • +

    you can go to production with passing tests and failing production

    +
  • +
+
+
+

To solve the aforementioned issues Accurest with Stub Runner were created. Their main idea is to give you very fast feedback, without the need +to set up the whole world of microservices.

+
+
+
+Stubbed Services +
+
+
+

If you work on stubs then the only applications you need are those that your application is using directly.

+
+
+
+Stubbed Services +
+
+
+

Accurest gives you the certainty that the stubs that you’re using were created by the service that you’re calling. Also if you can use them it means that they were +tested against the producer’s side. In other words - you can trust those stubs.

+
+
+
+
+

Purposes

+
+

The main purposes of Accurest with Stub Runner are:

+
+
+
    +
  • +

    to ensure that WireMock / Messaging stubs (used when developing the client) are doing exactly what actual server-side implementation will do,

    +
  • +
  • +

    to promote ATDD method and Microservices architectural style,

    +
  • +
  • +

    to provide a way to publish changes in contracts that are immediately visible on both sides,

    +
  • +
  • +

    to generate boilerplate test code used on the server side.

    +
  • +
+
+
+
+

Client Side

+
+

During the tests you want to have a Wiremock instance / Messaging route up and running that simulates the service Y. +You would like to feed that instance with a proper stub definition. That stub definition would need +to be valid and should also be reusable on the server side.

+
+
+

Summing it up: On this side, in the stub definition, you can use patterns for request stubbing and you need exact +values for responses.

+
+
+
+

Server Side

+
+

Being a service Y since you are developing your stub, you need to be sure that it’s actually resembling your +concrete implementation. You can’t have a situation where your stub acts in one way and your application on +production behaves in a different way.

+
+
+

That’s why from the provided stub acceptance tests will be generated that will ensure +that your application behaves in the same way as you define in your stub.

+
+
+

Summing it up: On this side, in the stub definition, you need exact values as request and can use patterns/methods +for response verification.

+
+
+
+

Dependencies

+
+

Accurest and Stub Runner are using the following libraries

+
+ +
+
+ +
+

Below you can find some resources related to Accurest and Stub Runner. Note that some can be outdated since the Accurest project +is under constant development.

+
+
+

Videos

+
+

Olga Maciaszek-Sharma talking about Accurest

+
+
+
+ +
+
+
+

Marcin Grzejszczak and Jakub Kubryński talking about Accurest

+
+
+
+ +
+
+
+ +
+
+

Samples

+
+

Here you can find some working samples. Check the readme of each project for more information.

+
+
+
+
+
+

Contract DSL

+
+
+

Contract DSL in Accurest is written in Groovy, but don’t be alarmed if you didn’t use Groovy before. Knowledge of the language is not really needed as our DSL uses only +a tiny subset of it (namely literals, method calls and closures). What’s more, Accurest’s DSL is designed to be programmer-readable without any knowledge of the DSL itself - + it’s statically typed.

+
+
+ + + + + +
+ + +Since 1.1.0 you can use the io.codearte.accurest.dsl.Accurest class in your DSL files. +
+
+
+

Let’s look at full example of a contract definition.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'PUT'
+                url '/api/12'
+                headers {
+                        header 'Content-Type': 'application/vnd.com.ofg.twitter-places-analyzer.v1+json'
+                }
+                body '''\
+                [{
+                        "created_at": "Sat Jul 26 09:38:57 +0000 2014",
+                        "id": 492967299297845248,
+                        "id_str": "492967299297845248",
+                        "text": "Gonna see you at Warsaw",
+                        "place":
+                        {
+                                "attributes":{},
+                                "bounding_box":
+                                {
+                                        "coordinates":
+                                                [[
+                                                        [-77.119759,38.791645],
+                                                        [-76.909393,38.791645],
+                                                        [-76.909393,38.995548],
+                                                        [-77.119759,38.995548]
+                                                ]],
+                                        "type":"Polygon"
+                                },
+                                "country":"United States",
+                                "country_code":"US",
+                                "full_name":"Washington, DC",
+                                "id":"01fbe706f872cb32",
+                                "name":"Washington",
+                                "place_type":"city",
+                                "url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
+                        }
+                }]
+        '''
+        }
+        response {
+                status 200
+        }
+}
+
+
+
+

Not all features of the DSL are used in example above. If you didn’t find what you are looking for, please check next paragraphs on this page.

+
+
+
+
+

You can easily compile Accurest Contracts to WireMock stubs mapping using standalone maven command: mvn io.codearte.accurest:accurest-maven-plugin:convert.

+
+
+
+
+

Limitations

+
+ + + + + +
+ + +Accurest doesn’t support XML properly. Please use JSON or help us implement this feature. +
+
+
+ + + + + +
+ + +Accurest supports equality check on text response. Regular expressions are not yet available. +
+
+
+
+

HTTP Top-Level Elements

+
+

Following methods can be called in the top-level closure of a contract definition. Request and response are mandatory, priority is optional.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        // Definition of HTTP request part of the contract
+        // (this can be a valid request or invalid depending
+        // on type of contract being specified).
+        request {
+                //...
+        }
+
+        // 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).
+        response {
+                //...
+        }
+
+        // Contract priority, which can be used for overriding
+        // contracts (1 is highest). Priority is optional.
+        priority 1
+}
+
+
+
+
+

Request

+
+

HTTP protocol requires only method and address to be specified in a request. The same information is mandatory in request definition of Accurest contract.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                // HTTP request method (GET/POST/PUT/DELETE).
+                method 'GET'
+
+                // Path component of request URL is specified as follows.
+                urlPath('/users')
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

It is possible to specify whole url instead of just path, but urlPath is the recommended way as it makes the tests host-independent.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'GET'
+
+                // Specifying `url` and `urlPath` in one contract is illegal.
+                url('http://localhost:8888/users')
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

Request may contain query parameters, which are specified in a closure nested in a call to urlPath or url.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                urlPath('/users') {
+
+                        // 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.
+                        queryParameters {
+
+                                // If a simple literal is used as value
+                                // default matcher function is used (equalTo)
+                                parameter 'limit': 100
+
+                                // `equalTo` function simply compares passed value
+                                // using identity operator (==).
+                                parameter 'filter': equalTo("email")
+
+                                // `containing` function matches strings
+                                // that contains passed substring.
+                                parameter 'gender': value(stub(containing("[mf]")), server('mf'))
+
+                                // `matching` function tests parameter
+                                // against passed regular expression.
+                                parameter 'offset': value(stub(matching("[0-9]+")), server(123))
+
+                                // `notMatching` functions tests if parameter
+                                // does not match passed regular expression.
+                                parameter 'loginStartsWith': value(stub(notMatching(".{0,2}")), server(3))
+                        }
+                }
+
+                //...
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

It may contain additional request headers…​

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // Each header is added in form `'Header-Name' : 'Header-Value'`.
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+
+                //...
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

…​and a request body.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // JSON and XML formats of request body are supported.
+                // Format will be determined from a header or body's content.
+                body '''{ "login" : "john", "name": "John The Contract" }'''
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+

Body’s format can also be specified explicitly by invoking one of format functions.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+
+                // In this case body will be formatted as XML.
+                body equalToXml(
+                                '''<user><login>john</login><name>John The Contract</name></user>'''
+                )
+        }
+
+        response {
+                //...
+        }
+}
+
+
+
+
+

Response

+
+

Minimal response must contain HTTP status code.

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                //...
+        }
+        response {
+                // Status code sent by the server
+                // in response to request specified above.
+                status 200
+        }
+}
+
+
+
+

Besides status response may contain headers and body, which are specified the same way as in the request (see previous paragraph).

+
+
+
+

Regular expressions

+
+

You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response +should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both +for your test and your server side tests.

+
+
+

Please see the example below:

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method('GET')
+                url $(client(~/\/[0-9]{2}/), server('/12'))
+        }
+        response {
+                status 200
+                body(
+                                id: value(
+                                                client('123'),
+                                                server(regex('[0-9]+'))
+                                ),
+                                surname: $(
+                                                client('Kowalsky'),
+                                                server('Lewandowski')
+                                ),
+                                name: 'Jan',
+                                created: $(client('2014-02-02 12:23:43'), server(execute('currentDate(it)'))),
+                                correlationId: value(client('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
+                                                server(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
+                                )
+                )
+                headers {
+                        header 'Content-Type': 'text/plain'
+                }
+        }
+}
+
+
+
+
+

Passing optional parameters

+
+

It is possible to provide optional parameters in your contract. It’s only possible to have optional parameter for the:

+
+
+
    +
  • +

    STUB side of the Request

    +
  • +
  • +

    TEST side of the Response

    +
  • +
+
+
+

Example:

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        priority 1
+        request {
+                method 'POST'
+                url '/users/password'
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body(
+                                email: $(stub(optional(regex(email()))), test('abc@abc.com')),
+                                callback_url: $(stub(regex(hostname())), test('http://partners.com'))
+                )
+        }
+        response {
+                status 404
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body(
+                                code: value(stub("123123"), test(optional("123123")))
+                )
+        }
+}
+
+
+
+

By wrapping a part of the body with the optional() method you are in fact creating a regular expression that should be present 0 or more times.

+
+
+

That way for the example above the following test would be generated if you pick Spock:

+
+
+
+
"""
+ given:
+  def request = given()
+    .header('Content-Type', 'application/json')
+    .body('''{"email":"abc@abc.com","callback_url":"http://partners.com"}''')
+
+ when:
+  def response = given().spec(request)
+    .post("/users/password")
+
+ then:
+  response.statusCode == 404
+  response.header('Content-Type')  == 'application/json'
+ and:
+  DocumentContext parsedJson = JsonPath.parse(response.body.asString())
+  assertThatJson(parsedJson).field("code").matches("(123123)?")
+"""
+
+
+
+

and the following stub:

+
+
+
+
'''
+{
+  "request" : {
+    "url" : "/users/password",
+    "method" : "POST",
+    "bodyPatterns" : [ {
+      "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]"
+    }, {
+      "matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
+    } ],
+    "headers" : {
+      "Content-Type" : {
+        "equalTo" : "application/json"
+      }
+    }
+  },
+  "response" : {
+    "status" : 404,
+    "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}",
+    "headers" : {
+      "Content-Type" : "application/json"
+    }
+  },
+  "priority" : 1
+}
+'''
+
+
+
+
+

Executing custom methods on server side

+
+

It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" +in the configuration. Please see the examples below:

+
+
+

Groovy DSL

+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        request {
+                method 'PUT'
+                url $(client(regex('^/api/[0-9]{2}$')), server('/api/12'))
+                headers {
+                        header 'Content-Type': 'application/json'
+                }
+                body '''\
+                                [{
+                                        "text": "Gonna see you at Warsaw"
+                                }]
+                        '''
+        }
+        response {
+                body (
+                                path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))),
+                                correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)')))
+                )
+                status 200
+        }
+}
+
+
+
+
+

Base Mock Spec

+
+
+
abstract class BaseMockMvcSpec extends Specification {
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new PairIdController())
+        }
+
+        void isProperCorrelationId(Integer correlationId) {
+                assert correlationId == 123456
+        }
+
+        void isEmpty(String value) {
+                assert value == null
+        }
+
+}
+
+
+
+
+
+

JAX-RS support

+
+

Starting with release 0.8.0 we support JAX-RS 2 Client API. Base class needs to define protected WebTarget webTarget and server initialization, right now the only option how to test JAX-RS API is to start a web server.

+
+
+

Request with a body needs to have a content type set otherwise application/octet-stream is going to be used.

+
+
+

In order to use JAX-RS mode, use the following settings:

+
+
+
+
testMode == 'JAXRSCLIENT'
+
+
+
+

Example of a test API generated:

+
+
+
+
'''
+ // when:
+  Response response = webTarget
+    .path("/users")
+    .queryParam("limit", "10")
+    .queryParam("offset", "20")
+    .queryParam("filter", "email")
+    .queryParam("sort", "name")
+    .queryParam("search", "55")
+    .queryParam("age", "99")
+    .queryParam("name", "Denis.Stepanov")
+    .queryParam("email", "bob@email.com")
+    .request()
+    .method("GET");
+
+  String responseAsString = response.readEntity(String.class);
+
+ // then:
+  assertThat(response.getStatus()).isEqualTo(200);
+ // and:
+  DocumentContext parsedJson = JsonPath.parse(responseAsString);
+  assertThatJson(parsedJson).field("property1").isEqualTo("a");
+'''
+
+
+
+
+

Messaging Top-Level Elements

+
+ + + + + +
+ + +Feature available since 1.1.0 +
+
+
+

The DSL for messaging looks a little bit different than the one that focuses on HTTP.

+
+
+

Output triggered by a method

+
+

The output message can be triggered by calling a method (e.g. a Scheduler was started and a message was sent)

+
+
+
+
def dsl = GroovyDsl.make {
+        // Human readable description
+        description 'Some description'
+        // Label by means of which the output message can be triggered
+        label 'some_label'
+        // input to the contract
+        input {
+                // the contract will be triggered by a method
+                triggeredBy('bookReturnedTriggered()')
+        }
+        // output message of the contract
+        outputMessage {
+                // destination to which the output message will be sent
+                sentTo('output')
+                // the body of the output message
+                body('''{ "bookName" : "foo" }''')
+                // the headers of the output message
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

In this case the output message will be sent to output if a method called bookReturnedTriggered will be executed. In the message publisher’s side +we will generate a test that will call that method to trigger the message. On the consumer side you can use the some_label to trigger the message.

+
+
+
+

Output triggered by a message

+
+

The output message can be triggered by receiving a message.

+
+
+
+
def dsl = GroovyDsl.make {
+        description 'Some Description'
+        label 'some_label'
+        // input is a message
+        input {
+                // the message was received from this destination
+                messageFrom('input')
+                // has the following body
+                messageBody([
+                        bookName: 'foo'
+                ])
+                // and the following headers
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('output')
+                body([
+                        bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

In this case the output message will be sent to output if a proper message will be received on the input destination. In the message publisher’s side +we will generate a test that will send the input message to the defined destination. On the consumer side you can either send a message to the input +destination or use the some_label to trigger the message.

+
+
+
+

Consumer / Producer

+
+

In HTTP you have a notion of client/stub and `server/test notation. You can use them also in messaging but we’re providing also the consumer and produer methods +as presented below (note you can use either $ or value methods to provide consumer and producer parts)

+
+
+
+
Accurest.make {
+        label 'some_label'
+        input {
+                messageFrom value(consumer('jms:output'), producer('jms:input'))
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo $(consumer('jms:input'), producer('jms:output'))
+                body([
+                                bookName: 'foo'
+                ])
+        }
+}
+
+
+
+
+
+
+
+

Accurest HTTP

+
+
+

Gradle Project

+
+

Prerequisites

+
+

In order to use Accurest with Wiremock you have to use gradle or maven plugin.

+
+
+
Add gradle plugin
+
+
+
buildscript {
+        repositories {
+                mavenCentral()
+        }
+        dependencies {
+                classpath 'io.codearte.accurest:accurest-gradle-plugin:${accurest_version}'
+        }
+}
+
+apply plugin: 'groovy'
+apply plugin: 'accurest'
+
+dependencies {
+        testCompile 'org.codehaus.groovy:groovy-all:2.4.6'
+        testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
+        testCompile 'com.jayway.restassured:spring-mock-mvc:2.9.0' // needed if you're going to use Spring MockMvc
+}
+
+
+
+
+
Add maven plugin
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

Read more: accurest-maven-plugin

+
+
+
+
Add stubs
+
+

By default Accurest is looking for stubs in src/test/resources/accurest directory.

+
+
+

Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure

+
+
+
+
src/test/resources/accurest/myservice/shouldCreateUser.groovy
+src/test/resources/accurest/myservice/shouldReturnUser.groovy
+
+
+
+

Accurest will create test class defaultBasePackage.MyService with two methods

+
+
+
    +
  • +

    shouldCreateUser()

    +
  • +
  • +

    shouldReturnUser()

    +
  • +
+
+
+
+
+

Run plugin

+
+

Plugin registers itself to be invoked before check task. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke generateAccurest task.

+
+
+
+

Configure plugin

+
+

To change default configuration just add accurest snippet to your Gradle config

+
+
+
+
accurest {
+        testMode = 'MockMvc'
+        baseClassForTests = 'org.mycompany.tests'
+        generatedTestSourcesDir = project.file('src/accurest')
+}
+
+
+
+
Configuration options
+
+
    +
  • +

    testMode - defines mode for acceptance tests. By default MockMvc which is based on Spring’s MockMvc. It can also be changed to JaxRsClient or to Explicit for real HTTP calls.

    +
  • +
  • +

    imports - array with imports that should be included in generated tests (for example ['org.myorg.Matchers']). By default empty array []

    +
  • +
  • +

    staticImports - array with static imports that should be included in generated tests(for example ['org.myorg.Matchers.*']). By default empty array []

    +
  • +
  • +

    basePackageForTests - specifies base package for all generated tests. By default set to io.codearte.accurest.tests

    +
  • +
  • +

    baseClassForTests - base class for generated tests. By default spock.lang.Specification if using Spock tests.

    +
  • +
  • +

    ruleClassForTests - specifies Rule which should be added to generated test classes.

    +
  • +
  • +

    ignoredFiles - Ant matcher allowing defining stub files for which processing should be skipped. By default empty array []

    +
  • +
  • +

    contractsDslDir - directory containing contracts written using the GroovyDSL. By default $rootDir/src/test/resources/accurest

    +
  • +
  • +

    generatedTestSourcesDir - test source directory where tests generated from Groovy DSL should be placed. By default $buildDir/generated-test-sources/accurest

    +
  • +
  • +

    stubsOutputDir - dir where the generated Wiremock stubs from Groovy DSL should be placed

    +
  • +
  • +

    targetFramework - the target test framework to be used; currently Spock and JUnit are supported with JUnit being the default framework

    +
  • +
+
+
+
+
Base class for tests
+
+

When using Accurest in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified.

+
+
+
+
abstract class BaseMockMvcSpec extends Specification {
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new PairIdController())
+        }
+
+        void isProperCorrelationId(Integer correlationId) {
+                assert correlationId == 123456
+        }
+
+        void isEmpty(String value) {
+                assert value == null
+        }
+
+}
+
+
+
+

In case of using Explicit mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of JAXRSCLIENT mode this base class +should also contain protected WebTarget webTarget field, right now the only option to test JAX-RS API is to start a web server.

+
+
+
+
+

Invoking generated tests

+
+

To ensure that provider side is complaint with defined contracts, you need to invoke:

+
+
+
+
./gradlew generateAccurest test
+
+
+
+
+

Accurest on consumer side

+
+

In consumer service you need to configure Accurest plugin in exactly the same way as in case of provider. If you don’t want to use Stub Runner then you need to copy contracts stored in +src/test/resources/accurest and generate WireMock json stubs using:

+
+
+
+
./gradlew generateWireMockClientStubs
+
+
+
+

Note that stubsOutputDir option has to be set for stub generation to work.

+
+
+

When present, json stubs can be used in consumer automated tests.

+
+
+
+
@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
+class LoanApplicationServiceSpec extends Specification {
+
+ @ClassRule
+ @Shared
+ WireMockClassRule wireMockRule == new WireMockClassRule()
+
+ @Autowired
+ LoanApplicationService sut
+
+ def 'should successfully apply for loan'() {
+   given:
+         LoanApplication application =
+                        new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123)
+   when:
+        LoanApplicationResult loanApplication == sut.loanApplication(application)
+   then:
+        loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED
+        loanApplication.rejectionReason === null
+ }
+}
+
+
+
+

Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Accurest.

+
+
+
+
+

Using in your Maven project

+
+

Add maven plugin

+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

Read more: accurest-maven-plugin

+
+
+
+

Add stubs

+
+

By default Accurest is looking for stubs in src/test/resources/accurest directory. +Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure

+
+
+
+
src/test/resources/accurest/myservice/shouldCreateUser.groovy
+src/test/resources/accurest/myservice/shouldReturnUser.groovy
+
+
+
+

Accurest will create test class defaultBasePackage.MyService with two methods + - shouldCreateUser() + - shouldReturnUser()

+
+
+
+

Run plugin

+
+

Plugin goal generateTests is assigned to be invoked in phase generate-test-sources. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke generateTests goal.

+
+
+
+

Configure plugin

+
+

To change default configuration just add configuration section to plugin definition or execution definition.

+
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <basePackageForTests>com.ofg.twitter.place</basePackageForTests>
+        <baseClassForTests>com.ofg.twitter.place.BaseMockMvcSpec</baseClassForTests>
+    </configuration>
+</plugin>
+
+
+
+
Important configuration options
+
+
    +
  • +

    testMode - defines mode for acceptance tests. By default MockMvc which is based on Spring’s MockMvc. It can also be changed to JaxRsClient or to Explicit for real HTTP calls.

    +
  • +
  • +

    basePackageForTests - specifies base package for all generated tests. By default set to io.codearte.accurest.tests.

    +
  • +
  • +

    ruleClassForTests - specifies Rule which should be added to generated test classes.

    +
  • +
  • +

    baseClassForTests - base class for generated tests. By default spock.lang.Specification if using Spock tests.

    +
  • +
  • +

    contractsDir - directory containing contracts written using the GroovyDSL. By default /src/test/resources/accurest.

    +
  • +
  • +

    testFramework - the target test framework to be used; currently Spock and JUnit are supported with Spock being the default framework

    +
  • +
+
+
+

For complete information take a look at Plugin Documentation

+
+
+
+
Base class for tests
+
+
+
When using Accurest in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified.
+
+
+
+
+
package org.mycompany.tests
+
+import org.mycompany.ExampleSpringController
+import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
+import spock.lang.Specification
+
+class  MvcSpec extends Specification {
+  def setup() {
+   RestAssuredMockMvc.standaloneSetup(new ExampleSpringController())
+  }
+}
+
+
+
+

In case of using Explicit mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of JAXRSCLIENT mode this base class should also contain protected WebTarget webTarget field, right now the only option to test JAX-RS API is to start a web server.

+
+
+
+
+

Invoking generated tests

+
+

Accurest Maven Plugins generates verification code into directory /generated-test-sources/accurest and attach this directory to testCompile goal.

+
+
+

For Groovy Spock code use:

+
+
+
+
<plugin>
+        <groupId>org.codehaus.gmavenplus</groupId>
+        <artifactId>gmavenplus-plugin</artifactId>
+        <version>1.5</version>
+        <executions>
+                <execution>
+                        <goals>
+                                <goal>testCompile</goal>
+                        </goals>
+                </execution>
+        </executions>
+        <configuration>
+                <testSources>
+                        <testSource>
+                                <directory>${project.basedir}/src/test/groovy</directory>
+                                <includes>
+                                        <include>**/*.groovy</include>
+                                </includes>
+                        </testSource>
+                        <testSource>
+                                <directory>${project.build.directory}/generated-test-sources/accurest</directory>
+                                <includes>
+                                        <include>**/*.groovy</include>
+                                </includes>
+                        </testSource>
+                </testSources>
+        </configuration>
+</plugin>
+
+
+
+

To ensure that provider side is complaint with defined contracts, you need to invoke mvn generateTest test

+
+
+
+

Accurest on consumer side

+
+

In consumer service you need to configure Accurest plugin in exactly the same way as in case of provider. You need to copy contracts stored in src/test/resources/accurest and generate Wiremock json stubs using: mvn generateStubs command. By default generated WireMock mapping is stored in directory target/mappings. Your project should create from this generated mappings additional artifact with classifier stubs for easy deploy to maven repository.

+
+
+

Sample configuration:

+
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <version>${accurest-plugin.version}</version>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

When present, json stubs can be used in consumer automated tests.

+
+
+
+
@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
+class LoanApplicationServiceSpec extends Specification {
+
+ @ClassRule
+ @Shared
+ WireMockClassRule wireMockRule == new WireMockClassRule()
+
+ @Autowired
+ LoanApplicationService sut
+
+ def 'should successfully apply for loan'() {
+   given:
+         LoanApplication application =
+                        new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123)
+   when:
+        LoanApplicationResult loanApplication == sut.loanApplication(application)
+   then:
+        loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED
+        loanApplication.rejectionReason === null
+ }
+}
+
+
+
+

Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Accurest.

+
+
+
+
+

Scenarios

+
+

It’s possible to handle scenarios with Accurest. All you need to do is to stick to proper naming convention while creating your contracts. The convention requires to include order number followed by the underscore.

+
+
+
+
my_contracts_dir\
+  scenario1\
+    1_login.groovy
+    2_showCart.groovy
+    3_logout.groovy
+
+
+
+

Such tree will cause Accurest generating Wiremock’s scenario with name scenario1 and three steps: + - login marked as Started pointing to: + - showCart marked as Step1 pointing to: + - logout marked as Step2 which will close the scenario. +More details about Wiremock scenarios can be found under [http://wiremock.org/stateful-behaviour.html](http://wiremock.org/stateful-behaviour.html)

+
+
+

Accurest will also generate tests with guaranteed order of execution.

+
+
+
+
+
+

Accurest Messaging

+
+
+ + + + + +
+ + +Feature available since 1.1.0 +
+
+
+

Accurest allows you to verify your application that uses messaging as means of communication. +All of our integrations are working with Spring but you can also set one yourself.

+
+
+

Integrations

+
+

You can use one of the three integration configurations:

+
+
+
    +
  • +

    Apache Camel

    +
  • +
  • +

    Spring Integration

    +
  • +
  • +

    Spring Cloud Stream

    +
  • +
+
+
+

If you’re using Spring Boot, the aforementioned test configurations will be appended automatically.

+
+
+

You have to provide as a dependency one of the Accurest Messaging modules. Example for Gradle:

+
+
+
+
// for Apache Camel
+testCompile "io.codearte.accurest:accurest-messaging-camel:${accurestVersion}"
+// for Spring Integration
+testCompile "io.codearte.accurest:accurest-messaging-integration:${accurestVersion}"
+// for Spring Cloud Stream
+testCompile "io.codearte.accurest:accurest-messaging-stream:${accurestVersion}"
+
+
+
+
+

Manual Integration

+
+

The accurest-messaging-core module contains 3 main interfaces:

+
+
+
    +
  • +

    AccurestMessage - describes a message received / sent to a channel / queue / topic etc.

    +
  • +
  • +

    AccurestMessageBuilder - describes how to build a message

    +
  • +
  • +

    AccurestMessaging - class that allows you to build, send and receive messages

    +
  • +
  • +

    AccurestFilter - interface to filter out the messages that do not follow the pattern from the DSL

    +
  • +
+
+
+

In the generated test the AccurestMessaging is injected via @Inject annotation thus you can use other injection +frameworks than Spring.

+
+
+

You have to provide as a dependency the accurest-messaging-core module. Example for Gradle:

+
+
+
+
testCompile "io.codearte.accurest:accurest-messaging-core:${accurestVersion}"
+
+
+
+
+

Publisher side test generation

+
+

Having the input or outputMessage sections in your DSL will result in creation of tests on the publisher’s side. By default +JUnit tests will be created, however there is also a possibility to create Spock tests.

+
+
+

There are 3 main scenarios that we should take into consideration:

+
+
+
    +
  • +

    Scenario 1: there is no input message that produces an output one. The output message is triggered by a component +inside the application (e.g. scheduler)

    +
  • +
  • +

    Scenario 2: the input message triggers an output message

    +
  • +
  • +

    Scenario 3: the input message is consumed and there is no output message

    +
  • +
+
+
+

Scenario 1 (no input message)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('activemq:output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+ // when:
+  bookReturnedTriggered();
+
+ // then:
+  AccurestMessage response = accurestMessaging.receiveMessage("activemq:output");
+  assertThat(response).isNotNull();
+  assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo");
+ // and:
+  DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.getPayload()));
+  assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
'''
+ when:
+  bookReturnedTriggered()
+
+ then:
+  def response = accurestMessaging.receiveMessage('activemq:output')
+  assert response != null
+  response.getHeader('BOOK-NAME')  == 'foo'
+ and:
+  DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.payload))
+  assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
+
+'''
+
+
+
+
+

Scenario 2 (output triggered by input)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                messageFrom('jms:input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+// given:
+ AccurestMessage inputMessage = accurestMessaging.create(
+  "{\\"bookName\\":\\"foo\\"}"
+, headers()
+  .header("sample", "header"));
+
+// when:
+ accurestMessaging.send(inputMessage, "jms:input");
+
+// then:
+ AccurestMessage response = accurestMessaging.receiveMessage("jms:output");
+ assertThat(response).isNotNull();
+ assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo");
+// and:
+ DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.getPayload()));
+ assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
"""\
+given:
+   def inputMessage = accurestMessaging.create(
+    '''{"bookName":"foo"}''',
+    ['sample': 'header']
+  )
+
+when:
+   accurestMessaging.send(inputMessage, 'jms:input')
+
+then:
+   def response = accurestMessaging.receiveMessage('jms:output')
+   assert response !- null
+   response.getHeader('BOOK-NAME')  == 'foo'
+and:
+   DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.payload))
+   assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
+"""
+
+
+
+
+

Scenario 3 (no output message)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                messageFrom('jms:delete')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+                assertThat('bookWasDeleted()')
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+// given:
+ AccurestMessage inputMessage = accurestMessaging.create(
+        "{\\"bookName\\":\\"foo\\"}"
+, headers()
+        .header("sample", "header"));
+
+// when:
+ accurestMessaging.send(inputMessage, "jms:delete");
+
+// then:
+ bookWasDeleted();
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
'''
+given:
+         def inputMessage = accurestMessaging.create(
+                \'\'\'{"bookName":"foo"}\'\'\',
+                ['sample': 'header']
+        )
+
+when:
+         accurestMessaging.send(inputMessage, 'jms:delete')
+
+then:
+         noExceptionThrown()
+         bookWasDeleted()
+'''
+
+
+
+
+
+

Consumer Stub Side generation

+
+

Unlike the HTTP part - in Messaging we need to publish the Groovy DSL inside the JAR with a stub. Then it’s parsed on the consumer side +and proper stubbed routes are created.

+
+
+

For more infromation please consult the Stub Runner Messaging sections.

+
+
+

Gradle Setup

+
+

Example of Accurest Gradle setup:

+
+
+
+
ext {
+        contractsDir = file("mappings")
+        stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
+        wireMockStubsOutputDir = file(new File(stubsOutputDirRoot, 'repository/mappings/'))
+        contractsOutputDir = file(new File(stubsOutputDirRoot, 'repository/accurest/'))
+}
+
+task copyContracts(type: Copy) {
+        from contractsDir
+        include '**/*.groovy'
+        into contractsOutputDir
+}
+
+task stubsJar(type: Jar, dependsOn: ["generateWireMockClientStubs", copyContracts]) {
+        baseName = "${project.name}"
+        classifier = "stubs"
+        from stubsOutputDirRoot
+}
+
+artifacts {
+        archives stubsJar
+}
+
+publishing {
+        publications {
+                stubs(MavenPublication) {
+                        artifactId "${project.name}-stubs"
+                        artifact stubsJar
+                }
+        }
+}
+
+
+
+
+

Maven Setup

+
+

Example of Maven can be found in the Accurest Maven Plugin README

+
+
+
+
+
+
+

Stub Runner

+
+
+

One of the issues that you could have encountered while using Accurest was to pass the generated WireMock JSON stubs from the server side to the client side (or various clients). + The same takes place in terms of client side generation for messaging.

+
+
+

Copying the JSON files / setting the client side for messaging manually is out of the question.

+
+
+

Publishing stubs as JARs

+
+

The easiest approach would be to centralize the way stubs are kept. For example you can keep them as JARs in a Maven repository.

+
+
+

Gradle

+
+

Example of Accurest Gradle setup:

+
+
+
+
ext {
+        contractsDir = file("mappings")
+        stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
+        wireMockStubsOutputDir = file(new File(stubsOutputDirRoot, 'repository/mappings/'))
+        contractsOutputDir = file(new File(stubsOutputDirRoot, 'repository/accurest/'))
+}
+
+task copyContracts(type: Copy) {
+        from contractsDir
+        include '**/*.groovy'
+        into contractsOutputDir
+}
+
+task stubsJar(type: Jar, dependsOn: ["generateWireMockClientStubs", copyContracts]) {
+        baseName = "${project.name}"
+        classifier = "stubs"
+        from stubsOutputDirRoot
+}
+
+artifacts {
+        archives stubsJar
+}
+
+publishing {
+        publications {
+                stubs(MavenPublication) {
+                        artifactId "${project.name}-stubs"
+                        artifact stubsJar
+                }
+        }
+}
+
+
+
+
+

Maven

+
+

Example of Maven can be found in the Accurest Maven Plugin README

+
+
+
+
+

Modules

+
+

Accurest comes with a new structure of modules

+
+
+
+
└── stub-runner
+    ├── stub-runner
+    ├── stub-runner-boot
+    ├── stub-runner-junit
+    ├── stub-runner-spring
+    └── stub-runner-spring-cloud
+
+
+
+
+

Stub Runner Core

+
+

Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +Consumer Driven Contracts.

+
+
+

Stub Runner allows you to automatically download the stubs of the provided dependencies, start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined.

+
+
+

Running stubs

+
+
Running using main app
+
+

You can set the following options to the main class:

+
+
+
+
-maxp (--maxPort) N            : Maximum port value to be assigned to the
+                                 Wiremock instance. Defaults to 15000
+                                 (default: 15000)
+-minp (--minPort) N            : Minimal port value to be assigned to the
+                                 Wiremock instance. Defaults to 10000
+                                 (default: 10000)
+-s (--stubs) VAL               : Comma separated list of Ivy representation of
+                                 jars with stubs. Eg. groupid:artifactid1,group
+                                 id2:artifactid2:version:classifier
+-sr (--stubRepositoryRoot) VAL : Location of a Jar containing server where you
+                                 keep your stubs (e.g. http://nexus.net/content
+                                 /repositories/repository)
+-ss (--stubsSuffix) VAL        : Suffix for the jar containing stubs (e.g.
+                                 'stubs' if the stub jar would have a 'stubs'
+                                 classifier for stubs: foobar-stubs ).
+                                 Defaults to 'stubs' (default: stubs)
+-wo (--workOffline)            : Switch to work offline. Defaults to 'false'
+                                 (default: false)
+
+
+
+
+
Building a Fat Jar
+
+

Just call the following command:

+
+
+
+
./gradlew stub-runner-root:stub-runner:shadowJar -PfatJar
+
+
+
+

and inside the build/lib there will be a Fat Jar with classifier fatJar waiting for you to execute. E.g.

+
+
+
+
java -jar stub-runner/stub-runner/build/libs/stub-runner-1.0.1-SNAPSHOT-fatJar.jar -sr http://a.b.com -s a:b:c,d:e,f:g:h:i
+
+
+
+
+
+

Stub runner configuration

+
+

You can configure the stub runner by either passing the full arguments list with the -Pargs like this:

+
+
+
+
./gradlew stub-runner-root:stub-runner:run -Pargs="-c pl -minp 10000 -maxp 10005 -s a:b:c,d:e,f:g:h"
+
+
+
+

or each parameter separately with a -P prefix and without the hyphen - in the name of the param

+
+
+
+
./gradlew stub-runner-root:stub-runner:run -Pc=pl -Pminp=10000 -Pmaxp=10005 -Ps=a:b:c,d:e,f:g:h
+
+
+
+
HTTP Stubs
+
+

Stubs are defined in JSON documents, whose syntax is defined in WireMock documentation

+
+
+

Example:

+
+
+
+
{
+    "request": {
+        "method": "GET",
+        "url": "/ping"
+    },
+    "response": {
+        "status": 200,
+        "body": "pong",
+        "headers": {
+            "Content-Type": "text/plain"
+        }
+    }
+}
+
+
+
+
+
Viewing registered mappings
+
+

Every stubbed collaborator exposes list of defined mappings under __/admin/ endpoint.

+
+
+
+
Messaging Stubs
+
+

Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up.

+
+
+
+
+
+

Stub Runner Boot

+
+ + + + + +
+ + +Feature available since 1.1.0 +
+
+
+

Accurest Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers.

+
+
+

One of the usecases is to run some smoke (end to end) tests on a deployed application. You can read + more about this in the "Microservice Deployment" article at Too Much Coding blog.

+
+
+

How to use it?

+
+

Just add the

+
+
+
+
compile "io.codearte.accurest:stub-runner-boot:${accurestVersion}"
+
+
+
+

and a messaging implementation:

+
+
+
+
// for Apache Camel
+compile "io.codearte.accurest:stub-runner-messaging-camel:${accurestVersion}"
+// for Spring Integration
+compile "io.codearte.accurest:stub-runner-messaging-integration:${accurestVersion}"
+// for Spring Cloud Stream
+compile "io.codearte.accurest:stub-runner-messaging-stream:${accurestVersion}"
+
+
+
+

Build a fat-jar and you’re ready to go!

+
+
+

For the properties check the Stub Runner Spring section.

+
+
+
+

Endpoints

+
+
HTTP
+
+
    +
  • +

    GET /stubs - returns a list of all running stubs in ivy:integer notation

    +
  • +
  • +

    GET /stubs/{ivy} - returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    +
  • +
+
+
+
+
Messaging
+
+

For Messaging

+
+
+
    +
  • +

    GET /triggers - returns a list of all running labels in ivy : [ label1, label2 …​] notation

    +
  • +
  • +

    POST /triggers/{label} - executes a trigger with label

    +
  • +
  • +

    POST /triggers/{ivy}/{label} - executes a trigger with label for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    +
  • +
+
+
+
+
+

Example

+
+
+
@ContextConfiguration(classes = [StubRunnerBootSpec, StubRunnerBoot], loader = SpringApplicationContextLoader)
+@EnableBinding
+@Configuration
+class StubRunnerBootSpec extends Specification {
+
+        @Autowired StubRunning stubRunning
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
+                                new TriggerController(stubRunning))
+        }
+
+        def 'should return a list of running stub servers in "full ivy:port" notation'() {
+                when:
+                        String response = RestAssuredMockMvc.get('/stubs').body.asString()
+                then:
+                        def root = new JsonSlurper().parseText(response)
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs' instanceof Integer
+        }
+
+        def 'should return a port on which a [#stubId] stub is running'() {
+                when:
+                        def response = RestAssuredMockMvc.get("/stubs/${stubId}")
+                then:
+                        response.statusCode == 200
+                        response.body.as(Integer) > 0
+                where:
+                        stubId << ['io.codearte.accurest.stubs:streamService:+:stubs',
+                                           'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs',
+                                           'io.codearte.accurest.stubs:streamService:+',
+                                           'io.codearte.accurest.stubs:streamService',
+                                           'streamService']
+        }
+
+        def 'should return 404 when missing stub was called'() {
+                when:
+                        def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
+                then:
+                        response.statusCode == 404
+        }
+
+        def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
+                when:
+                        String response = RestAssuredMockMvc.get('/triggers').body.asString()
+                then:
+                        def root = new JsonSlurper().parseText(response)
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"])
+        }
+
+        def 'should trigger a messaging label'() {
+                given:
+                        StubRunning stubRunning = Mock()
+                        RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/delete_book")
+                then:
+                        response.statusCode == 200
+                and:
+                        1 * stubRunning.trigger('delete_book')
+        }
+
+        def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
+                given:
+                        StubRunning stubRunning = Mock()
+                        RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
+                then:
+                        response.statusCode == 200
+                and:
+                        1 * stubRunning.trigger(stubId, 'delete_book')
+                where:
+                        stubId << ['io.codearte.accurest.stubs:streamService:stubs', 'io.codearte.accurest.stubs:streamService', 'streamService']
+        }
+
+        def 'should return when trigger is missing'() {
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/missing_label")
+                then:
+                        response.statusCode == 404
+                        def root = new JsonSlurper().parseText(response.body.asString())
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"])
+        }
+
+}
+
+
+
+
+
+

Stub Runner JUnit Rule

+
+

Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id:

+
+
+
+
@ClassRule public static AccurestRule rule = new AccurestRule()
+                .repoRoot(repoRoot())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer");
+
+
+
+

After that rule gets executed Stub Runner connects to your Maven repository and for the given list of dependencies tries to:

+
+
+
    +
  • +

    download them

    +
  • +
  • +

    cache them locally

    +
  • +
  • +

    unzip them to a temporary folder

    +
  • +
  • +

    start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port

    +
  • +
  • +

    feed the WireMock server with all JSON files that are valid WireMock definitions

    +
  • +
+
+
+

Stub Runner uses Eclipse Aether mechanism to download the Maven dependencies. +Check their docs for more information.

+
+
+

Since the AccurestRule implements the StubFinder it allows you to find the started stubs:

+
+
+
+
package io.codearte.accurest.stubrunner
+
+import io.codearte.accurest.dsl.GroovyDsl
+
+interface StubFinder extends StubTrigger {
+        /**
+         * For the given groupId and artifactId tries to find the matching
+         * URL of the running stub.
+         *
+         * @param groupId - might be null. In that case a search only via artifactId takes place
+         * @return URL of a running stub or null if not found
+         */
+        URL findStubUrl(String groupId, String artifactId)
+
+        /**
+         * For the given Ivy notation {@code groupId:artifactId} tries to find the matching
+         * URL of the running stub. You can also pass only {@code artifactId}.
+         *
+         * @param ivyNotation - Ivy representation of the Maven artifact
+         * @return URL of a running stub or null if not found
+         */
+        URL findStubUrl(String ivyNotation)
+
+        /**
+         * Returns all running stubs
+         */
+        RunningStubs findAllRunningStubs()
+
+        /**
+         * Returns the list of Accurest contracts
+         */
+        Map<StubConfiguration, Collection<GroovyDsl>> getAccurestContracts()
+}
+
+
+
+

Example of usage in Spock tests:

+
+
+
+
@ClassRule @Shared AccurestRule rule = new AccurestRule()
+                .repoRoot(AccurestRuleSpec.getResource("/m2repo").toURI().toString())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer")
+
+def 'should start WireMock servers'() {
+        expect: 'WireMocks are running'
+                rule.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance') != null
+                rule.findStubUrl('loanIssuance') != null
+                rule.findStubUrl('loanIssuance') == rule.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance')
+                rule.findStubUrl('io.codearte.accurest.stubs:fraudDetectionServer') != null
+        and:
+                rule.findAllRunningStubs().isPresent('loanIssuance')
+                rule.findAllRunningStubs().isPresent('io.codearte.accurest.stubs', 'fraudDetectionServer')
+                rule.findAllRunningStubs().isPresent('io.codearte.accurest.stubs:fraudDetectionServer')
+        and: 'Stubs were registered'
+                "${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
+                "${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
+}
+
+
+
+

Example of usage in JUnit tests:

+
+
+
+
@Test
+public void should_start_wiremock_servers() throws Exception {
+        // expect: 'WireMocks are running'
+                then(rule.findStubUrl("io.codearte.accurest.stubs", "loanIssuance")).isNotNull();
+                then(rule.findStubUrl("loanIssuance")).isNotNull();
+                then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl("io.codearte.accurest.stubs", "loanIssuance"));
+                then(rule.findStubUrl("io.codearte.accurest.stubs:fraudDetectionServer")).isNotNull();
+        // and:
+                then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
+                then(rule.findAllRunningStubs().isPresent("io.codearte.accurest.stubs", "fraudDetectionServer")).isTrue();
+                then(rule.findAllRunningStubs().isPresent("io.codearte.accurest.stubs:fraudDetectionServer")).isTrue();
+        // and: 'Stubs were registered'
+                then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance");
+                then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer");
+}
+
+
+
+

Check the Common properties for JUnit and Spring for more information on how to apply global configuration of Stub Runner.

+
+
+

Providing fixed ports

+
+

You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule.

+
+
+
+

Fluent API

+
+

When using the AccurestRule you can add a stub to download and then pass the port for the last downloaded stub.

+
+
+
+
@ClassRule public static AccurestRule rule = new AccurestRule()
+                .repoRoot(repoRoot())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .withPort(12345)
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer:12346");
+
+
+
+

You can see that for this example the following test is valid:

+
+
+
+
then(rule.findStubUrl("loanIssuance")).isEqualTo(URI.create("http://localhost:12345").toURL());
+then(rule.findStubUrl("fraudDetectionServer")).isEqualTo(URI.create("http://localhost:12346").toURL());
+
+
+
+
+
+

Stub Runner Spring

+
+

Sets up Spring configuration of the Stub Runner project.

+
+
+

By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs.

+
+
+

If you want to find the URL of your stubbed dependency you can autowire the StubFinder interface and use +its methods as presented below:

+
+
+
+
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
+class StubRunnerConfigurationSpec extends Specification {
+
+        @Autowired StubFinder stubFinder
+
+        def 'should start WireMock servers'() {
+                expect: 'WireMocks are running'
+                        stubFinder.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance') != null
+                        stubFinder.findStubUrl('loanIssuance') != null
+                        stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance')
+                        stubFinder.findStubUrl('io.codearte.accurest.stubs:fraudDetectionServer') != null
+                and:
+                        stubFinder.findAllRunningStubs().isPresent('loanIssuance')
+                        stubFinder.findAllRunningStubs().isPresent('io.codearte.accurest.stubs', 'fraudDetectionServer')
+                        stubFinder.findAllRunningStubs().isPresent('io.codearte.accurest.stubs:fraudDetectionServer')
+                and: 'Stubs were registered'
+                        "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
+                        "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
+        }
+
+        @Configuration
+        @Import(StubRunnerConfiguration)
+        @EnableAutoConfiguration
+        static class Config {}
+}
+
+
+
+

for the following configuration file:

+
+
+
+
stubrunner.stubs.repository.root: classpath:m2repo/repository/
+stubrunner.stubs.ids: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
+
+
+
+
+

Stub Runner Spring Cloud

+
+

Registers the stubs in the provided Service Discovery. It’s enough to add the jar

+
+
+
+
io.codearte.accurest:stub-runner-spring-cloud
+
+
+
+

and the Stub Runner autoconfiguration should be picked up.

+
+
+

Stubbing Service Discovery

+
+

The most important feature of Stub Runner Spring Cloud is the fact that it’s stubbing

+
+
+
    +
  • +

    DiscoveryClient

    +
  • +
  • +

    Ribbon ServerList

    +
  • +
+
+
+

that means that regardles of the fact whether you’re using Zookeeper, Consul, Eureka or anything else, you don’t need that in your tests. +We’re starting WireMock instances of your dependencies and we’re telling your application whenever you’re using Feign, load balanced RestTemplate +or DiscoveryClient directly, to call those stubbed servers instead of calling the real Service Discovery tool.

+
+
+
+

Additional Configuration

+
+

You can match the artifactId of the stub with the name of your app by using the stubrunner.stubs.idsToServiceIds: map. +You can disable Stub Runner Ribbon support by providing: stubrunner.cloud.ribbon.enabled equal to false +You can disable Stub Runner support by providing: stubrunner.cloud.enabled equal to false

+
+
+
+
+

Common properties for JUnit and Spring

+
+

Some of the properties that are repetitive can be set using system properties or property sources (for Spring). Here are their names with their default values:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property nameDefault valueDescription

stubrunner.port.range.min

10000

Minimal value of a port for a started WireMock with stubs

stubrunner.port.range.max

15000

Minimal value of a port for a started WireMock with stubs

stubrunner.stubs.repository.root

Comma separated list of Maven repo urls. If blank then will call the local maven repo

stubrunner.stubs.classifier

stubs

Default classifier for the stub artifacts

stubrunner.work-offline

false

If true then will not contact any remote repositories to download stubs

stubrunner.stubs.ids

Comma separated list of Ivy notation of stubs to download

+
+

Stub runner stubs ids

+
+

You can provide the stubs to download via the stubrunner.stubs.ids system property. They follow the following pattern:

+
+
+
+
groupId:artifactId:version:classifier:port
+
+
+
+

version, classifier and port are optional.

+
+
+
    +
  • +

    If you don’t provide the port then a random one will be picked

    +
  • +
  • +

    If you don’t provide the classifier then the default one will be taken.

    +
  • +
  • +

    If you don’t provide the version then the + will be passed and the latest one will be downloaded

    +
  • +
+
+
+

Where port means the port of the WireMock server.

+
+
+
+
+
+
+

Stub Runner for Messaging

+
+
+ + + + + +
+ + +Feature available since 1.1.0 +
+
+
+

Stub Runner has the functionality to run the published stubs in memory. It can integrate with the following frameworks out of the box

+
+
+
    +
  • +

    Spring Integration

    +
  • +
  • +

    Spring Cloud Stream

    +
  • +
  • +

    Apache Camel

    +
  • +
+
+
+

It also provides points of entry to integrate with any other solution on the market.

+
+
+

Stub triggering

+
+

To trigger a message it’s enough to use the StubTigger interface:

+
+
+
+
package io.codearte.accurest.stubrunner
+
+interface StubTrigger {
+
+        /**
+         * Triggers an event by a given label for a given {@code groupid:artifactid} notation. You can use only {@code artifactId} too.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger(String ivyNotation, String labelName)
+
+        /**
+         * Triggers an event by a given label.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger(String labelName)
+
+        /**
+         * Triggers all possible events.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger()
+
+        /**
+         * Returns a mapping of ivy notation of a dependency to all the labels it has.
+         *
+         * Feature related to messaging.
+         */
+        Map<String, Collection<String>> labels()
+}
+
+
+
+

For convenience the StubFinder interface extends StubTrigger so it’s enough to use only one in your tests.

+
+
+

StubTrigger gives you the following options to trigger a message:

+
+
+

Trigger by label

+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+
Trigger by group and artifact ids
+
+
+
stubFinder.trigger('io.codearte.accurest.stubs:camelService', 'return_book_1')
+
+
+
+
+
Trigger by artifact ids
+
+
+
stubFinder.trigger('camelService', 'return_book_1')
+
+
+
+
+
Trigger all messages
+
+
+
stubFinder.trigger()
+
+
+
+
+
+
+

Stub Runner Messaging Camel

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-camel:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +camelService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── camelService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── camelService-0.0.1-SNAPSHOT.pom
+                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('jms:input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to jms:output

+
+
+
+
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
+receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the jms:output destination.

+
+
+
+
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])
+
+
+
+

Next we’ll want to listen to the output of the message sent to jms:output

+
+
+
+
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
+receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the jms:output destination.

+
+
+
+
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])
+
+
+
+
+
+
+

Stub Runner Messaging Integration

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Spring Integration. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-integration:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +integrationService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── integrationService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
+                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and the following Spring Integration Route:

+
+
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<beans:beans xmlns="http://www.springframework.org/schema/integration"
+                         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                         xmlns:beans="http://www.springframework.org/schema/beans"
+                         xsi:schemaLocation="http://www.springframework.org/schema/beans
+                        http://www.springframework.org/schema/beans/spring-beans.xsd
+                        http://www.springframework.org/schema/integration
+                        http://www.springframework.org/schema/integration/spring-integration.xsd">
+
+
+        <!-- REQUIRED FOR TESTING -->
+        <bridge input-channel="output"
+                        output-channel="outputTest"/>
+
+        <channel id="outputTest">
+                <queue/>
+        </channel>
+
+</beans:beans>
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to output

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('outputTest')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the output destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')
+
+
+
+

Next we’ll want to listen to the output of the message sent to output

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('outputTest')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the input destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
+
+
+
+
+
+
+

Stub Runner Messaging Stream

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Spring Stream. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+ + + + + +
+ + +In Stub Runner’s integration with Stream the messageFrom or sentTo Strings are resolved +first as a destination of a channel, and then if there is no such destination it’s resolved as a +channel name. +
+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-stream:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +streamService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── streamService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── streamService-0.0.1-SNAPSHOT.pom
+                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('returnBook')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('bookStorage')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('returnBook')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and the following Spring configuration:

+
+
+
+
stubrunner.stubs.repository.root: classpath:m2repo/repository/
+stubrunner.stubs.ids: io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs
+
+spring:
+  cloud:
+    stream:
+      bindings:
+        output:
+          destination: returnBook
+        input:
+          destination: bookStorage
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTrigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to a channel whose destination is returnBook

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('returnBook')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the bookStorage destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')
+
+
+
+

Next we’ll want to listen to the output of the message sent to returnBook

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('returnBook')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the output destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
+
+
+
+
+
+
+
+
+

Migration Guide

+
+
+

Migration to 0.4.7

+
+
    +
  • +

    in 0.4.7 we’ve fixed package name (coderate to codearte) so you’ve to do the same in your projects. This means replacing io.coderate.accurest.dsl.GroovyDsl with io.codearte.accurest.dsl.GroovyDsl

    +
  • +
+
+
+
+

Migration to 1.0.0-RC1

+
+
    +
  • +

    from 1.0.0 we’re distinguish ignored contracts from excluded contracts:

    +
  • +
  • +

    excludedFiles pattern tells Accurest to skip processing those files at all

    +
  • +
  • +

    ignoredFiles pattern tells Accurest to generate contracts and tests, but tests will be marked as @Ignore

    +
  • +
  • +

    from 1.0.0 the basePackageForTests behaviour has changed

    +
  • +
  • +

    prior to the change all DSL files had to be under contractsDslDir/basePackageForTests/subpackage resulting in basePackageForTests.subpackage test package creation

    +
  • +
  • +

    now all DSL files have to be under contractsDslDir/subpackage resulting in basePackageForTests.subpackage test package creation

    +
  • +
  • +

    If you don’t migrate to the new approach you will have your tests under contractsDslDir.contractsDslDir.subpackage

    +
  • +
+
+
+
+

Migration to 1.1.0

+
+
    +
  • +

    from 1.1.0 we’re setting JUnit as a default testing utility. You have to pass the following option to keep Spock +as your first choice:

    +
  • +
+
+
+
+
targetFramework = 'Spock'
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/introduction.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/introduction.html new file mode 100644 index 0000000000..5f173f30de --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/introduction.html @@ -0,0 +1,797 @@ + + + + + + + +Introduction + + + + + + + +
+
+

Introduction

+
+
+

Just to make long story short - Accurest is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped +with Contract Definition Language (DSL). Contract definitions are used by Accurest to produce following resources:

+
+
+
    +
  • +

    JSON stub definitions to be used by Wiremock when doing integration testing on the client code (client tests). +Test code must still be written by hand, test data is produced by Accurest.

    +
  • +
  • +

    Messaging routes if you’re using one. We’re integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to

    +
  • +
  • +

    Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (server tests). Full test is generated by Accurest.

    +
  • +
+
+
+

Accurest moves TDD to the level of software architecture.

+
+
+

Why?

+
+

Let us assume that we have a system comprising of multiple microservices:

+
+
+
+Microservices Architecture +
+
+
+

Testing issues

+
+

If we wanted to test the application in top left corner if it can communicate with other services then we could do one of two things:

+
+
+
    +
  • +

    deploy all microservices and perform end to end tests

    +
  • +
  • +

    mock other microservices in unit / integration tests

    +
  • +
+
+
+

Both have their advantages but also a lot of disadvantages. Let’s focus on the latter.

+
+
+

Deploy all microservices and perform end to end tests

+
+
+

Advantages:

+
+
+
    +
  • +

    simulates production

    +
  • +
  • +

    tests real communication between services

    +
  • +
+
+
+

Disadvantages:

+
+
+
    +
  • +

    to test one microservice we would have to deploy 6 microservices, a couple of databases etc.

    +
  • +
  • +

    the environment where the tests would be conducted would be locked for a single suite of tests (i.e. nobody else would be able to run the tests in the meantime).

    +
  • +
  • +

    long to run

    +
  • +
  • +

    very late feedback

    +
  • +
  • +

    extremely hard to debug

    +
  • +
+
+
+

Mock other microservices in unit / integration tests

+
+
+

Advantages:

+
+
+
    +
  • +

    very fast feedback

    +
  • +
  • +

    no infrastructure requirements

    +
  • +
+
+
+

Disadvantages:

+
+
+
    +
  • +

    the implementor of the service creates stubs thus they might have nothing to do with the reality

    +
  • +
  • +

    you can go to production with passing tests and failing production

    +
  • +
+
+
+

To solve the aforementioned issues Accurest with Stub Runner were created. Their main idea is to give you very fast feedback, without the need +to set up the whole world of microservices.

+
+
+
+Stubbed Services +
+
+
+

If you work on stubs then the only applications you need are those that your application is using directly.

+
+
+
+Stubbed Services +
+
+
+

Accurest gives you the certainty that the stubs that you’re using were created by the service that you’re calling. Also if you can use them it means that they were +tested against the producer’s side. In other words - you can trust those stubs.

+
+
+
+
+

Purposes

+
+

The main purposes of Accurest with Stub Runner are:

+
+
+
    +
  • +

    to ensure that WireMock / Messaging stubs (used when developing the client) are doing exactly what actual server-side implementation will do,

    +
  • +
  • +

    to promote ATDD method and Microservices architectural style,

    +
  • +
  • +

    to provide a way to publish changes in contracts that are immediately visible on both sides,

    +
  • +
  • +

    to generate boilerplate test code used on the server side.

    +
  • +
+
+
+
+

Client Side

+
+

During the tests you want to have a Wiremock instance / Messaging route up and running that simulates the service Y. +You would like to feed that instance with a proper stub definition. That stub definition would need +to be valid and should also be reusable on the server side.

+
+
+

Summing it up: On this side, in the stub definition, you can use patterns for request stubbing and you need exact +values for responses.

+
+
+
+

Server Side

+
+

Being a service Y since you are developing your stub, you need to be sure that it’s actually resembling your +concrete implementation. You can’t have a situation where your stub acts in one way and your application on +production behaves in a different way.

+
+
+

That’s why from the provided stub acceptance tests will be generated that will ensure +that your application behaves in the same way as you define in your stub.

+
+
+

Summing it up: On this side, in the stub definition, you need exact values as request and can use patterns/methods +for response verification.

+
+
+
+

Dependencies

+
+

Accurest and Stub Runner are using the following libraries

+
+ +
+
+ +
+

Below you can find some resources related to Accurest and Stub Runner. Note that some can be outdated since the Accurest project +is under constant development.

+
+
+

Videos

+
+

Olga Maciaszek-Sharma talking about Accurest

+
+
+
+ +
+
+
+

Marcin Grzejszczak and Jakub Kubryński talking about Accurest

+
+
+
+ +
+
+
+ +
+
+

Samples

+
+

Here you can find some working samples. Check the readme of each project for more information.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/messaging.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/messaging.html new file mode 100644 index 0000000000..623d82b4b3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/messaging.html @@ -0,0 +1,914 @@ + + + + + + + +Accurest Messaging + + + + + + + +
+
+

Accurest Messaging

+
+
+ + + + + +
+ + +Feature available since {messaging_version} +
+
+
+

Accurest allows you to verify your application that uses messaging as means of communication. +All of our integrations are working with Spring but you can also set one yourself.

+
+
+

Integrations

+
+

You can use one of the three integration configurations:

+
+
+
    +
  • +

    Apache Camel

    +
  • +
  • +

    Spring Integration

    +
  • +
  • +

    Spring Cloud Stream

    +
  • +
+
+
+

If you’re using Spring Boot, the aforementioned test configurations will be appended automatically.

+
+
+

You have to provide as a dependency one of the Accurest Messaging modules. Example for Gradle:

+
+
+
+
// for Apache Camel
+testCompile "io.codearte.accurest:accurest-messaging-camel:${accurestVersion}"
+// for Spring Integration
+testCompile "io.codearte.accurest:accurest-messaging-integration:${accurestVersion}"
+// for Spring Cloud Stream
+testCompile "io.codearte.accurest:accurest-messaging-stream:${accurestVersion}"
+
+
+
+
+

Manual Integration

+
+

The accurest-messaging-core module contains 3 main interfaces:

+
+
+
    +
  • +

    AccurestMessage - describes a message received / sent to a channel / queue / topic etc.

    +
  • +
  • +

    AccurestMessageBuilder - describes how to build a message

    +
  • +
  • +

    AccurestMessaging - class that allows you to build, send and receive messages

    +
  • +
  • +

    AccurestFilter - interface to filter out the messages that do not follow the pattern from the DSL

    +
  • +
+
+
+

In the generated test the AccurestMessaging is injected via @Inject annotation thus you can use other injection +frameworks than Spring.

+
+
+

You have to provide as a dependency the accurest-messaging-core module. Example for Gradle:

+
+
+
+
testCompile "io.codearte.accurest:accurest-messaging-core:${accurestVersion}"
+
+
+
+
+

Publisher side test generation

+
+

Having the input or outputMessage sections in your DSL will result in creation of tests on the publisher’s side. By default +JUnit tests will be created, however there is also a possibility to create Spock tests.

+
+
+

There are 3 main scenarios that we should take into consideration:

+
+
+
    +
  • +

    Scenario 1: there is no input message that produces an output one. The output message is triggered by a component +inside the application (e.g. scheduler)

    +
  • +
  • +

    Scenario 2: the input message triggers an output message

    +
  • +
  • +

    Scenario 3: the input message is consumed and there is no output message

    +
  • +
+
+
+

Scenario 1 (no input message)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('activemq:output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+ // when:
+  bookReturnedTriggered();
+
+ // then:
+  AccurestMessage response = accurestMessaging.receiveMessage("activemq:output");
+  assertThat(response).isNotNull();
+  assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo");
+ // and:
+  DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.getPayload()));
+  assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
'''
+ when:
+  bookReturnedTriggered()
+
+ then:
+  def response = accurestMessaging.receiveMessage('activemq:output')
+  assert response != null
+  response.getHeader('BOOK-NAME')  == 'foo'
+ and:
+  DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.payload))
+  assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
+
+'''
+
+
+
+
+

Scenario 2 (output triggered by input)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                messageFrom('jms:input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+// given:
+ AccurestMessage inputMessage = accurestMessaging.create(
+  "{\\"bookName\\":\\"foo\\"}"
+, headers()
+  .header("sample", "header"));
+
+// when:
+ accurestMessaging.send(inputMessage, "jms:input");
+
+// then:
+ AccurestMessage response = accurestMessaging.receiveMessage("jms:output");
+ assertThat(response).isNotNull();
+ assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo");
+// and:
+ DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.getPayload()));
+ assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
"""\
+given:
+   def inputMessage = accurestMessaging.create(
+    '''{"bookName":"foo"}''',
+    ['sample': 'header']
+  )
+
+when:
+   accurestMessaging.send(inputMessage, 'jms:input')
+
+then:
+   def response = accurestMessaging.receiveMessage('jms:output')
+   assert response !- null
+   response.getHeader('BOOK-NAME')  == 'foo'
+and:
+   DocumentContext parsedJson = JsonPath.parse(accurestObjectMapper.writeValueAsString(response.payload))
+   assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
+"""
+
+
+
+
+

Scenario 3 (no output message)

+
+

For the given contract:

+
+
+
+
def contractDsl = GroovyDsl.make {
+        label 'some_label'
+        input {
+                messageFrom('jms:delete')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+                assertThat('bookWasDeleted()')
+        }
+}
+
+
+
+

The following JUnit test will be created:

+
+
+
+
'''
+// given:
+ AccurestMessage inputMessage = accurestMessaging.create(
+        "{\\"bookName\\":\\"foo\\"}"
+, headers()
+        .header("sample", "header"));
+
+// when:
+ accurestMessaging.send(inputMessage, "jms:delete");
+
+// then:
+ bookWasDeleted();
+'''
+
+
+
+

And the following Spock test would be created:

+
+
+
+
'''
+given:
+         def inputMessage = accurestMessaging.create(
+                \'\'\'{"bookName":"foo"}\'\'\',
+                ['sample': 'header']
+        )
+
+when:
+         accurestMessaging.send(inputMessage, 'jms:delete')
+
+then:
+         noExceptionThrown()
+         bookWasDeleted()
+'''
+
+
+
+
+
+

Consumer Stub Side generation

+
+

Unlike the HTTP part - in Messaging we need to publish the Groovy DSL inside the JAR with a stub. Then it’s parsed on the consumer side +and proper stubbed routes are created.

+
+
+

For more infromation please consult the Stub Runner Messaging sections.

+
+
+

Gradle Setup

+
+

Example of Accurest Gradle setup:

+
+
+
+
ext {
+        contractsDir = file("mappings")
+        stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
+        wireMockStubsOutputDir = file(new File(stubsOutputDirRoot, 'repository/mappings/'))
+        contractsOutputDir = file(new File(stubsOutputDirRoot, 'repository/accurest/'))
+}
+
+task copyContracts(type: Copy) {
+        from contractsDir
+        include '**/*.groovy'
+        into contractsOutputDir
+}
+
+task stubsJar(type: Jar, dependsOn: ["generateWireMockClientStubs", copyContracts]) {
+        baseName = "${project.name}"
+        classifier = "stubs"
+        from stubsOutputDirRoot
+}
+
+artifacts {
+        archives stubsJar
+}
+
+publishing {
+        publications {
+                stubs(MavenPublication) {
+                        artifactId "${project.name}-stubs"
+                        artifact stubsJar
+                }
+        }
+}
+
+
+
+
+

Maven Setup

+
+

Example of Maven can be found in the Accurest Maven Plugin README

+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/migration.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/migration.html new file mode 100644 index 0000000000..351572a2e3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/migration.html @@ -0,0 +1,594 @@ + + + + + + + +Migration Guide + + + + + + + +
+
+

Migration Guide

+
+
+

Migration to 0.4.7

+
+
    +
  • +

    in 0.4.7 we’ve fixed package name (coderate to codearte) so you’ve to do the same in your projects. This means replacing io.coderate.accurest.dsl.GroovyDsl with io.codearte.accurest.dsl.GroovyDsl

    +
  • +
+
+
+
+

Migration to 1.0.0-RC1

+
+
    +
  • +

    from 1.0.0 we’re distinguish ignored contracts from excluded contracts:

    +
  • +
  • +

    excludedFiles pattern tells Accurest to skip processing those files at all

    +
  • +
  • +

    ignoredFiles pattern tells Accurest to generate contracts and tests, but tests will be marked as @Ignore

    +
  • +
  • +

    from 1.0.0 the basePackageForTests behaviour has changed

    +
  • +
  • +

    prior to the change all DSL files had to be under contractsDslDir/basePackageForTests/subpackage resulting in basePackageForTests.subpackage test package creation

    +
  • +
  • +

    now all DSL files have to be under contractsDslDir/subpackage resulting in basePackageForTests.subpackage test package creation

    +
  • +
  • +

    If you don’t migrate to the new approach you will have your tests under contractsDslDir.contractsDslDir.subpackage

    +
  • +
+
+
+
+

Migration to {messaging_version}

+
+
    +
  • +

    from {messaging_version} we’re setting JUnit as a default testing utility. You have to pass the following option to keep Spock +as your first choice:

    +
  • +
+
+
+
+
targetFramework = 'Spock'
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/rest.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/rest.html new file mode 100644 index 0000000000..bab2a70944 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/rest.html @@ -0,0 +1,1032 @@ + + + + + + + +Accurest HTTP + + + + + + + +
+
+

Accurest HTTP

+
+
+

Gradle Project

+
+

Prerequisites

+
+

In order to use Accurest with Wiremock you have to use gradle or maven plugin.

+
+
+
Add gradle plugin
+
+
+
buildscript {
+        repositories {
+                mavenCentral()
+        }
+        dependencies {
+                classpath 'io.codearte.accurest:accurest-gradle-plugin:${accurest_version}'
+        }
+}
+
+apply plugin: 'groovy'
+apply plugin: 'accurest'
+
+dependencies {
+        testCompile 'org.codehaus.groovy:groovy-all:2.4.6'
+        testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
+        testCompile 'com.jayway.restassured:spring-mock-mvc:2.9.0' // needed if you're going to use Spring MockMvc
+}
+
+
+
+
+
Add maven plugin
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

Read more: accurest-maven-plugin

+
+
+
+
Add stubs
+
+

By default Accurest is looking for stubs in src/test/resources/accurest directory.

+
+
+

Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure

+
+
+
+
src/test/resources/accurest/myservice/shouldCreateUser.groovy
+src/test/resources/accurest/myservice/shouldReturnUser.groovy
+
+
+
+

Accurest will create test class defaultBasePackage.MyService with two methods

+
+
+
    +
  • +

    shouldCreateUser()

    +
  • +
  • +

    shouldReturnUser()

    +
  • +
+
+
+
+
+

Run plugin

+
+

Plugin registers itself to be invoked before check task. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke generateAccurest task.

+
+
+
+

Configure plugin

+
+

To change default configuration just add accurest snippet to your Gradle config

+
+
+
+
accurest {
+        testMode = 'MockMvc'
+        baseClassForTests = 'org.mycompany.tests'
+        generatedTestSourcesDir = project.file('src/accurest')
+}
+
+
+
+
Configuration options
+
+
    +
  • +

    testMode - defines mode for acceptance tests. By default MockMvc which is based on Spring’s MockMvc. It can also be changed to JaxRsClient or to Explicit for real HTTP calls.

    +
  • +
  • +

    imports - array with imports that should be included in generated tests (for example ['org.myorg.Matchers']). By default empty array []

    +
  • +
  • +

    staticImports - array with static imports that should be included in generated tests(for example ['org.myorg.Matchers.*']). By default empty array []

    +
  • +
  • +

    basePackageForTests - specifies base package for all generated tests. By default set to io.codearte.accurest.tests

    +
  • +
  • +

    baseClassForTests - base class for generated tests. By default spock.lang.Specification if using Spock tests.

    +
  • +
  • +

    ruleClassForTests - specifies Rule which should be added to generated test classes.

    +
  • +
  • +

    ignoredFiles - Ant matcher allowing defining stub files for which processing should be skipped. By default empty array []

    +
  • +
  • +

    contractsDslDir - directory containing contracts written using the GroovyDSL. By default $rootDir/src/test/resources/accurest

    +
  • +
  • +

    generatedTestSourcesDir - test source directory where tests generated from Groovy DSL should be placed. By default $buildDir/generated-test-sources/accurest

    +
  • +
  • +

    stubsOutputDir - dir where the generated Wiremock stubs from Groovy DSL should be placed

    +
  • +
  • +

    targetFramework - the target test framework to be used; currently Spock and JUnit are supported with JUnit being the default framework

    +
  • +
+
+
+
+
Base class for tests
+
+

When using Accurest in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified.

+
+
+
+
abstract class BaseMockMvcSpec extends Specification {
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new PairIdController())
+        }
+
+        void isProperCorrelationId(Integer correlationId) {
+                assert correlationId == 123456
+        }
+
+        void isEmpty(String value) {
+                assert value == null
+        }
+
+}
+
+
+
+

In case of using Explicit mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of JAXRSCLIENT mode this base class +should also contain protected WebTarget webTarget field, right now the only option to test JAX-RS API is to start a web server.

+
+
+
+
+

Invoking generated tests

+
+

To ensure that provider side is complaint with defined contracts, you need to invoke:

+
+
+
+
./gradlew generateAccurest test
+
+
+
+
+

Accurest on consumer side

+
+

In consumer service you need to configure Accurest plugin in exactly the same way as in case of provider. If you don’t want to use Stub Runner then you need to copy contracts stored in +src/test/resources/accurest and generate WireMock json stubs using:

+
+
+
+
./gradlew generateWireMockClientStubs
+
+
+
+

Note that stubsOutputDir option has to be set for stub generation to work.

+
+
+

When present, json stubs can be used in consumer automated tests.

+
+
+
+
@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
+class LoanApplicationServiceSpec extends Specification {
+
+ @ClassRule
+ @Shared
+ WireMockClassRule wireMockRule == new WireMockClassRule()
+
+ @Autowired
+ LoanApplicationService sut
+
+ def 'should successfully apply for loan'() {
+   given:
+         LoanApplication application =
+                        new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123)
+   when:
+        LoanApplicationResult loanApplication == sut.loanApplication(application)
+   then:
+        loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED
+        loanApplication.rejectionReason === null
+ }
+}
+
+
+
+

Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Accurest.

+
+
+
+
+

Using in your Maven project

+
+

Add maven plugin

+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

Read more: accurest-maven-plugin

+
+
+
+

Add stubs

+
+

By default Accurest is looking for stubs in src/test/resources/accurest directory. +Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure

+
+
+
+
src/test/resources/accurest/myservice/shouldCreateUser.groovy
+src/test/resources/accurest/myservice/shouldReturnUser.groovy
+
+
+
+

Accurest will create test class defaultBasePackage.MyService with two methods + - shouldCreateUser() + - shouldReturnUser()

+
+
+
+

Run plugin

+
+

Plugin goal generateTests is assigned to be invoked in phase generate-test-sources. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke generateTests goal.

+
+
+
+

Configure plugin

+
+

To change default configuration just add configuration section to plugin definition or execution definition.

+
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+                <goal>generateTests</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <basePackageForTests>com.ofg.twitter.place</basePackageForTests>
+        <baseClassForTests>com.ofg.twitter.place.BaseMockMvcSpec</baseClassForTests>
+    </configuration>
+</plugin>
+
+
+
+
Important configuration options
+
+
    +
  • +

    testMode - defines mode for acceptance tests. By default MockMvc which is based on Spring’s MockMvc. It can also be changed to JaxRsClient or to Explicit for real HTTP calls.

    +
  • +
  • +

    basePackageForTests - specifies base package for all generated tests. By default set to io.codearte.accurest.tests.

    +
  • +
  • +

    ruleClassForTests - specifies Rule which should be added to generated test classes.

    +
  • +
  • +

    baseClassForTests - base class for generated tests. By default spock.lang.Specification if using Spock tests.

    +
  • +
  • +

    contractsDir - directory containing contracts written using the GroovyDSL. By default /src/test/resources/accurest.

    +
  • +
  • +

    testFramework - the target test framework to be used; currently Spock and JUnit are supported with Spock being the default framework

    +
  • +
+
+
+

For complete information take a look at Plugin Documentation

+
+
+
+
Base class for tests
+
+
+
When using Accurest in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified.
+
+
+
+
+
package org.mycompany.tests
+
+import org.mycompany.ExampleSpringController
+import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
+import spock.lang.Specification
+
+class  MvcSpec extends Specification {
+  def setup() {
+   RestAssuredMockMvc.standaloneSetup(new ExampleSpringController())
+  }
+}
+
+
+
+

In case of using Explicit mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of JAXRSCLIENT mode this base class should also contain protected WebTarget webTarget field, right now the only option to test JAX-RS API is to start a web server.

+
+
+
+
+

Invoking generated tests

+
+

Accurest Maven Plugins generates verification code into directory /generated-test-sources/accurest and attach this directory to testCompile goal.

+
+
+

For Groovy Spock code use:

+
+
+
+
<plugin>
+        <groupId>org.codehaus.gmavenplus</groupId>
+        <artifactId>gmavenplus-plugin</artifactId>
+        <version>1.5</version>
+        <executions>
+                <execution>
+                        <goals>
+                                <goal>testCompile</goal>
+                        </goals>
+                </execution>
+        </executions>
+        <configuration>
+                <testSources>
+                        <testSource>
+                                <directory>${project.basedir}/src/test/groovy</directory>
+                                <includes>
+                                        <include>**/*.groovy</include>
+                                </includes>
+                        </testSource>
+                        <testSource>
+                                <directory>${project.build.directory}/generated-test-sources/accurest</directory>
+                                <includes>
+                                        <include>**/*.groovy</include>
+                                </includes>
+                        </testSource>
+                </testSources>
+        </configuration>
+</plugin>
+
+
+
+

To ensure that provider side is complaint with defined contracts, you need to invoke mvn generateTest test

+
+
+
+

Accurest on consumer side

+
+

In consumer service you need to configure Accurest plugin in exactly the same way as in case of provider. You need to copy contracts stored in src/test/resources/accurest and generate Wiremock json stubs using: mvn generateStubs command. By default generated WireMock mapping is stored in directory target/mappings. Your project should create from this generated mappings additional artifact with classifier stubs for easy deploy to maven repository.

+
+
+

Sample configuration:

+
+
+
+
<plugin>
+    <groupId>io.codearte.accurest</groupId>
+    <artifactId>accurest-maven-plugin</artifactId>
+    <version>${accurest-plugin.version}</version>
+    <executions>
+        <execution>
+            <goals>
+                <goal>convert</goal>
+                <goal>generateStubs</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+
+
+

When present, json stubs can be used in consumer automated tests.

+
+
+
+
@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
+class LoanApplicationServiceSpec extends Specification {
+
+ @ClassRule
+ @Shared
+ WireMockClassRule wireMockRule == new WireMockClassRule()
+
+ @Autowired
+ LoanApplicationService sut
+
+ def 'should successfully apply for loan'() {
+   given:
+         LoanApplication application =
+                        new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123)
+   when:
+        LoanApplicationResult loanApplication == sut.loanApplication(application)
+   then:
+        loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED
+        loanApplication.rejectionReason === null
+ }
+}
+
+
+
+

Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Accurest.

+
+
+
+
+

Scenarios

+
+

It’s possible to handle scenarios with Accurest. All you need to do is to stick to proper naming convention while creating your contracts. The convention requires to include order number followed by the underscore.

+
+
+
+
my_contracts_dir\
+  scenario1\
+    1_login.groovy
+    2_showCart.groovy
+    3_logout.groovy
+
+
+
+

Such tree will cause Accurest generating Wiremock’s scenario with name scenario1 and three steps: + - login marked as Started pointing to: + - showCart marked as Step1 pointing to: + - logout marked as Step2 which will close the scenario. +More details about Wiremock scenarios can be found under [http://wiremock.org/stateful-behaviour.html](http://wiremock.org/stateful-behaviour.html)

+
+
+

Accurest will also generate tests with guaranteed order of execution.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner.html new file mode 100644 index 0000000000..901a8fd60f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner.html @@ -0,0 +1,1258 @@ + + + + + + + +Stub Runner + + + + + + + +
+
+

Stub Runner

+
+
+

One of the issues that you could have encountered while using Accurest was to pass the generated WireMock JSON stubs from the server side to the client side (or various clients). + The same takes place in terms of client side generation for messaging.

+
+
+

Copying the JSON files / setting the client side for messaging manually is out of the question.

+
+
+

Publishing stubs as JARs

+
+

The easiest approach would be to centralize the way stubs are kept. For example you can keep them as JARs in a Maven repository.

+
+
+

Gradle

+
+

Example of Accurest Gradle setup:

+
+
+
+
ext {
+        contractsDir = file("mappings")
+        stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
+        wireMockStubsOutputDir = file(new File(stubsOutputDirRoot, 'repository/mappings/'))
+        contractsOutputDir = file(new File(stubsOutputDirRoot, 'repository/accurest/'))
+}
+
+task copyContracts(type: Copy) {
+        from contractsDir
+        include '**/*.groovy'
+        into contractsOutputDir
+}
+
+task stubsJar(type: Jar, dependsOn: ["generateWireMockClientStubs", copyContracts]) {
+        baseName = "${project.name}"
+        classifier = "stubs"
+        from stubsOutputDirRoot
+}
+
+artifacts {
+        archives stubsJar
+}
+
+publishing {
+        publications {
+                stubs(MavenPublication) {
+                        artifactId "${project.name}-stubs"
+                        artifact stubsJar
+                }
+        }
+}
+
+
+
+
+

Maven

+
+

Example of Maven can be found in the Accurest Maven Plugin README

+
+
+
+
+

Modules

+
+

Accurest comes with a new structure of modules

+
+
+
+
└── stub-runner
+    ├── stub-runner
+    ├── stub-runner-boot
+    ├── stub-runner-junit
+    ├── stub-runner-spring
+    └── stub-runner-spring-cloud
+
+
+
+
+

Stub Runner Core

+
+

Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +Consumer Driven Contracts.

+
+
+

Stub Runner allows you to automatically download the stubs of the provided dependencies, start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined.

+
+
+

Running stubs

+
+
Running using main app
+
+

You can set the following options to the main class:

+
+
+
+
-maxp (--maxPort) N            : Maximum port value to be assigned to the
+                                 Wiremock instance. Defaults to 15000
+                                 (default: 15000)
+-minp (--minPort) N            : Minimal port value to be assigned to the
+                                 Wiremock instance. Defaults to 10000
+                                 (default: 10000)
+-s (--stubs) VAL               : Comma separated list of Ivy representation of
+                                 jars with stubs. Eg. groupid:artifactid1,group
+                                 id2:artifactid2:version:classifier
+-sr (--stubRepositoryRoot) VAL : Location of a Jar containing server where you
+                                 keep your stubs (e.g. http://nexus.net/content
+                                 /repositories/repository)
+-ss (--stubsSuffix) VAL        : Suffix for the jar containing stubs (e.g.
+                                 'stubs' if the stub jar would have a 'stubs'
+                                 classifier for stubs: foobar-stubs ).
+                                 Defaults to 'stubs' (default: stubs)
+-wo (--workOffline)            : Switch to work offline. Defaults to 'false'
+                                 (default: false)
+
+
+
+
+
Building a Fat Jar
+
+

Just call the following command:

+
+
+
+
./gradlew stub-runner-root:stub-runner:shadowJar -PfatJar
+
+
+
+

and inside the build/lib there will be a Fat Jar with classifier fatJar waiting for you to execute. E.g.

+
+
+
+
java -jar stub-runner/stub-runner/build/libs/stub-runner-1.0.1-SNAPSHOT-fatJar.jar -sr http://a.b.com -s a:b:c,d:e,f:g:h:i
+
+
+
+
+
+

Stub runner configuration

+
+

You can configure the stub runner by either passing the full arguments list with the -Pargs like this:

+
+
+
+
./gradlew stub-runner-root:stub-runner:run -Pargs="-c pl -minp 10000 -maxp 10005 -s a:b:c,d:e,f:g:h"
+
+
+
+

or each parameter separately with a -P prefix and without the hyphen - in the name of the param

+
+
+
+
./gradlew stub-runner-root:stub-runner:run -Pc=pl -Pminp=10000 -Pmaxp=10005 -Ps=a:b:c,d:e,f:g:h
+
+
+
+
HTTP Stubs
+
+

Stubs are defined in JSON documents, whose syntax is defined in WireMock documentation

+
+
+

Example:

+
+
+
+
{
+    "request": {
+        "method": "GET",
+        "url": "/ping"
+    },
+    "response": {
+        "status": 200,
+        "body": "pong",
+        "headers": {
+            "Content-Type": "text/plain"
+        }
+    }
+}
+
+
+
+
+
Viewing registered mappings
+
+

Every stubbed collaborator exposes list of defined mappings under __/admin/ endpoint.

+
+
+
+
Messaging Stubs
+
+

Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up.

+
+
+
+
+
+

Stub Runner Boot

+
+ + + + + +
+ + +Feature available since {messaging_version} +
+
+
+

Accurest Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers.

+
+
+

One of the usecases is to run some smoke (end to end) tests on a deployed application. You can read + more about this in the "Microservice Deployment" article at Too Much Coding blog.

+
+
+

How to use it?

+
+

Just add the

+
+
+
+
compile "io.codearte.accurest:stub-runner-boot:${accurestVersion}"
+
+
+
+

and a messaging implementation:

+
+
+
+
// for Apache Camel
+compile "io.codearte.accurest:stub-runner-messaging-camel:${accurestVersion}"
+// for Spring Integration
+compile "io.codearte.accurest:stub-runner-messaging-integration:${accurestVersion}"
+// for Spring Cloud Stream
+compile "io.codearte.accurest:stub-runner-messaging-stream:${accurestVersion}"
+
+
+
+

Build a fat-jar and you’re ready to go!

+
+
+

For the properties check the Stub Runner Spring section.

+
+
+
+

Endpoints

+
+
HTTP
+
+
    +
  • +

    GET /stubs - returns a list of all running stubs in ivy:integer notation

    +
  • +
  • +

    GET /stubs/{ivy} - returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    +
  • +
+
+
+
+
Messaging
+
+

For Messaging

+
+
+
    +
  • +

    GET /triggers - returns a list of all running labels in ivy : [ label1, label2 …​] notation

    +
  • +
  • +

    POST /triggers/{label} - executes a trigger with label

    +
  • +
  • +

    POST /triggers/{ivy}/{label} - executes a trigger with label for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    +
  • +
+
+
+
+
+

Example

+
+
+
@ContextConfiguration(classes = [StubRunnerBootSpec, StubRunnerBoot], loader = SpringApplicationContextLoader)
+@EnableBinding
+@Configuration
+class StubRunnerBootSpec extends Specification {
+
+        @Autowired StubRunning stubRunning
+
+        def setup() {
+                RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
+                                new TriggerController(stubRunning))
+        }
+
+        def 'should return a list of running stub servers in "full ivy:port" notation'() {
+                when:
+                        String response = RestAssuredMockMvc.get('/stubs').body.asString()
+                then:
+                        def root = new JsonSlurper().parseText(response)
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs' instanceof Integer
+        }
+
+        def 'should return a port on which a [#stubId] stub is running'() {
+                when:
+                        def response = RestAssuredMockMvc.get("/stubs/${stubId}")
+                then:
+                        response.statusCode == 200
+                        response.body.as(Integer) > 0
+                where:
+                        stubId << ['io.codearte.accurest.stubs:streamService:+:stubs',
+                                           'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs',
+                                           'io.codearte.accurest.stubs:streamService:+',
+                                           'io.codearte.accurest.stubs:streamService',
+                                           'streamService']
+        }
+
+        def 'should return 404 when missing stub was called'() {
+                when:
+                        def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
+                then:
+                        response.statusCode == 404
+        }
+
+        def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
+                when:
+                        String response = RestAssuredMockMvc.get('/triggers').body.asString()
+                then:
+                        def root = new JsonSlurper().parseText(response)
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"])
+        }
+
+        def 'should trigger a messaging label'() {
+                given:
+                        StubRunning stubRunning = Mock()
+                        RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/delete_book")
+                then:
+                        response.statusCode == 200
+                and:
+                        1 * stubRunning.trigger('delete_book')
+        }
+
+        def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
+                given:
+                        StubRunning stubRunning = Mock()
+                        RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
+                then:
+                        response.statusCode == 200
+                and:
+                        1 * stubRunning.trigger(stubId, 'delete_book')
+                where:
+                        stubId << ['io.codearte.accurest.stubs:streamService:stubs', 'io.codearte.accurest.stubs:streamService', 'streamService']
+        }
+
+        def 'should return when trigger is missing'() {
+                when:
+                        def response = RestAssuredMockMvc.post("/triggers/missing_label")
+                then:
+                        response.statusCode == 404
+                        def root = new JsonSlurper().parseText(response.body.asString())
+                        root.'io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"])
+        }
+
+}
+
+
+
+
+
+

Stub Runner JUnit Rule

+
+

Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id:

+
+
+
+
@ClassRule public static AccurestRule rule = new AccurestRule()
+                .repoRoot(repoRoot())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer");
+
+
+
+

After that rule gets executed Stub Runner connects to your Maven repository and for the given list of dependencies tries to:

+
+
+
    +
  • +

    download them

    +
  • +
  • +

    cache them locally

    +
  • +
  • +

    unzip them to a temporary folder

    +
  • +
  • +

    start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port

    +
  • +
  • +

    feed the WireMock server with all JSON files that are valid WireMock definitions

    +
  • +
+
+
+

Stub Runner uses Eclipse Aether mechanism to download the Maven dependencies. +Check their docs for more information.

+
+
+

Since the AccurestRule implements the StubFinder it allows you to find the started stubs:

+
+
+
+
package io.codearte.accurest.stubrunner
+
+import io.codearte.accurest.dsl.GroovyDsl
+
+interface StubFinder extends StubTrigger {
+        /**
+         * For the given groupId and artifactId tries to find the matching
+         * URL of the running stub.
+         *
+         * @param groupId - might be null. In that case a search only via artifactId takes place
+         * @return URL of a running stub or null if not found
+         */
+        URL findStubUrl(String groupId, String artifactId)
+
+        /**
+         * For the given Ivy notation {@code groupId:artifactId} tries to find the matching
+         * URL of the running stub. You can also pass only {@code artifactId}.
+         *
+         * @param ivyNotation - Ivy representation of the Maven artifact
+         * @return URL of a running stub or null if not found
+         */
+        URL findStubUrl(String ivyNotation)
+
+        /**
+         * Returns all running stubs
+         */
+        RunningStubs findAllRunningStubs()
+
+        /**
+         * Returns the list of Accurest contracts
+         */
+        Map<StubConfiguration, Collection<GroovyDsl>> getAccurestContracts()
+}
+
+
+
+

Example of usage in Spock tests:

+
+
+
+
@ClassRule @Shared AccurestRule rule = new AccurestRule()
+                .repoRoot(AccurestRuleSpec.getResource("/m2repo").toURI().toString())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer")
+
+def 'should start WireMock servers'() {
+        expect: 'WireMocks are running'
+                rule.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance') != null
+                rule.findStubUrl('loanIssuance') != null
+                rule.findStubUrl('loanIssuance') == rule.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance')
+                rule.findStubUrl('io.codearte.accurest.stubs:fraudDetectionServer') != null
+        and:
+                rule.findAllRunningStubs().isPresent('loanIssuance')
+                rule.findAllRunningStubs().isPresent('io.codearte.accurest.stubs', 'fraudDetectionServer')
+                rule.findAllRunningStubs().isPresent('io.codearte.accurest.stubs:fraudDetectionServer')
+        and: 'Stubs were registered'
+                "${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
+                "${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
+}
+
+
+
+

Example of usage in JUnit tests:

+
+
+
+
@Test
+public void should_start_wiremock_servers() throws Exception {
+        // expect: 'WireMocks are running'
+                then(rule.findStubUrl("io.codearte.accurest.stubs", "loanIssuance")).isNotNull();
+                then(rule.findStubUrl("loanIssuance")).isNotNull();
+                then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl("io.codearte.accurest.stubs", "loanIssuance"));
+                then(rule.findStubUrl("io.codearte.accurest.stubs:fraudDetectionServer")).isNotNull();
+        // and:
+                then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
+                then(rule.findAllRunningStubs().isPresent("io.codearte.accurest.stubs", "fraudDetectionServer")).isTrue();
+                then(rule.findAllRunningStubs().isPresent("io.codearte.accurest.stubs:fraudDetectionServer")).isTrue();
+        // and: 'Stubs were registered'
+                then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance");
+                then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer");
+}
+
+
+
+

Check the Common properties for JUnit and Spring for more information on how to apply global configuration of Stub Runner.

+
+
+

Providing fixed ports

+
+

You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule.

+
+
+
+

Fluent API

+
+

When using the AccurestRule you can add a stub to download and then pass the port for the last downloaded stub.

+
+
+
+
@ClassRule public static AccurestRule rule = new AccurestRule()
+                .repoRoot(repoRoot())
+                .downloadStub("io.codearte.accurest.stubs", "loanIssuance")
+                .withPort(12345)
+                .downloadStub("io.codearte.accurest.stubs:fraudDetectionServer:12346");
+
+
+
+

You can see that for this example the following test is valid:

+
+
+
+
then(rule.findStubUrl("loanIssuance")).isEqualTo(URI.create("http://localhost:12345").toURL());
+then(rule.findStubUrl("fraudDetectionServer")).isEqualTo(URI.create("http://localhost:12346").toURL());
+
+
+
+
+
+

Stub Runner Spring

+
+

Sets up Spring configuration of the Stub Runner project.

+
+
+

By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs.

+
+
+

If you want to find the URL of your stubbed dependency you can autowire the StubFinder interface and use +its methods as presented below:

+
+
+
+
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
+class StubRunnerConfigurationSpec extends Specification {
+
+        @Autowired StubFinder stubFinder
+
+        def 'should start WireMock servers'() {
+                expect: 'WireMocks are running'
+                        stubFinder.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance') != null
+                        stubFinder.findStubUrl('loanIssuance') != null
+                        stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('io.codearte.accurest.stubs', 'loanIssuance')
+                        stubFinder.findStubUrl('io.codearte.accurest.stubs:fraudDetectionServer') != null
+                and:
+                        stubFinder.findAllRunningStubs().isPresent('loanIssuance')
+                        stubFinder.findAllRunningStubs().isPresent('io.codearte.accurest.stubs', 'fraudDetectionServer')
+                        stubFinder.findAllRunningStubs().isPresent('io.codearte.accurest.stubs:fraudDetectionServer')
+                and: 'Stubs were registered'
+                        "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
+                        "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
+        }
+
+        @Configuration
+        @Import(StubRunnerConfiguration)
+        @EnableAutoConfiguration
+        static class Config {}
+}
+
+
+
+

for the following configuration file:

+
+
+
+
stubrunner.stubs.repository.root: classpath:m2repo/repository/
+stubrunner.stubs.ids: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
+
+
+
+
+

Stub Runner Spring Cloud

+
+

Registers the stubs in the provided Service Discovery. It’s enough to add the jar

+
+
+
+
io.codearte.accurest:stub-runner-spring-cloud
+
+
+
+

and the Stub Runner autoconfiguration should be picked up.

+
+
+

Stubbing Service Discovery

+
+

The most important feature of Stub Runner Spring Cloud is the fact that it’s stubbing

+
+
+
    +
  • +

    DiscoveryClient

    +
  • +
  • +

    Ribbon ServerList

    +
  • +
+
+
+

that means that regardles of the fact whether you’re using Zookeeper, Consul, Eureka or anything else, you don’t need that in your tests. +We’re starting WireMock instances of your dependencies and we’re telling your application whenever you’re using Feign, load balanced RestTemplate +or DiscoveryClient directly, to call those stubbed servers instead of calling the real Service Discovery tool.

+
+
+
+

Additional Configuration

+
+

You can match the artifactId of the stub with the name of your app by using the stubrunner.stubs.idsToServiceIds: map. +You can disable Stub Runner Ribbon support by providing: stubrunner.cloud.ribbon.enabled equal to false +You can disable Stub Runner support by providing: stubrunner.cloud.enabled equal to false

+
+
+
+
+

Common properties for JUnit and Spring

+
+

Some of the properties that are repetitive can be set using system properties or property sources (for Spring). Here are their names with their default values:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property nameDefault valueDescription

stubrunner.port.range.min

10000

Minimal value of a port for a started WireMock with stubs

stubrunner.port.range.max

15000

Minimal value of a port for a started WireMock with stubs

stubrunner.stubs.repository.root

Comma separated list of Maven repo urls. If blank then will call the local maven repo

stubrunner.stubs.classifier

stubs

Default classifier for the stub artifacts

stubrunner.work-offline

false

If true then will not contact any remote repositories to download stubs

stubrunner.stubs.ids

Comma separated list of Ivy notation of stubs to download

+
+

Stub runner stubs ids

+
+

You can provide the stubs to download via the stubrunner.stubs.ids system property. They follow the following pattern:

+
+
+
+
groupId:artifactId:version:classifier:port
+
+
+
+

version, classifier and port are optional.

+
+
+
    +
  • +

    If you don’t provide the port then a random one will be picked

    +
  • +
  • +

    If you don’t provide the classifier then the default one will be taken.

    +
  • +
  • +

    If you don’t provide the version then the + will be passed and the latest one will be downloaded

    +
  • +
+
+
+

Where port means the port of the WireMock server.

+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner_msg.html b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner_msg.html new file mode 100644 index 0000000000..ae7b656eea --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/deprecated/stubrunner_msg.html @@ -0,0 +1,1247 @@ + + + + + + + +Stub Runner for Messaging + + + + + + + +
+
+

Stub Runner for Messaging

+
+
+ + + + + +
+ + +Feature available since {messaging_version} +
+
+
+

Stub Runner has the functionality to run the published stubs in memory. It can integrate with the following frameworks out of the box

+
+
+
    +
  • +

    Spring Integration

    +
  • +
  • +

    Spring Cloud Stream

    +
  • +
  • +

    Apache Camel

    +
  • +
+
+
+

It also provides points of entry to integrate with any other solution on the market.

+
+
+

Stub triggering

+
+

To trigger a message it’s enough to use the StubTigger interface:

+
+
+
+
package io.codearte.accurest.stubrunner
+
+interface StubTrigger {
+
+        /**
+         * Triggers an event by a given label for a given {@code groupid:artifactid} notation. You can use only {@code artifactId} too.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger(String ivyNotation, String labelName)
+
+        /**
+         * Triggers an event by a given label.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger(String labelName)
+
+        /**
+         * Triggers all possible events.
+         *
+         * Feature related to messaging.
+         *
+         * @return true - if managed to run a trigger
+         */
+        boolean trigger()
+
+        /**
+         * Returns a mapping of ivy notation of a dependency to all the labels it has.
+         *
+         * Feature related to messaging.
+         */
+        Map<String, Collection<String>> labels()
+}
+
+
+
+

For convenience the StubFinder interface extends StubTrigger so it’s enough to use only one in your tests.

+
+
+

StubTrigger gives you the following options to trigger a message:

+
+
+

Trigger by label

+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+
Trigger by group and artifact ids
+
+
+
stubFinder.trigger('io.codearte.accurest.stubs:camelService', 'return_book_1')
+
+
+
+
+
Trigger by artifact ids
+
+
+
stubFinder.trigger('camelService', 'return_book_1')
+
+
+
+
+
Trigger all messages
+
+
+
stubFinder.trigger()
+
+
+
+
+
+
+

Stub Runner Messaging Camel

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-camel:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +camelService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── camelService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── camelService-0.0.1-SNAPSHOT.pom
+                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('jms:input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('jms:output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to jms:output

+
+
+
+
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
+receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the jms:output destination.

+
+
+
+
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])
+
+
+
+

Next we’ll want to listen to the output of the message sent to jms:output

+
+
+
+
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
+receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the jms:output destination.

+
+
+
+
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])
+
+
+
+
+
+
+

Stub Runner Messaging Integration

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Spring Integration. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-integration:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +integrationService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── integrationService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
+                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('output')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('input')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('output')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and the following Spring Integration Route:

+
+
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<beans:beans xmlns="http://www.springframework.org/schema/integration"
+                         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                         xmlns:beans="http://www.springframework.org/schema/beans"
+                         xsi:schemaLocation="http://www.springframework.org/schema/beans
+                        http://www.springframework.org/schema/beans/spring-beans.xsd
+                        http://www.springframework.org/schema/integration
+                        http://www.springframework.org/schema/integration/spring-integration.xsd">
+
+
+        <!-- REQUIRED FOR TESTING -->
+        <bridge input-channel="output"
+                        output-channel="outputTest"/>
+
+        <channel id="outputTest">
+                <queue/>
+        </channel>
+
+</beans:beans>
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to output

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('outputTest')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the output destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')
+
+
+
+

Next we’ll want to listen to the output of the message sent to output

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('outputTest')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the input destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
+
+
+
+
+
+
+

Stub Runner Messaging Stream

+
+

Accurest Stub Runner’s messaging module gives you an easy way to integrate with Spring Stream. +For the provided artifacts it will automatically download the stubs and register the required +routes.

+
+
+ + + + + +
+ + +In Stub Runner’s integration with Stream the messageFrom or sentTo Strings are resolved +first as a destination of a channel, and then if there is no such destination it’s resolved as a +channel name. +
+
+
+

Adding it to the project

+
+

To use it you have to add the following dependency to your project (example for Gradle):

+
+
+
+
testCompile "io.codearte.accurest:stub-runner-messaging-stream:${accurestVersion}"
+
+
+
+
+

Examples

+
+
Stubs structure
+
+

Let us assume that we have the following Maven repository with a deployed stubs for the +streamService application.

+
+
+
+
└── .m2
+    └── repository
+        └── io
+            └── codearte
+                └── accurest
+                    └── stubs
+                        └── streamService
+                            ├── 0.0.1-SNAPSHOT
+                            │   ├── streamService-0.0.1-SNAPSHOT.pom
+                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
+                            │   └── maven-metadata-local.xml
+                            └── maven-metadata-local.xml
+
+
+
+

And the stubs contain the following structure:

+
+
+
+
├── META-INF
+│   └── MANIFEST.MF
+└── repository
+    ├── accurest
+    │   ├── bookDeleted.groovy
+    │   ├── bookReturned1.groovy
+    │   └── bookReturned2.groovy
+    └── mappings
+
+
+
+

Let’s consider the following contracts (let' number it with 1):

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_1'
+        input {
+                triggeredBy('bookReturnedTriggered()')
+        }
+        outputMessage {
+                sentTo('returnBook')
+                body('''{ "bookName" : "foo" }''')
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and number 2

+
+
+
+
io.codearte.accurest.dsl.GroovyDsl.make {
+        label 'return_book_2'
+        input {
+                messageFrom('bookStorage')
+                messageBody([
+                                bookName: 'foo'
+                ])
+                messageHeaders {
+                        header('sample', 'header')
+                }
+        }
+        outputMessage {
+                sentTo('returnBook')
+                body([
+                                bookName: 'foo'
+                ])
+                headers {
+                        header('BOOK-NAME', 'foo')
+                }
+        }
+}
+
+
+
+

and the following Spring configuration:

+
+
+
+
stubrunner.stubs.repository.root: classpath:m2repo/repository/
+stubrunner.stubs.ids: io.codearte.accurest.stubs:streamService:0.0.1-SNAPSHOT:stubs
+
+spring:
+  cloud:
+    stream:
+      bindings:
+        output:
+          destination: returnBook
+        input:
+          destination: bookStorage
+
+
+
+
+
Scenario 1 (no input message)
+
+

So as to trigger a message via the return_book_1 label we’ll use the StubTrigger interface as follows

+
+
+
+
stubFinder.trigger('return_book_1')
+
+
+
+

Next we’ll want to listen to the output of the message sent to a channel whose destination is returnBook

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('returnBook')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 2 (output triggered by input)
+
+

Since the route is set for you it’s enough to just send a message to the bookStorage destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')
+
+
+
+

Next we’ll want to listen to the output of the message sent to returnBook

+
+
+
+
AccurestMessage receivedMessage = messaging.receiveMessage('returnBook')
+
+
+
+

And the received message would pass the following assertions

+
+
+
+
receivedMessage != null
+assertJsons(receivedMessage.payload)
+receivedMessage.headers.get('BOOK-NAME') == 'foo'
+
+
+
+
+
Scenario 3 (input with no output)
+
+

Since the route is set for you it’s enough to just send a message to the output destination.

+
+
+
+
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Deps.png b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Deps.png new file mode 100644 index 0000000000000000000000000000000000000000..1426814308101b8ccf6ef95fd195e37c0a180392 GIT binary patch literal 37192 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 A(f|Me literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Stubs1.png b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/images/Stubs1.png new file mode 100644 index 0000000000000000000000000000000000000000..ebadfdb91014284b917367e3d9c9c8a137da3993 GIT binary patch literal 35170 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 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/index.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/index.adoc new file mode 120000 index 0000000000..28dc0a7e0c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/index.adoc @@ -0,0 +1 @@ +spring-cloud-contract-verifier.adoc \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/introduction.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/introduction.adoc new file mode 100644 index 0000000000..86eb75f37d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/introduction.adoc @@ -0,0 +1,148 @@ +:images_url: https://raw.githubusercontent.com/Codearte/accurest/master/docs/src/main/asciidoc/images + +== Introduction + +IMPORTANT: http://codearte.github.io/accurest/deprecated[The documentation to the deprecated Accurest project in version 1.1.0 is available here.] + +TIP: The Accurest project was initially started by Marcin Grzejszczak and Jakub Kubrynski (http://codearte.io[codearte.io]) + +Just to make long story short - Spring Cloud Contract Verifier is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped +with __Contract Definition Language__ (DSL). Contract definitions are used to produce following resources: + +* JSON stub definitions to be used by Wiremock when doing integration testing on the client code (__client tests__). +Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier. +* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to +* Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). +Full test is generated by Spring Cloud Contract Verifier. + +Spring Cloud Contract Verifier moves TDD to the level of software architecture. + +=== Why? + +Let us assume that we have a system comprising of multiple microservices: + +image::{images_url}/Deps.png[Microservices Architecture] + +==== Testing issues + +If we wanted to test the application in top left corner if it can communicate with other services then we could do one of two things: + +- deploy all microservices and perform end to end tests +- mock other microservices in unit / integration tests + +Both have their advantages but also a lot of disadvantages. Let's focus on the latter. + +*Deploy all microservices and perform end to end tests* + +Advantages: + +- simulates production +- tests real communication between services + +Disadvantages: + +- to test one microservice we would have to deploy 6 microservices, a couple of databases etc. +- the environment where the tests would be conducted would be locked for a single suite of tests (i.e. nobody else would be able to run the tests in the meantime). +- long to run +- very late feedback +- extremely hard to debug + +*Mock other microservices in unit / integration tests* + +Advantages: + +- very fast feedback +- no infrastructure requirements + +Disadvantages: + +- the implementor of the service creates stubs thus they might have nothing to do with the reality +- you can go to production with passing tests and failing production + +To solve the aforementioned issues Spring Cloud Contract Verifier with Stub Runner were created. Their main idea is to give you very fast feedback, without the need +to set up the whole world of microservices. + +image::{images_url}/Stubs1.png[Stubbed Services] + +If you work on stubs then the only applications you need are those that your application is using directly. + +image::{images_url}/Stubs2.png[Stubbed Services] + +Spring Cloud Contract Verifier gives you the certainty that the stubs that you're using were created by the service that you're calling. Also if you can use them it means that they were +tested against the producer's side. In other words - you can trust those stubs. + + +=== Purposes + +The main purposes of Spring Cloud Contract Verifier with Stub Runner are: + + - to ensure that WireMock / Messaging stubs (used when developing the client) are doing exactly what actual server-side implementation will do, + - to promote ATDD method and Microservices architectural style, + - to provide a way to publish changes in contracts that are immediately visible on both sides, + - to generate boilerplate test code used on the server side. + +=== Client Side + +During the tests you want to have a Wiremock instance / Messaging route up and running that simulates the service Y. +You would like to feed that instance with a proper stub definition. That stub definition would need +to be valid and should also be reusable on the server side. + +__Summing it up:__ On this side, in the stub definition, you can use patterns for request stubbing and you need exact +values for responses. + +=== Server Side + +Being a service Y since you are developing your stub, you need to be sure that it's actually resembling your +concrete implementation. You can't have a situation where your stub acts in one way and your application on +production behaves in a different way. + +That's why from the provided stub acceptance tests will be generated that will ensure +that your application behaves in the same way as you define in your stub. + +__Summing it up:__ On this side, in the stub definition, you need exact values as request and can use patterns/methods +for response verification. + +=== Dependencies + +Spring Cloud Contract Verifier and Stub Runner are using the following libraries + +- http://wiremock.org/[WireMock] +- https://github.com/jayway/JsonPath[Jayway JSONPath] +- https://github.com/marcingrzejszczak/jsonassert[JSONAssert from Marcin Grzejszczak] + +=== Additional links + +Below you can find some resources related to Spring Cloud Contract Verifier and Stub Runner. Note that some can be outdated since the Spring Cloud Contract Verifier project +is under constant development. + +==== Videos + +*Olga Maciaszek-Sharma talking about Accurest (Spring Cloud Contract Verifier predecessor)* + +video::daafmTYFoDU[youtube] + +*Marcin Grzejszczak and Jakub Kubryński talking about Accurest (Spring Cloud Contract Verifier predecessor)* + +video::130779882[vimeo] + +==== Readings + +- http://www.slideshare.net/MarcinGrzejszczak/stick-to-the-rules-consumer-driven-contracts-201507-confitura[Slides from Marcin Grzejszczak's talk about Accurest] +- http://toomuchcoding.com/blog/categories/accurest/[Accurest related articles from Marcin Grzejszczak's blog] + +=== Samples + +Here you can find some https://github.com/Codearte/accurest-samples[working samples]. Check the readme of each project for more information. + +=== Why use Spring Cloud Contract Verifier and not X ? + +For the time being Spring Cloud Contract Verifier is a JVM based tool. So it could be your first pick when you're already creating +software for the JVM. This project has a lot of really interesting features but especially quite a few of them definitely make +Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Contract (CDC) tooling. Out of many the most interesting are: + +- Possibility to do CDC with messaging +- Clear and easy to use, statically typed DSL +- Possibility to copy paste your current JSON file to the contract and only edit its elements +- Automatic generation of tests from the defined Contract +- Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory +- Spring Cloud integration - no discovery service is needed for integration tests \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/links.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/links.adoc new file mode 100644 index 0000000000..e8e1df762b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/links.adoc @@ -0,0 +1,11 @@ +== Links + +Here you can find interesting links related to Spring Cloud Contract Verifier: + +- https://github.com/Codearte/accurest[Spring Cloud Contract Verifier Github Repository] +- http://codearte.github.io/accurest[Spring Cloud Contract Verifier Documentation] +- http://codearte.github.io/accurest/deprecated[Accurest Legacy Documentation] +- https://codearte.github.io/accurest/#spring-cloud-contract-stub-runner[Spring Cloud Contract Stub Runner Documentation] +- http://codearte.github.io/accurest/#stub-runner-for-messaging[Spring Cloud Contract Stub Runner Messaging Documentation] +- https://gitter.im/Codearte/accurest[Spring Cloud Contract Verifier Gitter] +- https://github.com/Codearte/accurest-maven-plugin[Spring Cloud Contract Verifier Maven Plugin] diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/messaging.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/messaging.adoc new file mode 100644 index 0000000000..24bc2bc272 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/messaging.adoc @@ -0,0 +1,150 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core + +== Spring Cloud Contract Verifier Messaging + +Spring Cloud Contract Verifier allows you to verify your application that uses messaging as means of communication. +All of our integrations are working with Spring but you can also set one yourself. + +=== Integrations + +You can use one of the three integration configurations: + +- Apache Camel +- Spring Integration +- Spring Cloud Stream + +If you're using Spring Boot, the aforementioned test configurations will be appended automatically. + +You have to provide as a dependency one of the Spring Cloud Contract Verifier Messaging modules. Example for Gradle: + +[source,groovy,indent=0] +---- +// for Apache Camel +testCompile "org.springframework.cloud.contract:spring-cloud-contract-verifier-camel:${verifierVersion}" +// for Spring Integration +testCompile "org.springframework.cloud.contract:spring-cloud-contract-verifier-integration:${verifierVersion}" +// for Spring Cloud Stream +testCompile "org.springframework.cloud.contract:spring-cloud-contract-verifier-stream:${verifierVersion}" +---- + +=== Manual Integration + +The `spring-cloud-contract-verifier-messaging-core` module contains 3 main interfaces: + +- `ContractVerifierMessage` - describes a message received / sent to a channel / queue / topic etc. +- `ContractVerifierMessageBuilder` - describes how to build a message +- `ContractVerifierMessaging` - class that allows you to build, send and receive messages +- `ContractVerifierFilter` - interface to filter out the messages that do not follow the pattern from the DSL + +In the generated test the `ContractVerifierMessaging` is injected via `@Inject` annotation thus you can use other injection +frameworks than Spring. + +You have to provide as a dependency the `spring-cloud-contract-verifier-messaging-core` module. Example for Gradle: + +[source,groovy,indent=0] +---- +testCompile "org.springframework.cloud.contract:spring-cloud-contract-verifier-messaging-core:${verifierVersion}" +---- + +=== Publisher side test generation + +Having the `input` or `outputMessage` sections in your DSL will result in creation of tests on the publisher's side. By default +JUnit tests will be created, however there is also a possibility to create Spock tests. + +There are 3 main scenarios that we should take into consideration: + +- Scenario 1: there is no input message that produces an output one. The output message is triggered by a component +inside the application (e.g. scheduler) +- Scenario 2: the input message triggers an output message +- Scenario 3: the input message is consumed and there is no output message + +==== Scenario 1 (no input message) + +For the given contract: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_dsl] +---- + +The following JUnit test will be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_junit_test] +---- + +And the following Spock test would be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_test] +---- + +==== Scenario 2 (output triggered by input) + +For the given contract: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_dsl] +---- + +The following JUnit test will be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_junit] +---- + +And the following Spock test would be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_spock] +---- + +==== Scenario 3 (no output message) + +For the given contract: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl] +---- + +The following JUnit test will be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_junit] +---- + +And the following Spock test would be created: + +[source,groovy] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_spock] +---- + +=== Consumer Stub Side generation + +Unlike the HTTP part - in Messaging we need to publish the Groovy DSL inside the JAR with a stub. Then it's parsed on the consumer side +and proper stubbed routes are created. + +For more infromation please consult the Stub Runner Messaging sections. + +==== Gradle Setup + +Example of Spring Cloud Contract Verifier Gradle setup: + +[source,groovy,indent=0] +---- +include::{verifier_root_path}/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle[tags=jar_setup,indent=0] +---- + +==== Maven Setup + +Example of Maven can be found in the https://github.com/Codearte/accurest-maven-plugin/=publishing-wiremock-stubs-projectf-stubsjar[Spring Cloud Contract Verifier Maven Plugin README] diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/rest.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/rest.adoc new file mode 100644 index 0000000000..843d396ec6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/rest.adoc @@ -0,0 +1,437 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core + +== Spring Cloud Contract Verifier HTTP + +=== Gradle Project + +==== Prerequisites + +In order to use Spring Cloud Contract Verifier with Wiremock you have to use gradle or maven plugin. + +===== Add gradle plugin + +[source,groovy,indent=0] +---- +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.cloud.contract:spring-cloud-contract-verifier-gradle-plugin:${verifier_version}' + } +} + +apply plugin: 'groovy' +apply plugin: 'contract-verifier' + +dependencies { + testCompile 'org.codehaus.groovy:groovy-all:2.4.6' + testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' + testCompile 'com.jayway.restassured:spring-mock-mvc:2.9.0' // needed if you're going to use Spring MockMvc +} +---- + +====== Snapshot versions + +Add the additional snapshot repository to your build.gradle to use snapshot versions which are automatically uploaded after every successful build: + +[source,groovy,indent=0] +---- +repositories { + (...) + //Required only for SNAPSHOT versions + maven { url "https://repo.spring.io/plugins-snapshot-local" } +} +---- + +===== Add maven plugin + +[source,xml,indent=0] +---- + + org.springframework.cloud.contract + spring-cloud-contract-verifier-maven-plugin + + + + convert + generateStubs + generateTests + + + + +---- + + +Read more: http://codearte.github.io/accurest-maven-plugin/[spring-cloud-contract-verifier-maven-plugin] + +===== Add stubs + +By default Spring Cloud Contract Verifier is looking for stubs in `src/test/resources/contracts` directory. + +Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure + +[source,groovy,indent=0] +---- +src/test/resources/contracts/myservice/shouldCreateUser.groovy +src/test/resources/contracts/myservice/shouldReturnUser.groovy +---- + +Spring Cloud Contract Verifier will create test class `defaultBasePackage.MyService` with two methods + + - `shouldCreateUser()` + - `shouldReturnUser()` + +==== Run plugin + +Plugin registers itself to be invoked before `check` task. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke `generateContractTests` task. + +==== Default setup + +Default Gradle Plugin setup creates the following Gradle part of the build (it's a pseudocode) + +[source,groovy,indent=0] +---- +contractVerifier { + targetFramework = 'JUNIT' + testMode = 'MockMvc' + generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts") + contractsDslDir = "${project.rootDir}/src/test/resources/contracts" + basePackageForTests = 'org.springframework.cloud.contract.verifier.tests' + stubsOutputDir = project.file("${project.buildDir}/stubs") +} + +tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateWireMockClientStubs') { + baseName = project.name + classifier = contractVerifier.stubsSuffix + from contractVerifier.stubsOutputDir +} + +project.artifacts { + archives task +} + +tasks.create(type: Copy, name: 'copyContracts') { + from contractVerifier.contractsDslDir + into contractVerifier.stubsOutputDir +} + +verifierStubsJar.dependsOn 'copyContracts' + +publishing { + publications { + stubs(MavenPublication) { + artifactId project.name + artifact verifierStubsJar + } + } +} +---- + +==== Configure plugin + +To change default configuration just add `contractVerifier` snippet to your Gradle config + +[source,groovy,indent=0] +---- +contractVerifier { + testMode = 'MockMvc' + baseClassForTests = 'org.mycompany.tests' + generatedTestSourcesDir = project.file('src/generatedContract') +} +---- + +===== Configuration options + + - **testMode** - defines mode for acceptance tests. By default MockMvc which is based on Spring's MockMvc. It can also be changed to **JaxRsClient** or to **Explicit** for real HTTP calls. + - **imports** - array with imports that should be included in generated tests (for example ['org.myorg.Matchers']). By default empty array [] + - **staticImports** - array with static imports that should be included in generated tests(for example ['org.myorg.Matchers.*']). By default empty array [] + - **basePackageForTests** - specifies base package for all generated tests. By default set to org.springframework.cloud.contract.verifier.tests + - **baseClassForTests** - base class for generated tests. By default `spock.lang.Specification` if using Spock tests. + - **ruleClassForTests** - specifies Rule which should be added to generated test classes. + - **ignoredFiles** - Ant matcher allowing defining stub files for which processing should be skipped. By default empty array [] + - **contractsDslDir** - directory containing contracts written using the GroovyDSL. By default `$rootDir/src/test/resources/contracts` + - **generatedTestSourcesDir** - test source directory where tests generated from Groovy DSL should be placed. By default `$buildDir/generated-test-sources/contractVerifier` + - **stubsOutputDir** - dir where the generated WireMock stubs from Groovy DSL should be placed + - **targetFramework** - the target test framework to be used; currently Spock and JUnit are supported with JUnit being the default framework + +===== Base class for tests + +When using Spring Cloud Contract Verifier in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified. + +[source,groovy,indent=0] +---- +include::{verifier_root_path}/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy[tags=base_class,indent=0] +---- + +In case of using `Explicit` mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of `JAXRSCLIENT` mode this base class +should also contain `protected WebTarget webTarget` field, right now the only option to test JAX-RS API is to start a web server. + +==== Invoking generated tests + +To ensure that provider side is complaint with defined contracts, you need to invoke: + +[source,bash,indent=0] +---- +./gradlew generateContractTests test +---- + +==== Spring Cloud Contract Verifier on consumer side + +In consumer service you need to configure Spring Cloud Contract Verifier plugin in exactly the same way as in case of provider. If you don't want to use Stub Runner then you need to copy contracts stored in +`src/test/resources/contracts` and generate WireMock json stubs using: + +[source,bash,indent=0] +---- +./gradlew generateWireMockClientStubs +---- + +Note that `stubsOutputDir` option has to be set for stub generation to work. + +When present, json stubs can be used in consumer automated tests. + +[source,groovy,indent=0] +---- +@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application) +class LoanApplicationServiceSpec extends Specification { + + @ClassRule + @Shared + WireMockClassRule wireMockRule == new WireMockClassRule() + + @Autowired + LoanApplicationService sut + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123) + when: + LoanApplicationResult loanApplication == sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason === null + } +} +---- + +Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Spring Cloud Contract Verifier. + +=== Using in your Maven project + +==== Add maven plugin + +[source,xml,indent=0] +---- + + org.springframework.cloud.contract.verifier + spring-cloud-contract-verifier-maven-plugin + + + + convert + generateStubs + generateTests + + + + +---- + +Read more: http://codearte.github.io/accurest-maven-plugin/[spring-cloud-contract-verifier-maven-plugin] + +==== Add stubs + +By default Spring Cloud Contract Verifier is looking for stubs in `src/test/resources/contracts` directory. +Directory containing stub definitions is treated as a class name, and each stub definition is treated as a single test. +We assume that it contains at least one directory which will be used as test class name. If there is more than one level of nested directories all except the last one will be used as package name. +So with following structure + +[source,groovy,indent=0] +---- +src/test/resources/contracts/myservice/shouldCreateUser.groovy +src/test/resources/contracts/myservice/shouldReturnUser.groovy +---- + +Spring Cloud Contract Verifier will create test class `defaultBasePackage.MyService` with two methods + - `shouldCreateUser()` + - `shouldReturnUser()` + +==== Run plugin + +Plugin goal `generateTests` is assigned to be invoked in phase `generate-test-sources`. You have nothing to do as long as you want it to be part of your build process. If you just want to generate tests please invoke `generateTests` goal. + +==== Configure plugin + +To change default configuration just add `configuration` section to plugin definition or `execution` definition. + +[source,xml,indent=0] +---- + + org.springframework.cloud.contract.verifier + spring-cloud-contract-verifier-maven-plugin + + + + convert + generateStubs + generateTests + + + + + org.springframework.cloud.contract.verifier.twitter.place + org.springframework.cloud.contract.verifier.twitter.place.BaseMockMvcSpec + + +---- + +===== Important configuration options + + - **testMode** - defines mode for acceptance tests. By default `MockMvc` which is based on Spring's MockMvc. It can also be changed to `JaxRsClient` or to `Explicit` for real HTTP calls. + - **basePackageForTests** - specifies base package for all generated tests. By default set to `org.springframework.cloud.contract.verifier.tests`. + - **ruleClassForTests** - specifies Rule which should be added to generated test classes. + - **baseClassForTests** - base class for generated tests. By default `spock.lang.Specification` if using Spock tests. + - **contractsDir** - directory containing contracts written using the GroovyDSL. By default `/src/test/resources/contracts`. + - **testFramework** - the target test framework to be used; currently Spock and JUnit are supported with Spock being the default framework + +For complete information take a look at http://codearte.github.io/accurest-maven-plugin/plugin-info.html[Plugin Documentation] + +===== Base class for tests + + When using Spring Cloud Contract Verifier in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified. + +[source,groovy,indent=0] +---- +package org.mycompany.tests + +import org.mycompany.ExampleSpringController +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import spock.lang.Specification + +class MvcSpec extends Specification { + def setup() { + RestAssuredMockMvc.standaloneSetup(new ExampleSpringController()) + } +} +---- + +In case of using `Explicit` mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of `JAXRSCLIENT` mode this base class should also contain `protected WebTarget webTarget` field, right now the only option to test JAX-RS API is to start a web server. + +==== Invoking generated tests + +Spring Cloud Contract Verifier Maven Plugins generates verification code into directory `/generated-test-sources/contractVerifier` and attach this directory to `testCompile` goal. + +For Groovy Spock code use: + +[source,xml,indent=0] +---- + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + + testCompile + + + + + + + ${project.basedir}/src/test/groovy + + **/*.groovy + + + + ${project.build.directory}/generated-test-sources/contractVerifier + + **/*.groovy + + + + + +---- + +To ensure that provider side is complaint with defined contracts, you need to invoke `mvn generateTest test` + +==== Spring Cloud Contract Verifier on consumer side + +In consumer service you need to configure Spring Cloud Contract Verifier plugin in exactly the same way as in case of provider. You need to copy contracts stored in `src/test/resources/contracts` and generate Wiremock json stubs using: `mvn generateStubs` command. By default generated WireMock mapping is stored in directory `target/mappings`. Your project should create from this generated mappings additional artifact with classifier `stubs` for easy deploy to maven repository. + +Sample configuration: + +[source,xml,indent=0] +---- + + org.springframework.cloud.contract.verifier + spring-cloud-contract-verifier-maven-plugin + ${verifier-plugin.version} + + + + convert + generateStubs + + + + +---- + +When present, json stubs can be used in consumer automated tests. + +[source,groovy,indent=0] +---- +@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application) +class LoanApplicationServiceSpec extends Specification { + + @ClassRule + @Shared + WireMockClassRule wireMockRule == new WireMockClassRule() + + @Autowired + LoanApplicationService sut + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '12345678901'), amount: 123.123) + when: + LoanApplicationResult loanApplication == sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus === LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason === null + } +} +---- + +Underneath LoanApplication makes a call to FraudDetection service. This request is handled by Wiremock server configured using stubs generated by Spring Cloud Contract Verifier. + +=== Scenarios + +It's possible to handle scenarios with Spring Cloud Contract Verifier. All you need to do is to stick to proper naming convention while creating your contracts. The convention requires to include order number followed by the underscore. + +[source,indent=0] +---- +my_contracts_dir\ + scenario1\ + 1_login.groovy + 2_showCart.groovy + 3_logout.groovy +---- + +Such tree will cause Spring Cloud Contract Verifier generating Wiremock's scenario with name `scenario1` and three steps: + - login marked as `Started` pointing to: + - showCart marked as `Step1` pointing to: + - logout marked as `Step2` which will close the scenario. +More details about Wiremock scenarios can be found under [http://wiremock.org/stateful-behaviour.html](http://wiremock.org/stateful-behaviour.html) + +Spring Cloud Contract Verifier will also generate tests with guaranteed order of execution. diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc new file mode 100644 index 0000000000..96cc64d927 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc @@ -0,0 +1,22 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core +:stubrunner_core_path: {core_path}/spring-cloud-contract-stub-runner + += Spring Cloud Contract Verifier + +_Documentation Authors: Adam Dudczak, Marcin Grzejszczak, Jakub Kubryński, Karol Lassak, Olga Maciaszek-Sharma, Mariusz Smykuła_ + +include::introduction.adoc[] + +include::contract.adoc[] + +include::rest.adoc[] + +include::messaging.adoc[] + +include::stubrunner.adoc[] + +include::stubrunner_msg.adoc[] + +include::links.adoc[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner.adoc new file mode 100644 index 0000000000..6ba4fb98f9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner.adoc @@ -0,0 +1,97 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core + +== Spring Cloud Contract Stub Runner + +One of the issues that you could have encountered while using Spring Cloud Contract Verifier was to pass the generated WireMock JSON stubs from the server side to the client side (or various clients). + The same takes place in terms of client side generation for messaging. + +Copying the JSON files / setting the client side for messaging manually is out of the question. + +=== Snapshot versions + +Add the additional snapshot repository to your build.gradle to use snapshot versions which are automatically uploaded after every successful build: + +[source,groovy,indent=0] +---- +repositories { + (...) + //Required only for SNAPSHOT versions + maven { url "https://repo.spring.io/libs-snapshot-local" } +} +---- + +=== Publishing stubs as JARs + +The easiest approach would be to centralize the way stubs are kept. For example you can keep them as JARs in a Maven repository. + +==== Gradle + +Example of Spring Cloud Contract Verifier Gradle setup: + +[source,groovy,indent=0] +---- +include::{verifier_root_path}/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle[tags=jar_setup,indent=0] +---- + +==== Maven + +Example of Maven can be found in the https://github.com/Codearte/accurest-maven-plugin/=publishing-wiremock-stubs-projectf-stubsjar[Spring Cloud Contract Verifier Maven Plugin README] + +=== Modules + +Spring Cloud Contract Stub Runner comes with a new structure of modules + +[source,indent=0] +---- +└── spring-cloud-contract-stub-runner + ├── spring-cloud-contract-stub-runner + ├── spring-cloud-contract-stub-runner-boot + ├── spring-cloud-contract-stub-runner-junit + ├── spring-cloud-contract-stub-runner-spring + └── spring-cloud-contract-stub-runner-spring-cloud +---- + +include::../../../../spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/README.adoc[] + +include::../../../../spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/README.adoc[] + +include::../../../../spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/README.adoc[] + +include::../../../../spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/README.adoc[] + +include::../../../../spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/README.adoc[] + + +=== Common properties for JUnit and Spring + +Some of the properties that are repetitive can be set using system properties or property sources (for Spring). Here are their names with their default values: + +[frame="topbot",options="header"] +|====================== +| Property name | Default value | Description +|stubrunner.port.range.min|10000| Minimal value of a port for a started WireMock with stubs +|stubrunner.port.range.max|15000| Minimal value of a port for a started WireMock with stubs +|stubrunner.stubs.repository.root|| Comma separated list of Maven repo urls. If blank then will call the local maven repo +|stubrunner.stubs.classifier|stubs| Default classifier for the stub artifacts +|stubrunner.work-offline|false| If true then will not contact any remote repositories to download stubs +|stubrunner.stubs.ids|| Comma separated list of Ivy notation of stubs to download +|====================== + +==== Stub runner stubs ids + +You can provide the stubs to download via the `stubrunner.stubs.ids` system property. They follow the following pattern: + +[source,java,indent=0] +---- +groupId:artifactId:version:classifier:port +---- + +`version`, `classifier` and `port` are optional. + +* If you don't provide the `port` then a random one will be picked +* If you don't provide the `classifier` then the default one will be taken. +* If you don't provide the `version` then the `+` will be passed and the latest one will be downloaded + +Where `port` means the port of the WireMock server. \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner_msg.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner_msg.adoc new file mode 100644 index 0000000000..194076d611 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/asciidoc/stubrunner_msg.adoc @@ -0,0 +1,61 @@ +:core_path: ../../../.. +:verifier_root_path: {core_path}/spring-cloud-contract-verifier +:verifier_core_path: {verifier_root_path}/spring-cloud-contract-verifier-core +:stubrunner_core_path: {core_path}/spring-cloud-contract-stub-runner + +== Stub Runner for Messaging + +Stub Runner has the functionality to run the published stubs in memory. It can integrate with the following frameworks out of the box + +- Spring Integration +- Spring Cloud Stream +- Apache Camel + +It also provides points of entry to integrate with any other solution on the market. + +=== Stub triggering + +To trigger a message it's enough to use the `StubTrigger` interface: + +[source,groovy] +---- +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubTrigger.groovy[] +---- + +For convenience the `StubFinder` interface extends `StubTrigger` so it's enough to use only one in your tests. + +`StubTrigger` gives you the following options to trigger a message: + +==== Trigger by label + +[source,groovy] +---- +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +===== Trigger by group and artifact ids + +[source,groovy] +---- +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_group_artifact,indent=0] +---- + +===== Trigger by artifact ids + +[source,groovy] +---- +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_artifact,indent=0] +---- + +===== Trigger all messages + +[source,groovy] +---- +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_all,indent=0] +---- + +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/README.adoc[] + +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/README.adoc[] + +include::{stubrunner_core_path}/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/README.adoc[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/ruby/generate_readme.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/ruby/generate_readme.sh new file mode 100755 index 0000000000..6d0ce9dc54 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/docs/src/main/ruby/generate_readme.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby + +base_dir = File.join(File.dirname(__FILE__),'../../..') +src_dir = File.join(base_dir, "/src/main/asciidoc") +require 'asciidoctor' +require 'optparse' + +options = {} +file = "#{src_dir}/README.adoc" + +OptionParser.new do |o| + o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' } + o.on('-h', '--help') { puts o; exit } + o.parse! +end + +file = ARGV[0] if ARGV.length>0 + +# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb +doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes] +header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a +header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k } +attrs = doc.attributes +attrs['allow-uri-read'] = true +puts attrs + +out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n" +doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs +out << doc.reader.read + +unless options[:to_file] + puts out +else + File.open(options[:to_file],'w+') do |file| + file.write(out) + end +end diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/generateReadme.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/generateReadme.sh new file mode 100755 index 0000000000..1638d7b172 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/generateReadme.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew :docs:clean :docs:generateReadme --configure-on-demand \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle.properties new file mode 100644 index 0000000000..91057965e2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle.properties @@ -0,0 +1,33 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +nexusUsername = +nexusPassword = + +wiremockVersion = 2.0.10-beta +jsonassertVersion = 0.4.7 + +BOM_VERSION=Brixton-SR1 +springBootVersion=1.3.3.RELEASE +springVersion=4.2.5.RELEASE +springIntegrationDslVersion=1.1.2.RELEASE +springStreamVersion=1.0.0.RELEASE +springCloudVersion=1.1.0.RELEASE +springCloudZookeeperVersion=1.0.0.RELEASE +camelVersion=2.17.0 +aetherVersion=1.1.0 + +verifierVersion=1.0.0.BUILD-SNAPSHOT \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/release.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/release.gradle new file mode 100644 index 0000000000..ae90a8c9d7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/release.gradle @@ -0,0 +1,92 @@ +apply plugin: 'maven' +apply plugin: 'signing' + +ext { + resolveRepoName = { Project project -> + String version = project.version + String name = project.name + String resolvedRepoName = "libs-${resolveVersion(version)}-local" + if (name.contains("gradle")) { + resolvedRepoName = "plugins-${resolveVersion(version)}-local" + } + logger.lifecycle("For project [$project.name] with " + + "version [$project.version] the resolved Artifactory repo is [$resolvedRepoName]") + return resolvedRepoName + } + + resolveVersion = { String version -> + if (version.endsWith('BUILD-SNAPSHOT')) return 'snapshot' + if (version.matches('[0-9].[0-9].[0-9].M[0-9]+')) return 'milestone' + if (version.matches('[0-9].[0-9].[0-9].RC[0-9]+')) return 'milestone' + if (version.endsWith('RELEASE')) return 'release' + return 'snapshot' + } + + isReleaseVersion = !version.endsWith("SNAPSHOT") +} + +uploadArchives.dependsOn { [check] } + +ext { + repositoryUrl = "https://repo.spring.io/${repoPrefix()}-${resolveVersion(version)}-local" + snapshotRepositoryUrl = "https://repo.spring.io/${repoPrefix()}-snapshot-local" + repoUrl = isReleaseVersion ? repositoryUrl : snapshotRepositoryUrl +} + +afterEvaluate { + uploadArchives { + repositories { + mavenDeployer { + // POM signature + if (isReleaseVersion) { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + } + // Target repository + repository(url: repoUrl) { + authentication(userName: repoUser, password: repoPass) + } + pom.project { + name "$project.name" + packaging 'jar' + description 'Consumer Driven Contract Verifier and Stub Runner' + url 'https://github.com/Codearte/accurest' + inceptionYear '2014' + + scm { + connection 'scm:git:git@github.com:Codearte/accurest.git' + developerConnection 'scm:git:git@github.com:Codearte/accurest.git' + url 'https://github.com/Codearte/accurest' + } + + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + + developers { + developer { + id 'jkubrynski' + name 'Jakub Kubrynski' + email 'jk ATT codearte DOTT io' + } + developer { + id 'marcingrzejszczak' + name 'Marcin Grzejszczak' + email 'marcin ATT grzejszczak DOTT pl' + } + } + } + } + } + } +} + +String repoPrefix() { + return project.name.contains('gradle') ? 'plugins' : 'libs' +} + +if (projectsToSkipPublication.any { project.name.contains(it) }) { + uploadArchives.enabled = false +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..72e5e30b39 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Jun 15 17:19:08 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew new file mode 100755 index 0000000000..9d82f78915 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew.bat new file mode 100644 index 0000000000..5f192121eb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/publishDocs.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/publishDocs.sh new file mode 100755 index 0000000000..17ed699b55 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/publishDocs.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew :docs:clean :docs:publishGhPages --configure-on-demand \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/build.gradle new file mode 100644 index 0000000000..bf8efc7f10 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/build.gradle @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-core") + compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + compile "org.apache.camel:camel-spring-boot-starter:${camelVersion}" + compile "org.apache.camel:camel-jms:${camelVersion}" + compile 'org.apache.activemq:activemq-camel:5.12.1' + compile 'org.apache.activemq:activemq-pool:5.12.1' + compile "org.apache.camel:camel-jackson:${camelVersion}" + + testCompile project(":$verifier-root:$verifier-messaging-root:$verifier-camel") + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleted.groovy new file mode 100644 index 0000000000..52af5fd871 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleted.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookDeleted implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookDeleted(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleter.groovy new file mode 100644 index 0000000000..c1ad517de9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookDeleter.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.camel.Exchange +import org.springframework.stereotype.Component + +import java.util.concurrent.atomic.AtomicBoolean + +@CompileStatic +@Slf4j +@Component +class BookDeleter { + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + void bookDeleted(Exchange exchange) { + BookDeleted bookDeleted = exchange.in.getBody(BookDeleted) + log.info("Deleting book [$bookDeleted]") + bookSuccessfulyDeleted.set(true) + log.info("Book successfuly deleted [$bookSuccessfulyDeleted]") + } + + AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false) +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookReturned.groovy new file mode 100644 index 0000000000..373743a094 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookRouteConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookRouteConfiguration.groovy new file mode 100644 index 0000000000..06ed844919 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookRouteConfiguration.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import org.apache.activemq.camel.component.ActiveMQComponent +import org.apache.camel.RoutesBuilder +import org.apache.camel.model.dataformat.JsonLibrary +import org.apache.camel.spring.SpringRouteBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +/** + * @author Marcin Grzejszczak + */ +@Configuration +class BookRouteConfiguration { + + @Bean + ActiveMQComponent activeMQComponent(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) { + return new ActiveMQComponent(brokerURL: url) + } + + @Bean + RoutesBuilder myRouter(BookService bookService, BookDeleter bookDeleter) { + return new SpringRouteBuilder() { + + @Override + public void configure() throws Exception { + // scenario 1 - from bean to output + from("direct:start").unmarshal().json(JsonLibrary.Jackson, BookReturned).bean(bookService).to("jms:output") + // scenario 2 - from input to output + from("jms:input").unmarshal().json(JsonLibrary.Jackson, BookReturned).bean(bookService).to("jms:output") + // scenario 3 - from input to no output + from("jms:delete").unmarshal().json(JsonLibrary.Jackson, BookDeleted).bean(bookDeleter) + } + + }; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookService.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookService.groovy new file mode 100644 index 0000000000..f60ec6fbe9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/BookService.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.camel.Exchange +import org.springframework.stereotype.Service + +@Service +@CompileStatic +@Slf4j +class BookService { + + /** + Scenario for "should generate tests triggered by a method": + client side: must have a possibility to "trigger" sending of a message to the given messageFrom + server side: will run the method and await upon receiving message on the output messageFrom + + Method triggers sending a message to a source + */ + void returnBook(Exchange exchange) { + BookReturned bookReturned = exchange.in.getBody(BookReturned) + log.info("Returning book [$bookReturned]") + exchange.out.with { + body = bookReturned + setHeader('BOOK-NAME', bookReturned.bookName) + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplication.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplication.groovy new file mode 100644 index 0000000000..da6bddcb83 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/main/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplication.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class CamelMessagingApplication { + + static void main(String[] args) { + SpringApplication.run(CamelMessagingApplication.class, args) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/test/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplicationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/test/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..09f5a23df7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-camel/src/test/groovy/org/springframework/cloud/contract/verifier/samples/camel/CamelMessagingApplicationSpec.groovy @@ -0,0 +1,160 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.camel + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.apache.camel.model.ModelCamelContext +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import javax.inject.Inject +/** + * SPIKE ON TESTS FROM NOTES IN MessagingSpec + */ +// Context configuration would end up in base class +@ContextConfiguration(classes = [CamelMessagingApplication], loader = SpringApplicationContextLoader) +public class CamelMessagingApplicationSpec extends Specification { + + // ALL CASES + @Inject ContractVerifierMessaging contractVerifierMessaging + ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper() + + def "should work for triggered based messaging"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('activemq:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // generated test should look like this: + when: + bookReturnedTriggered() + then: + def response = contractVerifierMessaging.receiveMessage('activemq:output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + header('Content-Type', 'application/json') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'jms:input') + then: + def response = contractVerifierMessaging.receiveMessage('jms:output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests without destination, triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'jms:delete') + then: + noExceptionThrown() + bookWasDeleted() + } + + // BASE CLASS WOULD HAVE THIS: + + @Autowired ModelCamelContext camelContext + @Autowired BookDeleter bookDeleter + + void bookReturnedTriggered() { + camelContext.createProducerTemplate().sendBody('direct:start', '''{"bookName" : "foo" }''') + } + + PollingConditions pollingConditions = new PollingConditions() + + void bookWasDeleted() { + pollingConditions.eventually { + assert bookDeleter.bookSuccessfulyDeleted.get() + } + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/build.gradle new file mode 100644 index 0000000000..21eeaff00b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/build.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-core") + compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + compile "org.springframework.boot:spring-boot-starter-integration:${springBootVersion}" + + testCompile project(":$verifier-root:$verifier-messaging-root:$verifier-integration") + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy new file mode 100644 index 0000000000..e43c21c9d2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookDeleted { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookDeleted(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy new file mode 100644 index 0000000000..e0041a8dcb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder + +import java.util.concurrent.atomic.AtomicBoolean + +@CompileStatic +@Slf4j +class BookListener { + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + Message returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + return MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ])) + } + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + void bookDeleted(BookDeleted bookDeleted) { + log.info("Deleting book [$bookDeleted]") + bookSuccessfulyDeleted.set(true) + } + + AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false) +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy new file mode 100644 index 0000000000..130c19d468 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy new file mode 100644 index 0000000000..888df79bce --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Service + +@Service +@CompileStatic +@Slf4j +class BookService { + + @Autowired @Qualifier("outputChannel") MessageChannel outputChannel + + /** + Scenario for "should generate tests triggered by a method": + client side: must have a possibility to "trigger" sending of a message to the given messageFrom + server side: will run the method and await upon receiving message on the output messageFrom + + Method triggers sending a message to a source + */ + void returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + outputChannel.send(MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ]))) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplication.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplication.groovy new file mode 100644 index 0000000000..811fcbdfc3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplication.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ImportResource + +@SpringBootApplication +@ImportResource("classpath*:integration-context.xml") +class IntegrationMessagingApplication { + + static void main(String[] args) { + SpringApplication.run(IntegrationMessagingApplication.class, args) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/resources/integration-context.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/resources/integration-context.xml new file mode 100644 index 0000000000..3dd916e22f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/main/resources/integration-context.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..68eaf5009b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-integration/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/IntegrationMessagingApplicationSpec.groovy @@ -0,0 +1,168 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import javax.inject.Inject +// Context configuration would end up in base class +@ContextConfiguration(classes = [IntegrationMessagingApplication], loader = SpringApplicationContextLoader) +public class IntegrationMessagingApplicationSpec extends Specification { + + // ALL CASES + @Inject ContractVerifierMessaging contractVerifierMessaging + ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper() + + def "should work for triggered based messaging"() { + given: + // tag::method_trigger[] + def dsl = Contract.make { + // Human readable description + description 'Some description' + // Label by means of which the output message can be triggered + label 'some_label' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('bookReturnedTriggered()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('output') + // the body of the output message + body('''{ "bookName" : "foo" }''') + // the headers of the output message + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::method_trigger[] + // generated test should look like this: + when: + bookReturnedTriggered() + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests triggered by a message"() { + given: + // tag::message_trigger[] + def dsl = Contract.make { + description 'Some Description' + label 'some_label' + // input is a message + input { + // the message was received from this destination + messageFrom('input') + // has the following body + messageBody([ + bookName: 'foo' + ]) + // and the following headers + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::message_trigger[] + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'input') + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests without destination, triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'delete') + then: + noExceptionThrown() + bookWasDeleted() + } + + // BASE CLASS WOULD HAVE THIS: + + @Autowired BookService bookService + @Autowired BookListener bookListener + + void bookReturnedTriggered() { + bookService.returnBook(new BookReturned("foo")) + } + + void bookWasDeleted() { + assert bookListener.bookSuccessfulyDeleted.get() + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/build.gradle new file mode 100644 index 0000000000..dcc70f5e6e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/build.gradle @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +repositories { + mavenLocal() + jcenter() + maven { + url "http://repo.spring.io/snapshot" + } + maven { + url "http://repo.spring.io/milestone" + } +} + +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-core") + compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + compile "org.springframework:spring-jms:${springVersion}" + + compile 'org.apache.activemq:activemq-broker:5.12.1' + compile 'org.apache.activemq:activemq-pool:5.12.1' + + testCompile project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookDeleted.groovy new file mode 100644 index 0000000000..3ee03c12ec --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookDeleted.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookDeleted implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookDeleted(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookListener.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookListener.groovy new file mode 100644 index 0000000000..117bf6a9b6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookListener.groovy @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import com.fasterxml.jackson.databind.ObjectMapper +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jms.annotation.JmsListener +import org.springframework.jms.core.JmsTemplate +import org.springframework.jms.core.MessageCreator +import org.springframework.stereotype.Service + +import javax.jms.JMSException +import javax.jms.Message +import javax.jms.Session +import java.util.concurrent.atomic.AtomicBoolean + +@CompileStatic +@Slf4j +@Service +class BookListener { + + @Autowired JmsTemplate jmsTemplate + ObjectMapper objectMapper = new ObjectMapper() + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + @JmsListener(destination = "input") + void returnBook(String messageAsString) { + BookReturned bookReturned = objectMapper.readerFor(BookReturned).readValue(messageAsString) as BookReturned + log.info("Returning book [$bookReturned]") + MessageCreator messageCreator = new MessageCreator() { + @Override + public Message createMessage(Session session) throws JMSException { + Message message = session.createObjectMessage(bookReturned); + message.setStringProperty('BOOK-NAME', bookReturned.bookName) + return message + } + }; + jmsTemplate.send('output', messageCreator) + } + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + @JmsListener(destination = "delete") + void bookDeleted(String bookDeletedAsString) { + BookDeleted bookDeleted = objectMapper.readerFor(BookDeleted).readValue(bookDeletedAsString) as BookDeleted + log.info("Deleting book [$bookDeleted]") + bookSuccessfulyDeleted.set(true) + } + + AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false) +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookReturned.groovy new file mode 100644 index 0000000000..a875572a76 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned implements Serializable { + String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookService.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookService.groovy new file mode 100644 index 0000000000..b6413c21c4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/BookService.groovy @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jms.core.JmsTemplate +import org.springframework.jms.core.MessageCreator +import org.springframework.stereotype.Service + +import javax.jms.JMSException +import javax.jms.Message +import javax.jms.Session + +@Service +@CompileStatic +@Slf4j +class BookService { + + @Autowired JmsTemplate jmsTemplate + + /** + Scenario for "should generate tests triggered by a method": + client side: must have a possibility to "trigger" sending of a message to the given messageFrom + server side: will run the method and await upon receiving message on the output messageFrom + + Method triggers sending a message to a source + */ + void returnBook(BookReturned bookReturned ) { + log.info("Returning book [$bookReturned]") + MessageCreator messageCreator = new MessageCreator() { + @Override + public Message createMessage(Session session) throws JMSException { + Message message = session.createObjectMessage(bookReturned); + message.setStringProperty('BOOK-NAME', bookReturned.bookName) + return message + } + }; + jmsTemplate.send('output', messageCreator) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringMessagingApplication.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringMessagingApplication.groovy new file mode 100644 index 0000000000..c4a6676fb8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/main/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringMessagingApplication.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.jms.annotation.EnableJms + +@SpringBootApplication +@EnableJms +class SpringMessagingApplication { + + static void main(String[] args) { + SpringApplication.run(SpringMessagingApplication.class, args) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringApplicationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringApplicationSpec.groovy new file mode 100644 index 0000000000..082cf2f2fc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/SpringApplicationSpec.groovy @@ -0,0 +1,158 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import javax.inject.Inject +/** + * SPIKE ON TESTS FROM NOTES IN MessagingSpec + */ +// Context configuration would end up in base class +@ContextConfiguration(classes = [SpringMessagingApplication], loader = SpringApplicationContextLoader) +public class SpringApplicationSpec extends Specification { + + // ALL CASES + @Inject ContractVerifierMessaging contractVerifierMessaging + ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper() + + def "should work for triggered based messaging"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // generated test should look like this: + when: + bookReturnedTriggered() + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'input') + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests without destination, triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'delete') + then: + noExceptionThrown() + bookWasDeleted() + } + + // BASE CLASS WOULD HAVE THIS: + + @Autowired BookService bookService + @Autowired BookListener bookListener + + void bookReturnedTriggered() { + bookService.returnBook(new BookReturned("foo")) + } + + PollingConditions pollingConditions = new PollingConditions() + + void bookWasDeleted() { + pollingConditions.eventually { + assert bookListener.bookSuccessfulyDeleted.get() + } + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessageBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessageBuilder.groovy new file mode 100644 index 0000000000..fdd0094caf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessageBuilder.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring.config + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder +import org.springframework.jms.core.MessageCreator + +import javax.jms.JMSException +import javax.jms.Message +import javax.jms.Session +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierSpringMessageBuilder implements ContractVerifierMessageBuilder { + + @Override + public ContractVerifierMessage create(final T payload, final Map headers) { + MessageCreator messageCreator = new MessageCreator() { + @Override + public Message createMessage(Session session) throws JMSException { + Message message = session.createObjectMessage(payload); + headers.entrySet().each { Map.Entry entry -> + message.setObjectProperty(entry.key, entry.value) + } + return message + } + }; + + return new SpringMessage<>(messageCreator) + } + + @Override + public ContractVerifierMessage create(Message message) { + return new SpringMessage<>(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessaging.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessaging.groovy new file mode 100644 index 0000000000..bd7e93503b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ContractVerifierSpringMessaging.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring.config + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.jms.core.JmsTemplate +import org.springframework.stereotype.Component + +import javax.jms.Message +import java.util.concurrent.TimeUnit +/** + * @author Marcin Grzejszczak + */ +@Component +public class ContractVerifierSpringMessaging implements ContractVerifierMessaging { + + private static final Logger log = LoggerFactory.getLogger(ContractVerifierSpringMessaging.class); + + private final JmsTemplate jmsTemplate; + private final ContractVerifierMessageBuilder builder; + + @Autowired + @SuppressWarnings("unchecked") + public ContractVerifierSpringMessaging(ContractVerifierMessageBuilder builder, JmsTemplate jmsTemplate) { + this.builder = builder + this.jmsTemplate = jmsTemplate + } + + @Override + @SuppressWarnings("unchecked") + public void send(T payload, Map headers, String destination) { + send(builder.create(payload, headers), destination); + } + + @Override + public void send(ContractVerifierMessage message, String destination) { + try { + jmsTemplate.send(destination, ((SpringMessage) message).messageCreator) + } catch (Exception e) { + log.error("Exception occurred while trying to send a message [" + message + "] " + + "to a channel with name [" + destination + "]", e); + throw e; + } + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + try { + return builder.create(jmsTemplate.receive(destination)); + } catch (Exception e) { + log.error("Exception occurred while trying to read a message from " + + " a channel with name [" + destination + "]", e); + throw new RuntimeException(e); + } + } + + @Override + public ContractVerifierMessage receiveMessage(String destination) { + return receiveMessage(destination, 5, TimeUnit.SECONDS); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage create(T t, Map headers) { + return builder.create(t, headers); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage create(Message message) { + return builder.create(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ManualContractVerifierConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ManualContractVerifierConfiguration.groovy new file mode 100644 index 0000000000..eb9966e3b3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/ManualContractVerifierConfiguration.groovy @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring.config + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.core.JmsTemplate + +/** + * @author Marcin Grzejszczak + */ +@Configuration +class ManualContractVerifierConfiguration { + + @Bean + ContractVerifierMessaging contractVerifierMessaging(JmsTemplate jmsTemplate) { + return new ContractVerifierSpringMessaging(new ContractVerifierSpringMessageBuilder(), jmsTemplate); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/SpringMessage.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/SpringMessage.groovy new file mode 100644 index 0000000000..28239443a1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-spring/src/test/groovy/org/springframework/cloud/contract/verifier/samples/spring/config/SpringMessage.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.spring.config + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.jms.core.MessageCreator + +import javax.jms.Message +import javax.jms.ObjectMessage +/** + * @author Marcin Grzejszczak + */ +public class SpringMessage implements ContractVerifierMessage { + + private final ObjectMessage messageDelegate; + final MessageCreator messageCreator; + + public SpringMessage(Message messageDelegate) { + this.messageDelegate = (ObjectMessage) messageDelegate; + this.messageCreator = null + } + + public SpringMessage(MessageCreator messageCreator) { + this.messageDelegate = null; + this.messageCreator = messageCreator + } + + @Override + public T getPayload() { + return messageDelegate.getObject(); + } + + @Override + public Map getHeaders() { + Map headers = [:] + def enumeration = messageDelegate.propertyNames + while(enumeration.hasMoreElements()) { + String propertyName = enumeration.nextElement() + headers << [(propertyName) : messageDelegate.getObjectProperty(propertyName)] + } + return headers + } + + @Override + Object getHeader(String key) { + return getHeaders().get(key) + } + + @Override + public Message convert() { + return messageDelegate; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/build.gradle new file mode 100644 index 0000000000..296798e705 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +repositories { + mavenLocal() + jcenter() + maven { + url "http://repo.spring.io/snapshot" + } + maven { + url "http://repo.spring.io/milestone" + } +} + +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-core") + compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + compile "org.springframework.cloud:spring-cloud-stream-binder-rabbit:${springStreamVersion}" + + testCompile project(":$verifier-root:$verifier-messaging-root:$verifier-stream") + testCompile "org.springframework.cloud:spring-cloud-stream-test-support:${springStreamVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy new file mode 100644 index 0000000000..e43c21c9d2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookDeleted.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookDeleted { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookDeleted(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy new file mode 100644 index 0000000000..6e70889df4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookListener.groovy @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.cloud.stream.annotation.StreamListener +import org.springframework.cloud.stream.messaging.Sink +import org.springframework.cloud.stream.messaging.Source +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder + +import java.util.concurrent.atomic.AtomicBoolean + +@CompileStatic +@Slf4j +@EnableBinding([DeleteSink, Sink]) +class BookListener { + + @Autowired Source source + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + @StreamListener(Sink.INPUT) + void returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + source.output().send(MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ]))) + } + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + @StreamListener(DeleteSink.INPUT) + void bookDeleted(BookDeleted bookDeleted) { + log.info("Deleting book [$bookDeleted]") + // ... doing some work + bookSuccessfulyDeleted.set(true) + } + + AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false) +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy new file mode 100644 index 0000000000..130c19d468 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy new file mode 100644 index 0000000000..4e8a8adefa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/BookService.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.stream.messaging.Source +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Service + +@Service +@CompileStatic +@Slf4j +class BookService { + + private final Source source + + @Autowired + BookService(Source source) { + this.source = source + } + + /** + Scenario for "should generate tests triggered by a method": + client side: must have a possibility to "trigger" sending of a message to the given messageFrom + server side: will run the method and await upon receiving message on the output messageFrom + + Method triggers sending a message to a source + */ + void returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + source.output().send(MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ]))) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/DeleteSink.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/DeleteSink.groovy new file mode 100644 index 0000000000..aa6e72e4e4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/DeleteSink.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import org.springframework.cloud.stream.annotation.Input +import org.springframework.cloud.stream.messaging.Sink +import org.springframework.messaging.SubscribableChannel + +/** + * @author Marcin Grzejszczak + */ +interface DeleteSink extends Sink { + + String INPUT = "delete"; + + @Input(DeleteSink.INPUT) + SubscribableChannel delete(); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplication.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplication.groovy new file mode 100644 index 0000000000..9fa8eb8f86 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplication.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.cloud.stream.messaging.Sink; +import org.springframework.cloud.stream.messaging.Source; + +@SpringBootApplication +@EnableBinding([Source, Sink]) +class StreamMessagingApplication { + + static void main(String[] args) { + SpringApplication.run(StreamMessagingApplication.class, args) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/resources/application.yml new file mode 100644 index 0000000000..1ff1cf7519 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/main/resources/application.yml @@ -0,0 +1,2 @@ +spring.cloud.stream.bindings.input.destination: inputDestination +spring.cloud.stream.bindings.output.destination: sentTo \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplicationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..1a21561554 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/samples/samples-messaging-stream/src/test/groovy/org/springframework/cloud/contract/verifier/samples/messaging/StreamMessagingApplicationSpec.groovy @@ -0,0 +1,152 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.samples.messaging + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import javax.inject.Inject +/** + * SPIKE ON TESTS FROM NOTES IN MessagingSpec + */ +// Context configuration would end up in base class +@ContextConfiguration(classes = [StreamMessagingApplication], loader = SpringApplicationContextLoader) +public class StreamMessagingApplicationSpec extends Specification { + + // ALL CASES + @Inject ContractVerifierMessaging contractVerifierMessaging + ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper() + + def "should work for triggered based messaging"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // generated test should look like this: + when: + bookReturnedTriggered() + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'input') + then: + def response = contractVerifierMessaging.receiveMessage('output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests without destination, triggered by a message"() { + given: + def dsl = Contract.make { + label 'some_label' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + + // generated test should look like this: + + //given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'] + ) + when: + contractVerifierMessaging.send(inputMessage, 'delete') + then: + noExceptionThrown() + bookWasDeleted() + } + + // BASE CLASS WOULD HAVE THIS: + + @Autowired BookService bookService + @Autowired BookListener bookListener + + void bookReturnedTriggered() { + bookService.returnBook(new BookReturned("foo")) + } + + void bookWasDeleted() { + assert bookListener.bookSuccessfulyDeleted.get() + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/scripts/runTests.sh b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/scripts/runTests.sh new file mode 100755 index 0000000000..4eb0cf6494 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/scripts/runTests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -o errexit + +ROOT_FOLDER=`pwd` +echo "Current folder is $ROOT_FOLDER" + +mkdir -p build +CONTRACT_VERIFIER_VERSION=1.0.0.BUILD-SNAPSHOT +export CONTRACT_VERIFIER_VERSION=${CONTRACT_VERIFIER_VERSION} + +echo "Current Spring Cloud Contract Verifier version is ${CONTRACT_VERIFIER_VERSION}" + +cd build +echo "Cloning samples" +git clone https://github.com/Codearte/accurest-samples +cd accurest-samples + +echo "Running Gradle tests" +. ./runTests.sh + +echo "Running Maven tests" +. ./runMavenTests.sh + +cd $ROOT_FOLDER \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/settings.gradle new file mode 100644 index 0000000000..bb28620d0c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/settings.gradle @@ -0,0 +1,39 @@ +String verifier = "spring-cloud-contract-verifier" +String stubRunner = "spring-cloud-contract-stub-runner" + +rootProject.name = "$verifier-parent" + +include "docs" + +include ":spring-cloud-contract-spec" + +include ":$verifier:$verifier-core", + ":$verifier:$verifier-gradle-plugin", + ":$verifier:$verifier-converters", + ":$verifier:$verifier-testing-utils" + +include ":$verifier:$verifier-messaging:$verifier-messaging-core", + ":$verifier:$verifier-messaging:$verifier-integration", + ":$verifier:$verifier-messaging:$verifier-stream", + ":$verifier:$verifier-messaging:$verifier-camel" + +include ":$stubRunner:$stubRunner", + ":$stubRunner:$stubRunner-spring", + ":$stubRunner:$stubRunner-boot", + ":$stubRunner:$stubRunner-spring-cloud", + ":$stubRunner:$stubRunner-junit" + +include ":$stubRunner:$stubRunner-messaging:$stubRunner-integration", + ":$stubRunner:$stubRunner-messaging:$stubRunner-stream", + ":$stubRunner:$stubRunner-messaging:$stubRunner-camel" + +include ":samples:samples-messaging-stream", + ":samples:samples-messaging-integration", + ":samples:samples-messaging-camel", + ":samples:samples-messaging-spring" + +//to prevent StackOverflow in Sonar +project(":$verifier").name = "$verifier-root" +project(":$stubRunner").name = "$stubRunner-root" +project(":$verifier:$verifier-messaging").name = "$verifier-messaging-root" +project(":$stubRunner:$stubRunner-messaging").name = "$stubRunner-messaging-root" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy new file mode 100644 index 0000000000..7e424d019d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.internal.Input +import org.springframework.cloud.contract.spec.internal.OutputMessage +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.spec.internal.Response + +/** + * The point of entry to the DSL + * + * @since 1.0.0 + */ +@TypeChecked +@EqualsAndHashCode +@ToString(includeFields = true, includePackage = false, includeNames = true) +class Contract { + + Integer priority + Request request + Response response + String label + String description + Input input + OutputMessage outputMessage + + protected Contract() {} + + /** + * Factory method to create the DSL + */ + static Contract make(Closure closure) { + Contract dsl = new Contract() + closure.delegate = dsl + closure() + return dsl + } + + void priority(int priority) { + this.priority = priority + } + + void label(String label) { + this.label = label + } + + void description(String description) { + this.description = description + } + + void request(@DelegatesTo(Request) Closure closure) { + this.request = new Request() + closure.delegate = request + closure() + } + + void response(@DelegatesTo(Response) Closure closure) { + this.response = new Response() + closure.delegate = response + closure() + } + + void input(@DelegatesTo(Input) Closure closure) { + this.input = new Input() + closure.delegate = input + closure() + } + + void outputMessage(@DelegatesTo(OutputMessage) Closure closure) { + this.outputMessage = new OutputMessage() + closure.delegate = outputMessage + closure() + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractVerifierException.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractVerifierException.groovy new file mode 100644 index 0000000000..2750320176 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractVerifierException.groovy @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec + +import groovy.transform.CompileStatic + +/** + * @author Jakub Kubrynski, codearte.io + */ +@CompileStatic +class ContractVerifierException extends RuntimeException { + + ContractVerifierException(String message) { + super(message) + } + + ContractVerifierException(String message, Throwable cause) { + super(message, cause) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy new file mode 100644 index 0000000000..38e42ad8ba --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a body of a request / response or a message + * + * @since 1.0.0 + */ +@ToString(includePackage = false, includeFields = true, includeNames = true) +@EqualsAndHashCode(includeFields = true) +@CompileStatic +class Body extends DslProperty { + + Body(Map body) { + super(extractValue(body, { DslProperty p -> p.clientValue}), extractValue(body, {DslProperty p -> p.serverValue})) + } + + private static Map extractValue(Map body, Closure valueProvider) { + body.collectEntries { Map.Entry entry -> + [(entry.key): valueProvider(entry.value)] + } as Map + } + + Body(List bodyAsList) { + super(bodyAsList.collect { DslProperty p -> p.clientValue }, bodyAsList.collect { DslProperty p -> p.serverValue }) + } + + Body(Object bodyAsValue) { + this("${bodyAsValue}") + } + + Body(GString bodyAsValue) { + super(bodyAsValue, bodyAsValue) + } + + Body(DslProperty bodyAsValue) { + super(bodyAsValue.clientValue, bodyAsValue.serverValue) + } + + Body(MatchingStrategy matchingStrategy) { + super(matchingStrategy, matchingStrategy) + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy new file mode 100644 index 0000000000..e16afaffb5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic + +/** + * Represents a client side {@link DslProperty} + * + * @since 1.0.0 + */ +@CompileStatic +class ClientDslProperty extends DslProperty { + + ClientDslProperty(Object singleValue) { + super(singleValue) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy new file mode 100644 index 0000000000..6cbf2a3a6f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy @@ -0,0 +1,166 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked + +import java.util.regex.Pattern + +/** + * Contains useful common methods for the DSL. + * + * @TypeChecked instead of @CompileStatic due to usage of double dispatch. + * Double dispatch doesn't work if you're using @CompileStatic + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +class Common { + + @Delegate private final RegexPatterns regexPatterns = new RegexPatterns() + + Map convertObjectsToDslProperties(Map body) { + return body.collectEntries { + Map.Entry entry -> + [(entry.key): toDslProperty(entry.value)] + } as Map + } + + Collection convertObjectsToDslProperties(List body) { + return (body.collect { + Object element -> toDslProperty(element) + } as List) + } + + DslProperty toDslProperty(Object property) { + return new DslProperty(property) + } + + DslProperty toDslProperty(Map property) { + return new DslProperty(property.collectEntries { + [(it.key): toDslProperty(it.value)] + }) + } + + DslProperty toDslProperty(List property) { + return new DslProperty(property.collect { + toDslProperty(it) + }) + } + + DslProperty toDslProperty(DslProperty property) { + return property + } + + NamedProperty named(DslProperty name, DslProperty value){ + return new NamedProperty(name, value) + } + + NamedProperty named(Map namedMap){ + return new NamedProperty(namedMap) + } + + DslProperty value(ClientDslProperty client, ServerDslProperty server) { + assertThatSidesMatch(client.clientValue, server.serverValue) + return new DslProperty(client.clientValue, server.serverValue) + } + + DslProperty value(Object value) { + return new DslProperty(value) + } + + DslProperty value(ServerDslProperty server, ClientDslProperty client) { + assertThatSidesMatch(client.clientValue, server.serverValue) + return new DslProperty(client.clientValue, server.serverValue) + } + + DslProperty $(ClientDslProperty client, ServerDslProperty server) { + return value(client, server) + } + + DslProperty $(ServerDslProperty server, ClientDslProperty client) { + return value(server, client) + } + + Pattern regex(String regex) { + return Pattern.compile(regex) + } + + OptionalProperty optional(Object object) { + return new OptionalProperty(object) + } + + ExecutionProperty execute(String commandToExecute) { + return new ExecutionProperty(commandToExecute) + } + + ClientDslProperty client(Object clientValue) { + return new ClientDslProperty(clientValue) + } + + ClientDslProperty stub(Object clientValue) { + return new ClientDslProperty(clientValue) + } + + ServerDslProperty server(Object serverValue) { + return new ServerDslProperty(serverValue) + } + + ServerDslProperty test(Object serverValue) { + return new ServerDslProperty(serverValue) + } + + void assertThatSidesMatch(OptionalProperty stubSide, Object testSide) { + assert testSide ==~ Pattern.compile(stubSide.optionalPattern()) + } + + void assertThatSidesMatch(Pattern pattern, String value) { + assert value ==~ pattern + } + + void assertThatSidesMatch(String value, Pattern pattern) { + assert value ==~ pattern + } + + void assertThatSidesMatch(MatchingStrategy firstSide, MatchingStrategy secondSide) { + if (firstSide.type == MatchingStrategy.Type.ABSENT && secondSide != MatchingStrategy.Type.ABSENT) { + throwAbsentError() + } + } + + void assertThatSidesMatch(MatchingStrategy firstSide, Object secondSide) { + if (firstSide.type == MatchingStrategy.Type.ABSENT) { + throwAbsentError() + } + } + + void assertThatSidesMatch(Object firstSide, MatchingStrategy secondSide) { + if (secondSide.type == MatchingStrategy.Type.ABSENT) { + throwAbsentError() + } + } + + private void throwAbsentError() { + throw new IllegalStateException("Absent cannot only be used only on one side") + } + + void assertThatSidesMatch(Object firstSide, Object secondSide) { + // do nothing + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy new file mode 100644 index 0000000000..4be6c853ae --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents an element of a DSL that can contain client or sever side values + * + * @since 1.0.0 + */ +@CompileStatic +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeNames = true) +class DslProperty { + + final T clientValue + final T serverValue + + DslProperty(T clientValue, T serverValue) { + this.clientValue = clientValue + this.serverValue = serverValue + } + + DslProperty(T singleValue) { + this.clientValue = singleValue + this.serverValue = singleValue + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy new file mode 100644 index 0000000000..6f79e6dbcf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic + +/** + * Represents a property that will become an executable method in the + * generated tests + * + * @since 1.0.0 + */ +@CompileStatic +class ExecutionProperty { + + private static final String PLACEHOLDER_VALUE = '\\$it' + + final String executionCommand + + ExecutionProperty(String executionCommand) { + this.executionCommand = executionCommand + } + + /** + * Inserts the provided code as a parameter to the method and returns + * the code that represents that method execution + */ + String insertValue(String valueToInsert) { + return executionCommand.replaceAll(PLACEHOLDER_VALUE, valueToInsert) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Header.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Header.groovy new file mode 100644 index 0000000000..9989d875b4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Header.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a header of a request / response or a message + * + * @since 1.0.0 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@CompileStatic +class Header extends DslProperty { + + String name + + Header(String name, DslProperty dslProperty) { + super(dslProperty.clientValue, dslProperty.serverValue) + this.name = name + } + + Header(String name, Object value) { + super(value) + this.name = name + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy new file mode 100644 index 0000000000..036a23598e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +/** + * Represents a set of headers of a request / response or a message + * + * @since 1.0.0 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@TypeChecked +class Headers { + + Set
entries = [] + + void header(Map singleHeader) { + Map.Entry first = singleHeader.entrySet().first() + entries << new Header(first?.key, first?.value) + } + + void header(String headerKey, Object headerValue) { + entries << new Header(headerKey, headerValue) + } + + void executeForEachHeader(Closure closure) { + entries?.each { + header -> closure(header) + } + } + + /** + * Converts the headers into their stub side representations and returns as + * a map of String key => Object value. + */ + Map asStubSideMap() { + def acc = [:].withDefault { [] as Collection } + return entries.inject(acc as Map) { Map map, Header header -> + map[header.name] = header.clientValue + return map + } as Map + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + Headers headers = (Headers) o + if (entries != headers.entries) return false + return true + } + + int hashCode() { + return entries.hashCode() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy new file mode 100644 index 0000000000..124b25ce50 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Input.groovy @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +/** + * Represents an input for messaging. The input can be a message or some + * action inside the application. + * + * @since 1.0.0 + */ +@TypeChecked +@EqualsAndHashCode +@ToString(includePackage = false, includeNames = true) +class Input extends Common { + + DslProperty messageFrom + ExecutionProperty triggeredBy + Headers messageHeaders + BodyType messageBody + ExecutionProperty assertThat + + Input() {} + + Input(Input input) { + this.messageFrom = input.messageFrom + this.messageHeaders = input.messageHeaders + this.messageBody = input.messageBody + } + + /** + * Helper method to provide a better name for the producer side + */ + ServerDslProperty producer(Object clientValue) { + return new ServerDslProperty(clientValue) + } + + /** + * Helper method to provide a better name for the consumer side + */ + ClientDslProperty consumer(Object clientValue) { + return new ClientDslProperty(clientValue) + } + + /** + * Name of a destination from which message would come to trigger action in the system + */ + void messageFrom(String messageFrom) { + this.messageFrom = new DslProperty<>(messageFrom) + } + + /** + * Name of a destination from which message would come to trigger action in the system + */ + void messageFrom(DslProperty messageFrom) { + this.messageFrom = messageFrom + } + + /** + * Function that needs to be executed to trigger action in the system + */ + void triggeredBy(String triggeredBy) { + this.triggeredBy = new ExecutionProperty(triggeredBy) + } + + BodyType messageBody(Object bodyAsValue) { + this.messageBody = new BodyType(bodyAsValue) + } + + void messageHeaders(@DelegatesTo(Headers) Closure closure) { + this.messageHeaders = new Headers() + closure.delegate = messageHeaders + closure() + } + + static class BodyType extends DslProperty { + + BodyType(Object clientValue, Object serverValue) { + super(clientValue, serverValue) + } + + BodyType(Object singleValue) { + super(singleValue) + } + } + + void assertThat(String assertThat) { + this.assertThat = new ExecutionProperty(assertThat) + } +} + + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ServerInput extends Input { + ServerInput(Input request) { + super(request) + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ClientInput extends Input { + ClientInput(Input request) { + super(request) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/JSONCompareMode.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/JSONCompareMode.groovy new file mode 100644 index 0000000000..c1e3b21e7d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/JSONCompareMode.groovy @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +/** + * Represents JSON comparison modes + * + * @since 1.0.0 + */ +enum JSONCompareMode { + STRICT, LENIENT, NON_EXTENSIBLE, STRICT_ORDER +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingStrategy.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingStrategy.groovy new file mode 100644 index 0000000000..0b30b40f23 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingStrategy.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString; + +/** + * Represents a matching strategy for a JSON + * + * @since 1.0.0 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@CompileStatic +class MatchingStrategy extends DslProperty { + + Type type + JSONCompareMode jsonCompareMode + + MatchingStrategy(Object value, Type type) { + this(value, type, null) + } + + MatchingStrategy(Object value, Type type, JSONCompareMode jsonCompareMode) { + super(value) + this.type = type + this.jsonCompareMode = jsonCompareMode + } + + MatchingStrategy(DslProperty value, Type type) { + this(value, type, null) + } + + MatchingStrategy(DslProperty value, Type type, JSONCompareMode jsonCompareMode) { + super(value.clientValue, value.serverValue) + this.type = type + this.jsonCompareMode = jsonCompareMode + } + + enum Type { + EQUAL_TO("equalTo"), CONTAINS("containing"), MATCHING("matches"), NOT_MATCHING("doesNotMatch"), + EQUAL_TO_JSON("equalToJson"), EQUAL_TO_XML("equalToXml"), ABSENT("absent") + + final String name + + Type(name) { + this.name = name + } + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Multipart.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Multipart.groovy new file mode 100644 index 0000000000..59efa9fe77 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Multipart.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@ToString(includePackage = false, includeFields = true, includeNames = true) +@EqualsAndHashCode(includeFields = true) +@CompileStatic +class Multipart extends DslProperty { + + Multipart(Map multipart) { + super(extractValue(multipart, { DslProperty p -> p.clientValue}), extractValue(multipart, {DslProperty p -> p.serverValue})) + } + + private static Map extractValue(Map multipart, Closure valueProvider) { + return multipart.collectEntries { Map.Entry entry -> + [(entry.key): valueProvider(entry.value)] + } as Map + } + + Multipart(List multipartAsList) { + super(multipartAsList.collect { DslProperty p -> p.clientValue }, multipartAsList.collect { DslProperty p -> p.serverValue }) + } + + Multipart(Object multipartAsValue) { + this("${multipartAsValue}") + } + + Multipart(GString multipartAsValue) { + super(multipartAsValue, multipartAsValue) + } + + Multipart(DslProperty multipartAsValue) { + super(multipartAsValue.clientValue, multipartAsValue.serverValue) + } + + Multipart(MatchingStrategy matchingStrategy) { + super(matchingStrategy, matchingStrategy) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy new file mode 100644 index 0000000000..798ddcb0f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a property that has name and content. Used together with + * multipart requests. + * + * @since 1.0.0 + */ +@ToString(includePackage = false, includeFields = true, includeNames = true) +@EqualsAndHashCode(includeFields = true) +@CompileStatic +class NamedProperty { + + private static final String NAME = 'name' + private static final String CONTENT = 'content' + + DslProperty name + DslProperty value + + NamedProperty(DslProperty name, DslProperty value) { + this.name = name + this.value = value + } + + NamedProperty(Map namedMap) { + this(namedMap?.get(NAME), namedMap?.get(CONTENT)) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy new file mode 100644 index 0000000000..6ac232f29c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic + +/** + * Represents a property that may or may not be there + * + * @since 1.0.0 + */ +@CompileStatic +class OptionalProperty { + final Object value + + OptionalProperty(Object value) { + this.value = value + } + + /** + * String version of a regular expression that wraps the provided value + * in an optional function + */ + String optionalPattern() { + return "($value)?" + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy new file mode 100644 index 0000000000..260bd68f5a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +@TypeChecked +@EqualsAndHashCode +@ToString(includePackage = false, includeNames = true) +class OutputMessage extends Common { + + DslProperty sentTo + Headers headers + DslProperty body + ExecutionProperty assertThat + + OutputMessage() {} + + OutputMessage(OutputMessage outputMessage) { + this.sentTo = outputMessage.sentTo + this.headers = outputMessage.headers + this.body = outputMessage.body + } + + void sentTo(String sentTo) { + this.sentTo = new DslProperty(sentTo) + } + + void sentTo(DslProperty sentTo) { + this.sentTo = sentTo + } + + ServerDslProperty producer(Object clientValue) { + return new ServerDslProperty(clientValue) + } + + ClientDslProperty consumer(Object clientValue) { + return new ClientDslProperty(clientValue) + } + + void body(Object bodyAsValue) { + this.body = new DslProperty(bodyAsValue) + } + + void body(DslProperty bodyAsValue) { + this.body = bodyAsValue + } + + void headers(@DelegatesTo(Headers) Closure closure) { + this.headers = new Headers() + closure.delegate = headers + closure() + } + + void assertThat(String assertThat) { + this.assertThat = new ExecutionProperty(assertThat) + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ServerOutputMessage extends OutputMessage { + ServerOutputMessage(OutputMessage request) { + super(request) + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ClientOutputMessage extends OutputMessage { + ClientOutputMessage(OutputMessage request) { + super(request) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy new file mode 100644 index 0000000000..d34ea61383 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +import static org.springframework.cloud.contract.spec.util.ValidateUtils.validateServerValueIsAvailable + +/** + * Represents a single HTTP query parameter + * + * @since 1.0.0 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@CompileStatic +class QueryParameter extends DslProperty { + + String name + + QueryParameter(String name, DslProperty dslProperty) { + super(dslProperty.clientValue, dslProperty.serverValue) + validateServerValueIsAvailable(dslProperty.serverValue, "Query parameter '$name'") + this.name = name + } + + QueryParameter(String name, MatchingStrategy matchingStrategy) { + super(matchingStrategy) + validateServerValueIsAvailable(matchingStrategy, "Query parameter '$name'") + this.name = name + } + + QueryParameter(String name, Object value) { + super(value) + validateServerValueIsAvailable(value, "Query parameter '$name'") + this.name = name + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy new file mode 100644 index 0000000000..26cbec3730 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@TypeChecked +class QueryParameters { + + List parameters = [] + + void parameter(Map singleParameter) { + Map.Entry first = singleParameter.entrySet().first() + parameters << new QueryParameter(first?.key, first?.value) + } + + void parameter(String parameterName, Object parameterValue) { + parameters << new QueryParameter(parameterName, parameterValue) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy new file mode 100644 index 0000000000..7177ecf7c0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic + +import java.util.regex.Pattern + +/** + * Contains most common regular expression patterns + * + * @since 1.0.0 + */ +@CompileStatic +class RegexPatterns { + + private static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/) + private static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/) + private static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?') + private static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])'); + private static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?'); + private static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}'); + private static final Pattern URL = Pattern.compile('((www\\.|(http|https|ftp|news|file)+\\:\\/\\/)[_.a-z0-9-]+\\.[a-z0-9\\/_:@=.+?,##%&~-]*[^.|\\\'|\\# |!|\\(|?|,| |>|<|;|\\)])') + + String onlyAlphaUnicode() { + return ONLY_ALPHA_UNICODE.pattern() + } + + String number() { + return NUMBER.pattern() + } + + String anyBoolean() { + return TRUE_OR_FALSE.pattern() + } + + String ipAddress() { + return IP_ADDRESS.pattern() + } + + String hostname() { + return HOSTNAME_PATTERN.pattern() + } + + String email() { + return EMAIL.pattern() + } + + String url() { + return URL.pattern() + } + + static String multipartParam(Object name, Object value) { + return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"\r\n(Content-Type: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$value\r\n--\\1.*" + } + + static String multipartFile(Object name, Object filename, Object content) { + return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n(Content-Type: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$content\r\n--\\1.*"; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy new file mode 100644 index 0000000000..724771d2f4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy @@ -0,0 +1,193 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +/** + * Represents the request side of the HTTP communication + * + * @since 1.0.0 + */ +@TypeChecked +@EqualsAndHashCode +@ToString(includePackage = false, includeNames = true) +class Request extends Common { + + DslProperty method + Url url + UrlPath urlPath + Headers headers + Body body + Multipart multipart + + Request() { + } + + Request(Request request) { + this.method = request.method + this.url = request.url + this.urlPath = request.urlPath + this.headers = request.headers + this.body = request.body + this.multipart = request.multipart + } + + void method(String method) { + this.method = toDslProperty(method) + } + + void method(DslProperty method) { + this.method = toDslProperty(method) + } + + void url(Object url) { + this.url = new Url(url) + } + + void url(DslProperty url) { + this.url = new Url(url) + } + + void url(Object url, @DelegatesTo(UrlPath) Closure closure) { + this.url = new Url(url) + closure.delegate = this.url + closure() + } + + void url(DslProperty url, @DelegatesTo(UrlPath) Closure closure) { + this.url = new Url(url) + closure.delegate = this.url + closure() + } + + void urlPath(String path) { + this.urlPath = new UrlPath(path) + } + + void urlPath(DslProperty path) { + this.urlPath = new UrlPath(path) + } + + void urlPath(String path, @DelegatesTo(UrlPath) Closure closure) { + this.urlPath = new UrlPath(path) + closure.delegate = urlPath + closure() + } + + void urlPath(DslProperty path, @DelegatesTo(UrlPath) Closure closure) { + this.urlPath = new UrlPath(path) + closure.delegate = urlPath + closure() + } + + void headers(@DelegatesTo(Headers) Closure closure) { + this.headers = new Headers() + closure.delegate = headers + closure() + } + + void body(Map body) { + this.body = new Body(convertObjectsToDslProperties(body)) + } + + void body(List body) { + this.body = new Body(convertObjectsToDslProperties(body)) + } + + void body(DslProperty dslProperty) { + this.body = new Body(dslProperty) + } + + void body(Object bodyAsValue) { + this.body = new Body(bodyAsValue) + } + + Body getBody() { + return body + } + + void multipart(Map body) { + this.multipart = new Multipart(convertObjectsToDslProperties(body)) + } + + void multipart(List multipartAsList) { + this.multipart = new Multipart(convertObjectsToDslProperties(multipartAsList)) + } + + void multipart(DslProperty dslProperty) { + this.multipart = new Multipart(dslProperty) + } + + void multipart(Object multipartAsValue) { + this.multipart = new Multipart(multipartAsValue) + } + + MatchingStrategy equalTo(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.EQUAL_TO) + } + + MatchingStrategy containing(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.CONTAINS) + } + + MatchingStrategy matching(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.MATCHING) + } + + MatchingStrategy notMatching(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.NOT_MATCHING) + } + + MatchingStrategy equalToXml(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.EQUAL_TO_XML) + } + + MatchingStrategy equalToJson(Object value) { + return new MatchingStrategy(value, MatchingStrategy.Type.EQUAL_TO_JSON) + } + + MatchingStrategy absent() { + return new MatchingStrategy(true, MatchingStrategy.Type.ABSENT) + } + + void assertThatSidesMatch(Object stubSide, OptionalProperty testSide) { + throw new IllegalStateException("Optional can be used only for the stub side of the request!") + } + +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ServerRequest extends Request { + ServerRequest(Request request) { + super(request) + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ClientRequest extends Request { + ClientRequest(Request request) { + super(request) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy new file mode 100644 index 0000000000..454841becd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +/** + * Represents the response side of the HTTP communication + * + * @since 1.0.0 + */ +@TypeChecked +@EqualsAndHashCode +@ToString(includePackage = false, includeFields = true) +class Response extends Common { + + DslProperty status + DslProperty delay + Headers headers + Body body + boolean async + + Response() { + } + + Response(Response response) { + this.status = response.status + this.headers = response.headers + this.body = response.body + } + + void status(int status) { + this.status = toDslProperty(status) + } + + void status(DslProperty status) { + this.status = toDslProperty(status) + } + + void headers(@DelegatesTo(Headers) Closure closure) { + this.headers = new Headers() + closure.delegate = headers + closure() + } + + void body(Map body) { + this.body = new Body(convertObjectsToDslProperties(body)) + } + + void body(List body) { + this.body = new Body(convertObjectsToDslProperties(body)) + } + + void body(Object bodyAsValue) { + this.body = new Body(bodyAsValue) + } + + void fixedDelayMilliseconds(int timeInMilliseconds) { + this.delay = toDslProperty(timeInMilliseconds) + } + + void async() { + this.async = true + } + + void assertThatSidesMatch(OptionalProperty stubSide, Object testSide) { + throw new IllegalStateException("Optional can be used only in the test side of the response!") + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ServerResponse extends Response { + ServerResponse(Response request) { + super(request) + } +} + +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class ClientResponse extends Response { + ClientResponse(Response request) { + super(request) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy new file mode 100644 index 0000000000..3cf7ec93cc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a server side {@link DslProperty} + * + * @since 1.0.0 + */ +@CompileStatic +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false) +class ServerDslProperty extends DslProperty { + + ServerDslProperty(Object singleValue) { + super(singleValue) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy new file mode 100644 index 0000000000..1abee32aa5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +import static org.springframework.cloud.contract.spec.util.ValidateUtils.validateServerValueIsAvailable + +/** + * Represents a URL that may contain query parameters + * + * @since 1.0.0 + */ +@ToString(includePackage = false, includeFields = true, includeNames = true) +@EqualsAndHashCode(includeFields = true) +@CompileStatic +class Url extends DslProperty { + + QueryParameters queryParameters + + Url(DslProperty prop) { + super(prop.clientValue, prop.serverValue) + validateServerValueIsAvailable(prop.serverValue, "Url") + } + + Url(Object url) { + super(url) + validateServerValueIsAvailable(url, "Url") + } + + void queryParameters(@DelegatesTo(QueryParameters) Closure closure) { + this.queryParameters = new QueryParameters() + closure.delegate = queryParameters + closure() + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy new file mode 100644 index 0000000000..df4f277459 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a url path + * + * @since 1.0.0 + */ +@ToString(includePackage = false, includeFields = true, includeNames = true) +@EqualsAndHashCode(includeFields = true) +@CompileStatic +class UrlPath extends Url { + + UrlPath(String path) { + super(path) + } + + UrlPath(DslProperty path) { + super(path) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/ValidateUtils.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/ValidateUtils.groovy new file mode 100644 index 0000000000..087ddb77f0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/ValidateUtils.groovy @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.util + +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.MatchingStrategy + +import java.util.regex.Pattern + +import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.ABSENT +import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.EQUAL_TO + +/** + * Checks the validity of DSL entries. + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @since 1.0.0 + */ +@TypeChecked +class ValidateUtils { + + static Object validateServerValueIsAvailable(Object value) { + validateServerValueIsAvailable(value, "Server value") + return value + } + + static Object validateServerValueIsAvailable(Object value, String msg) { + validateServerValue(value, msg) + return value + } + + static void validateServerValue(Pattern pattern, String msg) { + throw new IllegalStateException("$msg can't be a pattern for the server side") + } + + static List ALLOWED_MATCHING_TYPES_ON_SERVER_SIDE = [EQUAL_TO, ABSENT] + + static void validateServerValue(MatchingStrategy matchingStrategy, String msg) { + if (!ALLOWED_MATCHING_TYPES_ON_SERVER_SIDE.contains(matchingStrategy.type)) { + throw new IllegalStateException("$msg can't be of a matching type: $matchingStrategy.type for the server side") + } + validateServerValue(matchingStrategy.serverValue, msg) + } + + static void validateServerValue(DslProperty value, String msg) { + validateServerValue(value.serverValue, msg) + } + + static void validateServerValue(Object value, String msg) { + // OK + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ExecutionPropertySpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ExecutionPropertySpec.groovy new file mode 100644 index 0000000000..65b3997d99 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ExecutionPropertySpec.groovy @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import spock.lang.Specification + +class ExecutionPropertySpec extends Specification { + + def 'should insert passed value in place of $it placeholder'() { + given: + String commandToExecute = 'commandToExecute($it)' + ExecutionProperty executionProperty = new ExecutionProperty(commandToExecute) + and: + String valueToInsert = 'someObject.itsValue' + when: + String commandWithInsertedValue = executionProperty.insertValue(valueToInsert) + then: + 'commandToExecute(someObject.itsValue)' == commandWithInsertedValue + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy new file mode 100644 index 0000000000..80b9333bce --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import spock.lang.Specification + +import java.util.regex.Pattern + +class RegexPatternsSpec extends Specification { + + RegexPatterns regexPatterns = new RegexPatterns() + + def "should generate a regex for ip address [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.ipAddress()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + '123.123.123.123' || true + 'a.b.' || false + } + + def "should generate a regex for hostname [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.hostname()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'https://asd.com' || true + 'https://asd.com:8080' || true + 'https://localhost' || true + 'https://localhost:8080' || true + 'https://asd.com/asd' || false + 'asd.com' || false + } + + def "should generate a regex for email [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.email()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'asd@asd.com' || true + 'a.b.' || false + } + + def "should generate a regex for url [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.url()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'ftp://asd.com:9090/asd/a?a=b' || true + 'a.b.' || false + } + + def "should generate a regex for a number [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.number()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + '1' || true + '1.0' || true + '0.1' || true + '.1' || true + '1.' || false + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/README.adoc new file mode 100644 index 0000000000..99ed7baee7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/README.adoc @@ -0,0 +1,54 @@ +=== Stub Runner Boot + +Spring Cloud Contract Verifier Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers. + +One of the usecases is to run some smoke (end to end) tests on a deployed application. You can read + more about this in the http://toomuchcoding.com/blog/2015/09/27/microservice-deployment/["Microservice Deployment" article at Too Much Coding blog.] + +==== How to use it? + +Just add the + +[source,groovy,indent=0] +---- +compile "org.springframework.cloud.contract:stub-runner-boot:${verifierVersion}" +---- + +and a messaging implementation: + +[source,groovy,indent=0] +---- +// for Apache Camel +compile "org.springframework.cloud.contract:spring-cloud-contract-stub-runner-camel:${verifierVersion}" +// for Spring Integration +compile "org.springframework.cloud.contract:spring-cloud-contract-stub-runner-integration:${verifierVersion}" +// for Spring Cloud Stream +compile "org.springframework.cloud.contract:spring-cloud-contract-stub-runner-stream:${verifierVersion}" +---- + +Build a fat-jar and you're ready to go! + +For the properties check the *Stub Runner Spring* section. + +==== Endpoints + +===== HTTP + +- GET `/stubs` - returns a list of all running stubs in `ivy:integer` notation +- GET `/stubs/{ivy}` - returns a port for the given `ivy` notation (when calling the endpoint `ivy` can also be `artifactId` only) + +===== Messaging + +For Messaging + +- GET `/triggers` - returns a list of all running labels in `ivy : [ label1, label2 ...]` notation +- POST `/triggers/{label}` - executes a trigger with `label` +- POST `/triggers/{ivy}/{label}` - executes a trigger with `label` for the given `ivy` notation (when calling the endpoint `ivy` can also be `artifactId` only) + +==== Example + +[source,groovy,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBootSpec.groovy[tags=boot_usage] +---- \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/build.gradle new file mode 100644 index 0000000000..a86573fc3a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +repositories { + mavenLocal() + jcenter() + maven { + url "http://repo.spring.io/snapshot" + } + maven { + url "http://repo.spring.io/milestone" + } +} + +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner-spring") + compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile project(":$stubRunner-root:$stubRunner-messaging-root:$stubRunner-stream") + testCompile 'com.jayway.restassured:spring-mock-mvc:2.9.0' + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/HttpStubsController.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/HttpStubsController.groovy new file mode 100644 index 0000000000..35730cd6ff --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/HttpStubsController.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.boot + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.contract.stubrunner.StubRunning +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * @author Marcin Grzejszczak + */ +@RestController +@RequestMapping("/stubs") +class HttpStubsController { + + private final StubRunning stubRunning + + @Autowired + HttpStubsController(StubRunning stubRunning) { + this.stubRunning = stubRunning + } + + @RequestMapping + Map stubs() { + return stubRunning.runStubs().toIvyToPortMapping() + } + + @RequestMapping(path = "/{ivy:.*}") + ResponseEntity stub(@PathVariable String ivy) { + Integer port = stubRunning.runStubs().getPort(ivy) + if (port) { + return ResponseEntity.ok(port) + } + return new ResponseEntity(HttpStatus.NOT_FOUND) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBoot.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBoot.groovy new file mode 100644 index 0000000000..daedaba6d6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBoot.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.boot + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.AutoConfigureAfter +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessageBuilder +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging +import org.springframework.context.annotation.Bean + +/** + * @author Marcin Grzejszczak + */ +@SpringBootApplication +@AutoConfigureAfter(StubRunnerConfiguration.class) +class StubRunnerBoot { + + static void main(String[] args) { + SpringApplication.run(StubRunnerBoot.class, args); + } + + @Bean + @ConditionalOnMissingBean + ContractVerifierMessaging noOpContractVerifierMessaging() { + return new NoOpContractVerifierMessaging() + } + + @Bean + @ConditionalOnMissingBean + ContractVerifierMessageBuilder noOpContractVerifierMessageBuilder() { + return new NoOpContractVerifierMessageBuilder() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/TriggerController.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/TriggerController.groovy new file mode 100644 index 0000000000..d06760166a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/groovy/org/springframework/cloud/contract/stubrunner/boot/TriggerController.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.boot + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import static org.springframework.web.bind.annotation.RequestMethod.GET +import static org.springframework.web.bind.annotation.RequestMethod.POST + +/** + * @author Marcin Grzejszczak + */ +@RestController +@RequestMapping("/triggers") +@Slf4j +class TriggerController { + + private final StubFinder stubFinder + + @Autowired + TriggerController(StubFinder stubFinder) { + this.stubFinder = stubFinder + } + + @RequestMapping(method = POST, path = "/{label:.*}") + ResponseEntity>> trigger(@PathVariable String label) { + return respond(label) { + stubFinder.trigger(label) + } + } + + @RequestMapping(method = POST, path = "/{ivyNotation:.*}/{label:.*}") + ResponseEntity>> triggerByArtifact(@PathVariable String ivyNotation, @PathVariable String label) { + return respond(label) { + stubFinder.trigger(ivyNotation, label) + } + } + + @RequestMapping(method = GET) + Map> labels() { + return stubFinder.labels() + } + + private ResponseEntity>> respond(String label, Closure closure) { + try { + closure() + return ResponseEntity.ok().body() + } catch (Exception e) { + log.debug("Exception occurred while trying to return [$label] label", e) + return new ResponseEntity>>(stubFinder.labels(), HttpStatus.NOT_FOUND) + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..31aea03047 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBootSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBootSpec.groovy new file mode 100644 index 0000000000..40a2e22f41 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/groovy/org/springframework/cloud/contract/stubrunner/boot/StubRunnerBootSpec.groovy @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.boot + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import groovy.json.JsonSlurper +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.stubrunner.StubRunning +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +// tag::boot_usage[] +@ContextConfiguration(classes = [StubRunnerBootSpec, StubRunnerBoot], loader = SpringApplicationContextLoader) +@EnableBinding +@Configuration +class StubRunnerBootSpec extends Specification { + + @Autowired StubRunning stubRunning + + def setup() { + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), + new TriggerController(stubRunning)) + } + + def 'should return a list of running stub servers in "full ivy:port" notation'() { + when: + String response = RestAssuredMockMvc.get('/stubs').body.asString() + then: + def root = new JsonSlurper().parseText(response) + root.'org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs' instanceof Integer + } + + def 'should return a port on which a [#stubId] stub is running'() { + when: + def response = RestAssuredMockMvc.get("/stubs/${stubId}") + then: + response.statusCode == 200 + response.body.as(Integer) > 0 + where: + stubId << ['org.springframework.cloud.contract.verifier.stubs:streamService:+:stubs', + 'org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs', + 'org.springframework.cloud.contract.verifier.stubs:streamService:+', + 'org.springframework.cloud.contract.verifier.stubs:streamService', + 'streamService'] + } + + def 'should return 404 when missing stub was called'() { + when: + def response = RestAssuredMockMvc.get("/stubs/a:b:c:d") + then: + response.statusCode == 404 + } + + def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() { + when: + String response = RestAssuredMockMvc.get('/triggers').body.asString() + then: + def root = new JsonSlurper().parseText(response) + root.'org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"]) + } + + def 'should trigger a messaging label'() { + given: + StubRunning stubRunning = Mock() + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning)) + when: + def response = RestAssuredMockMvc.post("/triggers/delete_book") + then: + response.statusCode == 200 + and: + 1 * stubRunning.trigger('delete_book') + } + + def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() { + given: + StubRunning stubRunning = Mock() + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning)) + when: + def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book") + then: + response.statusCode == 200 + and: + 1 * stubRunning.trigger(stubId, 'delete_book') + where: + stubId << ['org.springframework.cloud.contract.verifier.stubs:streamService:stubs', 'org.springframework.cloud.contract.verifier.stubs:streamService', 'streamService'] + } + + def 'should return when trigger is missing'() { + when: + def response = RestAssuredMockMvc.post("/triggers/missing_label") + then: + response.statusCode == 404 + def root = new JsonSlurper().parseText(response.body.asString()) + root.'org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"]) + } + +} +// end::boot_usage[] diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/application.yml new file mode 100644 index 0000000000..9b34d9d617 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/application.yml @@ -0,0 +1,2 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:streamService \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..8613d57be0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml new file mode 100644 index 0000000000..001cf26899 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/README.adoc new file mode 100644 index 0000000000..6fa5144dd0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/README.adoc @@ -0,0 +1,65 @@ +=== Stub Runner JUnit Rule + +Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id: + +[source,java,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java[tags=classrule] +---- + +After that rule gets executed Stub Runner connects to your Maven repository and for the given list of dependencies tries to: + +- download them +- cache them locally +- unzip them to a temporary folder +- start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port +- feed the WireMock server with all JSON files that are valid WireMock definitions + +Stub Runner uses https://wiki.eclipse.org/Aether[Eclipse Aether] mechanism to download the Maven dependencies. +Check their https://wiki.eclipse.org/Aether[docs] for more information. + +Since the `StubRunnerRule` implements the `StubFinder` it allows you to find the started stubs: + +[source,groovy,indent=0] +---- +include::../spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubFinder.groovy[] +---- + +Example of usage in Spock tests: + +[source,groovy,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy[tags=classrule] +---- + +Example of usage in JUnit tests: + +[source,java,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java[tags=test] +---- + +Check the *Common properties for JUnit and Spring* for more information on how to apply global configuration of Stub Runner. + +==== Providing fixed ports + +You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule. + +==== Fluent API + +When using the `StubRunnerRule` you can add a stub to download and then pass the port for the last downloaded stub. + +[source,java,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java[tags=classrule_with_port] +---- + +You can see that for this example the following test is valid: + +[source,java,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java[tags=test_with_port] +---- + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/build.gradle new file mode 100644 index 0000000000..3349027ab9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/build.gradle @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = 'JUnit rule for stub-runner' + +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner") + + compile localGroovy() + compile 'junit:junit:4.12' + + testCompile('org.spockframework:spock-core:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'cglib:cglib-nodep:2.2' + testCompile 'org.objenesis:objenesis:2.1' + testCompile 'ch.qos.logback:logback-classic:1.1.3' + testCompile 'org.assertj:assertj-core:2.3.0' + testCompile 'org.apache.commons:commons-io:1.3.2' +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/main/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/main/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java new file mode 100644 index 0000000000..f8067e292d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/main/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java @@ -0,0 +1,223 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.cloud.contract.stubrunner.BatchStubRunner; +import org.springframework.cloud.contract.stubrunner.BatchStubRunnerFactory; +import org.springframework.cloud.contract.stubrunner.RunningStubs; +import org.springframework.cloud.contract.stubrunner.StubConfiguration; +import org.springframework.cloud.contract.stubrunner.StubFinder; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptions; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder; +import org.springframework.cloud.contract.spec.Contract; + +/** + * JUnit class rule that allows you to download the provided stubs. + * + * @author Marcin Grzejszczak + */ +public class StubRunnerRule implements TestRule, StubFinder { + private static final String DELIMITER = ":"; + private static final String LATEST_VERSION = "+"; + + private StubRunnerOptionsBuilder stubRunnerOptionsBuilder = new StubRunnerOptionsBuilder(defaultStubRunnerOptions()); + private BatchStubRunner stubFinder; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + before(); + base.evaluate(); + stubFinder.close(); + } + + private void before() { + stubFinder = new BatchStubRunnerFactory(stubRunnerOptionsBuilder.build()).buildBatchStubRunner(); + stubFinder.runStubs(); + } + }; + } + + private StubRunnerOptions defaultStubRunnerOptions() { + return new StubRunnerOptionsBuilder() + .withMinPort(Integer.valueOf(System.getProperty("stubrunner.port.range.min", "10000"))) + .withMaxPort(Integer.valueOf(System.getProperty("stubrunner.port.range.max", "15000"))) + .withStubRepositoryRoot(System.getProperty("stubrunner.stubs.repository.root", "")) + .withWorkOffline(Boolean.parseBoolean(System.getProperty("stubrunner.work-offline", "false"))) + .withStubsClassifier(System.getProperty("stubrunner.stubs.classifier", "stubs")) + .withStubs(System.getProperty("stubrunner.stubs.ids", "")) + .build(); + } + + /** + * Override all options + * + * @see StubRunnerOptions + */ + public StubRunnerRule options(StubRunnerOptions stubRunnerOptions) { + stubRunnerOptionsBuilder.withOptions(stubRunnerOptions); + return this; + } + + /** + * Min value of port for WireMock server + */ + public StubRunnerRule minPort(int minPort) { + stubRunnerOptionsBuilder.withMinPort(minPort); + return this; + } + + /** + * Max value of port for WireMock server + */ + public StubRunnerRule maxPort(int maxPort) { + stubRunnerOptionsBuilder.withMaxPort(maxPort); + return this; + } + + /** + * String URI of repository containing stubs + */ + public StubRunnerRule repoRoot(String repoRoot) { + stubRunnerOptionsBuilder.withStubRepositoryRoot(repoRoot); + return this; + } + + /** + * Should download stubs or use only the local repository + */ + public StubRunnerRule workOffline(boolean workOffline) { + stubRunnerOptionsBuilder.withWorkOffline(workOffline); + return this; + } + + /** + * Group Id, artifact Id, version and classifier of a single stub to download + */ + public StubRunnerRule downloadStub(String groupId, String artifactId, String version, String classifier) { + stubRunnerOptionsBuilder.withStubs(groupId + DELIMITER + artifactId + DELIMITER + version + DELIMITER + classifier); + return this; + } + + /** + * Group Id, artifact Id and classifier of a single stub to download in the latest version + */ + public StubRunnerRule downloadLatestStub(String groupId, String artifactId, String classifier) { + stubRunnerOptionsBuilder.withStubs(groupId + DELIMITER + artifactId + DELIMITER + LATEST_VERSION + DELIMITER + classifier); + return this; + } + + /** + * Group Id, artifact Id and version of a single stub to download + */ + public StubRunnerRule downloadStub(String groupId, String artifactId, String version) { + stubRunnerOptionsBuilder.withStubs(groupId + DELIMITER + artifactId + DELIMITER + version); + return this; + } + + /** + * Group Id, artifact Id of a single stub to download. Default classifier will be picked. + */ + public StubRunnerRule downloadStub(String groupId, String artifactId) { + stubRunnerOptionsBuilder.withStubs(groupId + DELIMITER + artifactId); + return this; + } + + /** + * Ivy notation of a single stub to download. + */ + public StubRunnerRule downloadStub(String ivyNotation) { + stubRunnerOptionsBuilder.withStubs(ivyNotation); + return this; + } + + /** + * Stubs to download in Ivy notations + */ + public StubRunnerRule downloadStubs(String... ivyNotations) { + stubRunnerOptionsBuilder.withStubs(Arrays.asList(ivyNotations)); + return this; + } + + /** + * Stubs to download in Ivy notations + */ + public StubRunnerRule downloadStubs(List ivyNotations) { + stubRunnerOptionsBuilder.withStubs(ivyNotations); + return this; + } + + /** + * Appends port to last added stub + */ + public StubRunnerRule withPort(Integer port) { + stubRunnerOptionsBuilder.withPort(port); + return this; + } + + @Override + public URL findStubUrl(String groupId, String artifactId) { + return stubFinder.findStubUrl(groupId, artifactId); + } + + @Override + public URL findStubUrl(String ivyNotation) { + return stubFinder.findStubUrl(ivyNotation); + } + + @Override + public RunningStubs findAllRunningStubs() { + return stubFinder.findAllRunningStubs(); + } + + @Override + public Map> getContracts() { + return stubFinder.getContracts(); + } + + @Override + public boolean trigger(String ivyNotation, String labelName) { + return stubFinder.trigger(ivyNotation, labelName); + } + + @Override + public boolean trigger(String labelName) { + return stubFinder.trigger(labelName); + } + + @Override + public boolean trigger() { + return stubFinder.trigger(); + } + + @Override + public Map> labels() { + return stubFinder.labels(); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java new file mode 100644 index 0000000000..06519312b3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit; + +import java.io.InputStream; +import java.net.URI; + +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.BDDAssertions; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +public class StubRunnerRuleCustomPortJUnitTest { + + @BeforeClass + @AfterClass + public static void setupProps() { + System.getProperties().setProperty("stubrunner.stubs.repository.root", ""); + System.getProperties().setProperty("stubrunner.stubs.classifier", "stubs"); + } + + // tag::classrule_with_port[] + @ClassRule public static StubRunnerRule rule = new StubRunnerRule() + .repoRoot(repoRoot()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance") + .withPort(12345) + .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346"); + // end::classrule_with_port[] + + @Test + public void should_start_wiremock_servers() throws Exception { + // expect: 'WireMocks are running' + then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")); + then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isNotNull(); + // and: + BDDAssertions.then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue(); + BDDAssertions.then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs", "fraudDetectionServer")).isTrue(); + BDDAssertions.then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isTrue(); + // and: 'Stubs were registered' + then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance"); + then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer"); + // and: 'The port is fixed' + // tag::test_with_port[] + then(rule.findStubUrl("loanIssuance")).isEqualTo(URI.create("http://localhost:12345").toURL()); + then(rule.findStubUrl("fraudDetectionServer")).isEqualTo(URI.create("http://localhost:12346").toURL()); + // end::test_with_port[] + } + + private static String repoRoot() { + try { + return StubRunnerRuleCustomPortJUnitTest.class.getResource("/m2repo/repository/").toURI().toString(); + } catch (Exception e) { + return ""; + } + } + + private String httpGet(String url) throws Exception { + try(InputStream stream = URI.create(url).toURL().openStream()) { + return IOUtils.toString(stream); + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java new file mode 100644 index 0000000000..e5ae1d1c15 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit; + +import java.io.InputStream; +import java.net.URI; + +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.BDDAssertions; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +public class StubRunnerRuleJUnitTest { + + @BeforeClass + @AfterClass + public static void setupProps() { + System.getProperties().setProperty("stubrunner.stubs.repository.root", ""); + System.getProperties().setProperty("stubrunner.stubs.classifier", "stubs"); + } + + // tag::classrule[] + @ClassRule public static StubRunnerRule rule = new StubRunnerRule() + .repoRoot(repoRoot()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance") + .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"); + // end::classrule[] + + // tag::test[] + @Test + public void should_start_wiremock_servers() throws Exception { + // expect: 'WireMocks are running' + then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")); + then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isNotNull(); + // and: + BDDAssertions.then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue(); + BDDAssertions.then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs", "fraudDetectionServer")).isTrue(); + BDDAssertions.then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isTrue(); + // and: 'Stubs were registered' + then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance"); + then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer"); + } + // end::test[] + + private static String repoRoot() { + try { + return StubRunnerRuleJUnitTest.class.getResource("/m2repo/repository/").toURI().toString(); + } catch (Exception e) { + return ""; + } + } + + private String httpGet(String url) throws Exception { + try(InputStream stream = URI.create(url).toURL().openStream()) { + return IOUtils.toString(stream); + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy new file mode 100644 index 0000000000..3399d25ea0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit + +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.ClassRule +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerRuleSpec extends Specification { + + @BeforeClass + @AfterClass + void setupProps() { + System.getProperties().setProperty("stubrunner.stubs.repository.root", ""); + System.getProperties().setProperty("stubrunner.stubs.classifier", "stubs"); + } + + // tag::classrule[] + @ClassRule @Shared StubRunnerRule rule = new StubRunnerRule() + .repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance") + .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer") + + def 'should start WireMock servers'() { + expect: 'WireMocks are running' + rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null + rule.findStubUrl('loanIssuance') != null + rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') + rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null + and: + rule.findAllRunningStubs().isPresent('loanIssuance') + rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer') + rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') + and: 'Stubs were registered' + "${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + } + // end::classrule[] +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/logback.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/logback.xml new file mode 100644 index 0000000000..7eecabf2f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..40610c8cdb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml new file mode 100644 index 0000000000..281b9b3b80 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..59db343686 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml new file mode 100644 index 0000000000..615428dae6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062111 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/README.adoc new file mode 100644 index 0000000000..3fbd56f5dd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/README.adoc @@ -0,0 +1,123 @@ +:input_name: jms:input +:output_name: jms:output + +=== Stub Runner Camel + +Spring Cloud Contract Verifier Stub Runner's messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes. + +==== Adding it to the project + +To use it you have to add the following dependency to your project (example for Gradle): + +[source,groovy,indent=0] +---- +testCompile "org.springframework.cloud.contract:stub-runner-camel:${verifierVersion}" +---- + +==== Examples + +===== Stubs structure + +Let us assume that we have the following Maven repository with a deployed stubs for the +`camelService` application. + +[source,bash,indent=0] +---- +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── camelService + ├── 0.0.1-SNAPSHOT + │   ├── camelService-0.0.1-SNAPSHOT.pom + │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +---- + +And the stubs contain the following structure: + +[source,bash,indent=0] +---- +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +---- + +Let's consider the following contracts (let' number it with *1*): + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl,indent=0] +---- + +and number *2* + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0] +---- + +===== Scenario 1 (no input message) + +So as to trigger a message via the `return_book_1` label we'll use the `StubTigger` interface as follows + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_message,indent=0] +---- + +===== Scenario 2 (output triggered by input) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_send,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive_message,indent=0] +---- + +===== Scenario 3 (input with no output) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_no_output,indent=0] +---- diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/build.gradle new file mode 100644 index 0000000000..4aac34bc8e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/build.gradle @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner-spring") + compile project(":$verifier-root:$verifier-messaging-root:$verifier-camel") + compile "org.apache.camel:camel-spring-boot-starter:${camelVersion}" + compile "org.apache.camel:camel-jackson:${camelVersion}" + + testCompile "org.springframework:spring-beans:${springVersion}" + testCompile "org.apache.camel:camel-jms:${camelVersion}" + testCompile 'org.apache.activemq:activemq-camel:5.12.1' + testCompile 'org.apache.activemq:activemq-pool:5.12.1' + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.groovy new file mode 100644 index 0000000000..9acb8502c2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import org.apache.camel.RoutesBuilder +import org.apache.camel.spring.SpringRouteBuilder +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.BatchStubRunner +import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +/** + * Camel configuration that iterates over the downloaded Groovy DSLs + * and registers a route for each DSL. + * + * @author Marcin Grzejszczak + */ +@Configuration +class StubRunnerCamelConfiguration { + + @Bean + RoutesBuilder myRouter(BatchStubRunner batchStubRunner) { + return new SpringRouteBuilder() { + @Override + public void configure() throws Exception { + Map> contracts = batchStubRunner.contracts + (contracts.values().flatten() as Collection).findAll { it?.input?.messageFrom?.clientValue && it?.outputMessage?.sentTo }.each { + from(it.input.messageFrom.clientValue) + .filter(new StubRunnerCamelPredicate(it)) + .process(new StubRunnerCamelProcessor(it)) + .to(it.outputMessage.sentTo.clientValue) + } + } + }; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.groovy new file mode 100644 index 0000000000..3e5dc0448c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.groovy @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import com.toomuchcoding.jsonassert.JsonVerifiable +import groovy.transform.PackageScope +import org.springframework.cloud.contract.spec.Contract +import org.apache.camel.Exchange +import org.apache.camel.Predicate +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter + +import java.util.regex.Pattern + +/** + * Passes through a message that matches the one defined in the DSL + * + * @author Marcin Grzejszczak + */ +@PackageScope +class StubRunnerCamelPredicate implements Predicate { + + private final Contract groovyDsl + private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper() + + StubRunnerCamelPredicate(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + boolean matches(Exchange exchange) { + if(!headersMatch(exchange)){ + return false + } + Object inputMessage = exchange.in.body + JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValues(groovyDsl.input.messageBody) + DocumentContext parsedJson = JsonPath.parse(objectMapper.writeValueAsString(inputMessage)) + return jsonPaths.every { matchesJsonPath(parsedJson, it) } + } + + private boolean matchesJsonPath(DocumentContext parsedJson, JsonVerifiable jsonVerifiable) { + try { + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonVerifiable.jsonPath()) + return true + } catch (Exception e) { + return false + } + } + + private boolean headersMatch(Exchange exchange) { + Map headers = exchange.getIn().getHeaders() + return groovyDsl.input.messageHeaders.entries.every { + String name = it.name + Object value = it.clientValue + Object valueInHeader = headers.get(name) + return value instanceof Pattern ? + value.matcher(valueInHeader.toString()).matches() : + valueInHeader == value + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.groovy new file mode 100644 index 0000000000..acdf70191b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import groovy.transform.PackageScope +import org.springframework.cloud.contract.verifier.util.BodyExtractor +import org.apache.camel.Exchange +import org.apache.camel.Message +import org.apache.camel.Processor +import org.springframework.cloud.contract.spec.Contract + +/** + * Sends forward a message defined in the DSL. Also removes headers from the + * input message and provides the headers from the DSL. + * + * @author Marcin Grzejszczak + */ +@PackageScope +class StubRunnerCamelProcessor implements Processor { + + private final Contract groovyDsl + + StubRunnerCamelProcessor(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + void process(Exchange exchange) throws Exception { + Message input = exchange.in + groovyDsl.input.messageHeaders?.entries?.each { + input.removeHeader(it.name) + } + if (!groovyDsl.outputMessage) { + return + } + input.body = BodyExtractor.extractStubValueFrom(groovyDsl.outputMessage.body) + groovyDsl.outputMessage.headers?.entries?.each { + input.setHeader(it.name, it.clientValue) + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..26adb8c16c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.messaging.camel.StubRunnerCamelConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy new file mode 100644 index 0000000000..72a689a659 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode + +@CompileStatic +@EqualsAndHashCode +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy new file mode 100644 index 0000000000..544ca961b3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy @@ -0,0 +1,227 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.apache.activemq.camel.component.ActiveMQComponent +import org.apache.camel.CamelContext +import org.apache.camel.Exchange +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification +/** + * @author Marcin Grzejszczak + */ +@Configuration +@ComponentScan +@EnableAutoConfiguration +@ContextConfiguration(classes = CamelStubRunnerSpec, loader = SpringApplicationContextLoader) +class CamelStubRunnerSpec extends Specification { + + @Autowired StubFinder stubFinder + @Autowired CamelContext camelContext + + def setup() { + // ensure that message were taken from the queue + camelContext.createConsumerTemplate().receive('jms:output', 100) + } + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header']) + // end::client_send[] + then: + // tag::client_receive[] + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000) + // end::client_receive[] + and: + // tag::client_receive_message[] + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + // end::client_receive_message[] + } + + def 'should trigger a message by label'() { + when: + // tag::client_trigger[] + stubFinder.trigger('return_book_1') + // end::client_trigger[] + then: + // tag::client_trigger_receive[] + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000) + // end::client_trigger_receive[] + and: + // tag::client_trigger_message[] + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + // end::client_trigger_message[] + } + + def 'should trigger a label for the existing groupId:artifactId'() { + when: + // tag::trigger_group_artifact[] + stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:camelService', 'return_book_1') + // end::trigger_group_artifact[] + then: + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label for the existing artifactId'() { + when: + // tag::trigger_artifact[] + stubFinder.trigger('camelService', 'return_book_1') + // end::trigger_artifact[] + then: + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should throw an exception when missing label is passed'() { + when: + stubFinder.trigger('missing label') + then: + thrown(IllegalArgumentException) + } + + def 'should throw an exception when missing label and artifactid is passed'() { + when: + stubFinder.trigger('some:service', 'return_book_1') + then: + thrown(IllegalArgumentException) + } + + def 'should trigger messages by running all triggers'() { + when: + // tag::trigger_all[] + stubFinder.trigger() + // end::trigger_all[] + then: + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label with no output message'() { + when: + // tag::trigger_no_output[] + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header']) + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + def 'should not trigger a message that does not match input'() { + when: + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value']) + then: + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 100) + and: + receivedMessage == null + } + + private boolean assertThatBodyContainsBookNameFoo(Object payload) { + String objectAsString = payload instanceof String ? payload : + JsonOutput.toJson(payload) + def json = new JsonSlurper().parseText(objectAsString) + return json.bookName == 'foo' + } + + @Bean + ActiveMQComponent activeMQComponent(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) { + return new ActiveMQComponent(brokerURL: url) + } + + Contract dsl = + // tag::sample_dsl[] + Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('jms:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl[] + + Contract dsl2 = + // tag::sample_dsl_2[] + Contract.make { + label 'return_book_2' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl_2[] + + Contract dsl3 = + // tag::sample_dsl_3[] + Contract.make { + label 'delete_book' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + // end::sample_dsl_3[] +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy new file mode 100644 index 0000000000..279782c65e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy @@ -0,0 +1,98 @@ +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import org.apache.camel.CamelContext +import org.apache.camel.Exchange +import org.apache.camel.builder.ExchangeBuilder +import org.apache.camel.spring.SpringCamelContext +import org.springframework.cloud.contract.spec.Contract +import spock.lang.Specification + +class StubRunnerCamelProcessorSpec extends Specification { + + CamelContext camelContext = new SpringCamelContext() + Exchange message = ExchangeBuilder.anExchange(camelContext).build() + + def noOutputMessageContract = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + } + + def 'should not process the message if there is no output message'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(noOutputMessageContract) + when: + processor.process(message) + then: + noExceptionThrown() + } + + def dsl = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('returnBook') + body([ + responseId: $(producer(regex('[0-9]+')), consumer('123')) + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + def 'should process message when it has an output message section'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(dsl) + when: + processor.process(message) + then: + message.getIn().getBody(String) == '{"responseId":"123"}' + } + + def dslWithRegexInGString = Contract.make { + // Human readable description + description 'Should produce valid sensor data' + // Label by means of which the output message can be triggered + label 'sensor1' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createSensorData()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'sensor-data' + headers { + header('contentType': 'application/json') + } + // the body of the output message + body("""{"id":"${value(producer(regex('[0-9]+')), consumer('99'))}","temperature":"123.45"}""") + } + } + + def 'should convert dsl into message with regex in GString'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(dslWithRegexInGString) + when: + processor.process(message) + then: + message.getIn().getBody(String) == '''{"id":"99","temperature":"123.45"}''' + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml new file mode 100644 index 0000000000..db021bb201 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml @@ -0,0 +1,2 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:camelService \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..0b56465de4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..8ac2715bc8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + true + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml new file mode 100644 index 0000000000..237fd747a9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml new file mode 100644 index 0000000000..237fd747a9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/README.adoc new file mode 100644 index 0000000000..89f5a0a180 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/README.adoc @@ -0,0 +1,131 @@ +:input_name: input +:output_name: output + +=== Stub Runner Integration + +Spring Cloud Contract Verifier Stub Runner's messaging module gives you an easy way to integrate with Spring Integration. +For the provided artifacts it will automatically download the stubs and register the required +routes. + +==== Adding it to the project + +To use it you have to add the following dependency to your project (example for Gradle): + +[source,groovy,indent=0] +---- +testCompile "org.springframework.cloud.contract:stub-runner-integration:${verifierVersion}" +---- + +==== Examples + +===== Stubs structure + +Let us assume that we have the following Maven repository with a deployed stubs for the +`integrationService` application. + +[source,bash,indent=0] +---- +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── integrationService + ├── 0.0.1-SNAPSHOT + │   ├── integrationService-0.0.1-SNAPSHOT.pom + │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +---- + +And the stubs contain the following structure: + +[source,bash,indent=0] +---- +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +---- + +Let's consider the following contracts (let' number it with *1*): + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl,indent=0] +---- + +and number *2* + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0] +---- + +and the following Spring Integration Route: + +[source,xml] +---- +include::src/test/resources/integration-context.xml[] +---- + + +===== Scenario 1 (no input message) + +So as to trigger a message via the `return_book_1` label we'll use the `StubTigger` interface as follows + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_message,indent=0] +---- + +===== Scenario 2 (output triggered by input) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_send,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive_message,indent=0] +---- + +===== Scenario 3 (input with no output) + +Since the route is set for you it's enough to just send a message to the `{input_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=trigger_no_output,indent=0] +---- diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/build.gradle new file mode 100644 index 0000000000..714d543a31 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/build.gradle @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner-spring") + compile project(":$verifier-root:$verifier-messaging-root:$verifier-integration") + compile "org.springframework.integration:spring-integration-java-dsl:${springIntegrationDslVersion}" + compile 'com.fasterxml.jackson.core:jackson-databind:2.7.0' + + testCompile "org.springframework.boot:spring-boot-starter-integration:${springBootVersion}" + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationConfiguration.groovy new file mode 100644 index 0000000000..70f082d629 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationConfiguration.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.beans.factory.config.AutowireCapableBeanFactory +import org.springframework.cloud.contract.stubrunner.BatchStubRunner +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration +import org.springframework.context.Lifecycle +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.integration.dsl.FilterEndpointSpec +import org.springframework.integration.dsl.GenericEndpointSpec +import org.springframework.integration.dsl.IntegrationFlowBuilder +import org.springframework.integration.dsl.IntegrationFlows +import org.springframework.messaging.Message + +/** + * Spring Integration configuration that iterates over the downloaded Groovy DSLs + * and registers a flow for each DSL. + * + * @author Marcin Grzejszczak + */ +@Configuration +@Import(StubRunnerConfiguration) +@CompileStatic +class StubRunnerIntegrationConfiguration { + + @Bean + FlowRegistrar flowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) { + Map> contracts = batchStubRunner.contracts + contracts.each { StubConfiguration key, Collection value -> + String name = "${key.groupId}_${key.artifactId}" + value.findAll { it?.input?.messageFrom?.clientValue }.each { Contract dsl -> + String flowName = "${name}_${dsl.label}_${dsl.hashCode()}" + IntegrationFlowBuilder builder = IntegrationFlows.from(dsl.input.messageFrom.clientValue) + .filter(new StubRunnerIntegrationMessageSelector(dsl), { FilterEndpointSpec e -> e.id("${flowName}.filter") } ) + .transform(new StubRunnerIntegrationTransformer(dsl), { GenericEndpointSpec e -> e.id("${flowName}.transformer") }) + if (dsl.outputMessage?.sentTo) { + builder = builder.channel(dsl.outputMessage.sentTo.clientValue) + } else { + builder = builder.handle(new DummyMessageHandler(), "handle") + } + beanFactory.initializeBean(builder.get(), flowName) + beanFactory.getBean("${flowName}.filter", Lifecycle.class).start(); + beanFactory.getBean("${flowName}.transformer", Lifecycle.class).start(); + } + } + return new FlowRegistrar() + } + + @CompileStatic + private static class DummyMessageHandler { + void handle(Message message) {} + } + + static class FlowRegistrar {} +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.groovy new file mode 100644 index 0000000000..ff4413d5d5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import com.toomuchcoding.jsonassert.JsonVerifiable +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.integration.core.MessageSelector +import org.springframework.messaging.Message + +import java.util.regex.Pattern +/** + * Passes through a message that matches the one defined in the DSL + * + * @author Marcin Grzejszczak + */ +@CompileStatic +class StubRunnerIntegrationMessageSelector implements MessageSelector { + + private final Contract groovyDsl + private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper() + + StubRunnerIntegrationMessageSelector(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + boolean accept(Message message) { + if(!headersMatch(message)){ + return false + } + Object inputMessage = message.getPayload() + JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValues(groovyDsl.input.messageBody) + DocumentContext parsedJson = JsonPath.parse(objectMapper.writeValueAsString(inputMessage)) + return jsonPaths.every { matchesJsonPath(parsedJson, it) } + } + + private boolean matchesJsonPath(DocumentContext parsedJson, JsonVerifiable jsonVerifiable) { + try { + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonVerifiable.jsonPath()) + return true + } catch (Exception e) { + return false + } + } + + private boolean headersMatch(Message message) { + Map headers = message.getHeaders() + return groovyDsl.input.messageHeaders.entries.every { + String name = it.name + Object value = it.clientValue + Object valueInHeader = headers.get(name) + return value instanceof Pattern ? + value.matcher(valueInHeader.toString()).matches() : + valueInHeader == value + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformer.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformer.groovy new file mode 100644 index 0000000000..0bf9278b01 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformer.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.util.BodyExtractor +import org.springframework.integration.transformer.GenericTransformer +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder + +/** + * Sends forward a message defined in the DSL. + * + * @author Marcin Grzejszczak + */ +class StubRunnerIntegrationTransformer implements GenericTransformer, Message> { + + private final Contract groovyDsl + + StubRunnerIntegrationTransformer(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + Message transform(Message source) { + if (!groovyDsl.outputMessage) { + return source + } + String payload = BodyExtractor.extractStubValueFrom(groovyDsl.outputMessage.body) + Map headers = groovyDsl.outputMessage.headers.asStubSideMap() + return MessageBuilder.createMessage(payload, new MessageHeaders(headers)) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..a69b005443 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.messaging.integration.StubRunnerIntegrationConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/BookReturned.groovy new file mode 100644 index 0000000000..bbb47bbb60 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/BookReturned.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode + +@CompileStatic +@EqualsAndHashCode +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy new file mode 100644 index 0000000000..bdd7368377 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy @@ -0,0 +1,224 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.ImportResource +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import java.util.concurrent.TimeUnit +/** + * @author Marcin Grzejszczak + */ +@Configuration +@ComponentScan +@EnableAutoConfiguration +@ContextConfiguration(classes = IntegrationStubRunnerSpec, loader = SpringApplicationContextLoader) +@ImportResource("classpath*:integration-context.xml") +class IntegrationStubRunnerSpec extends Specification { + + @Autowired StubFinder stubFinder + @Autowired ContractVerifierMessaging messaging + + def setup() { + // ensure that message were taken from the queue + messaging.receiveMessage('outputTest', 100, TimeUnit.MILLISECONDS) + } + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'input') + // end::client_send[] + then: + // tag::client_receive[] + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest') + // end::client_receive[] + and: + // tag::client_receive_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_receive_message[] + } + + def 'should trigger a message by label'() { + when: + // tag::client_trigger[] + stubFinder.trigger('return_book_1') + // end::client_trigger[] + then: + // tag::client_trigger_receive[] + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest') + // end::client_trigger_receive[] + and: + // tag::client_trigger_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_trigger_message[] + } + + def 'should trigger a label for the existing groupId:artifactId'() { + when: + // tag::trigger_group_artifact[] + stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:integrationService', 'return_book_1') + // end::trigger_group_artifact[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label for the existing artifactId'() { + when: + // tag::trigger_artifact[] + stubFinder.trigger('integrationService', 'return_book_1') + // end::trigger_artifact[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should throw exception when missing label is passed'() { + when: + stubFinder.trigger('missing label') + then: + thrown(IllegalArgumentException) + } + + def 'should throw exception when missing label and artifactid is passed'() { + when: + stubFinder.trigger('some:service', 'return_book_1') + then: + thrown(IllegalArgumentException) + } + + def 'should trigger messages by running all triggers'() { + when: + // tag::trigger_all[] + stubFinder.trigger() + // end::trigger_all[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label with no output message'() { + when: + // tag::trigger_no_output[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete') + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + def 'should not trigger a message that does not match input'() { + when: + messaging.send(new BookReturned('not_matching'), [wrong: 'header_value'], 'input') + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('outputTest', 100, TimeUnit.MILLISECONDS) + and: + receivedMessage == null + } + + private boolean assertJsons(Object payload) { + String objectAsString = payload instanceof String ? payload : + JsonOutput.toJson(payload) + def json = new JsonSlurper().parseText(objectAsString) + return json.bookName == 'foo' + } + + Contract dsl = + // tag::sample_dsl[] + Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl[] + + Contract dsl2 = + // tag::sample_dsl_2[] + Contract.make { + label 'return_book_2' + input { + messageFrom('input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl_2[] + + Contract dsl3 = + // tag::sample_dsl_3[] + Contract.make { + label 'delete_book' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + // end::sample_dsl_3[] + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformerSpec.groovy new file mode 100644 index 0000000000..31b4d19ab4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationTransformerSpec.groovy @@ -0,0 +1,95 @@ +package org.springframework.cloud.contract.stubrunner.messaging.integration + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import spock.lang.Specification + +class StubRunnerIntegrationTransformerSpec extends Specification { + + Message message = MessageBuilder.withPayload("hello").build() + + def noOutputMessageContract = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + } + + def 'should not transform the message if there is no output message'() { + given: + StubRunnerIntegrationTransformer transformer = new StubRunnerIntegrationTransformer(noOutputMessageContract) + when: + def result = transformer.transform(message) + then: + result.is(message) + } + + def dsl = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('returnBook') + body([ + responseId: $(producer(regex('[0-9]+')), consumer('123')) + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + def 'should convert dsl into message'() { + given: + StubRunnerIntegrationTransformer transformer = new StubRunnerIntegrationTransformer(dsl) + when: + def result = transformer.transform(message) + then: + result.payload == '{"responseId":"123"}' + } + + def dslWithRegexInGString = Contract.make { + // Human readable description + description 'Should produce valid sensor data' + // Label by means of which the output message can be triggered + label 'sensor1' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createSensorData()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'sensor-data' + headers { + header('contentType': 'application/json') + } + // the body of the output message + body("""{"id":"${value(producer(regex('[0-9]+')), consumer('99'))}","temperature":"123.45"}""") + } + } + + def 'should convert dsl into message with regex in GString'() { + given: + StubRunnerIntegrationTransformer transformer = new StubRunnerIntegrationTransformer(dslWithRegexInGString) + when: + def result = transformer.transform(message) + then: + result.payload == '''{"id":"99","temperature":"123.45"}''' + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/application.yml new file mode 100644 index 0000000000..911b24bbaa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/application.yml @@ -0,0 +1,2 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:integrationService:0.0.1-SNAPSHOT \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/integration-context.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/integration-context.xml new file mode 100644 index 0000000000..1fdb1bb847 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/integration-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/integrationService-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/integrationService-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..0581ac022d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/integrationService-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + integrationService + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..e6c62c5897 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + integrationService + 0.0.1-SNAPSHOT + + + true + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata-local.xml new file mode 100644 index 0000000000..f619bafb54 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + integrationService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata.xml new file mode 100644 index 0000000000..f619bafb54 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + integrationService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/README.adoc new file mode 100644 index 0000000000..e93eaeb43b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/README.adoc @@ -0,0 +1,132 @@ +=== Stub Runner Stream + +Spring Cloud Contract Verifier Stub Runner's messaging module gives you an easy way to integrate with Spring Stream. +For the provided artifacts it will automatically download the stubs and register the required +routes. + +WARNING: In Stub Runner's integration with Stream the `messageFrom` or `sentTo` Strings are resolved +first as a `destination` of a channel, and then if there is no such `destination` it's resolved as a +channel name. + +==== Adding it to the project + +To use it you have to add the following dependency to your project (example for Gradle): + +[source,groovy,indent=0] +---- +testCompile "org.springframework.cloud.contract:stub-runner-stream:${verifierVersion}" +---- + +==== Examples + +===== Stubs structure + +Let us assume that we have the following Maven repository with a deployed stubs for the +`streamService` application. + +[source,bash,indent=0] +---- +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── streamService + ├── 0.0.1-SNAPSHOT + │   ├── streamService-0.0.1-SNAPSHOT.pom + │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +---- + +And the stubs contain the following structure: + +[source,bash,indent=0] +---- +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +---- + +Let's consider the following contracts (let' number it with *1*): + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl,indent=0] +---- + +and number *2* + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0] +---- + +and the following Spring configuration: + +[source,yaml] +---- +include::src/test/resources/application.yml[] +---- + + +===== Scenario 1 (no input message) + +So as to trigger a message via the `return_book_1` label we'll use the `StubTrigger` interface as follows + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +Next we'll want to listen to the output of the message sent to a channel whose `destination` is `returnBook` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_message,indent=0] +---- + +===== Scenario 2 (output triggered by input) + +Since the route is set for you it's enough to just send a message to the `bookStorage` `destination`. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_send,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `returnBook` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive_message,indent=0] +---- + +===== Scenario 3 (input with no output) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=trigger_no_output,indent=0] +---- diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/build.gradle new file mode 100644 index 0000000000..e211645f41 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/build.gradle @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner-spring") + compile project(":$verifier-root:$verifier-messaging-root:$verifier-stream") + compile "org.springframework.integration:spring-integration-java-dsl:${springIntegrationDslVersion}" + + testCompile "org.springframework:spring-context:${springVersion}" + testCompile "org.springframework:spring-beans:${springVersion}" + testCompile "org.springframework.cloud:spring-cloud-stream-test-support:${springStreamVersion}" + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamConfiguration.groovy new file mode 100644 index 0000000000..f6c1eb7ceb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamConfiguration.groovy @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import groovy.transform.CompileStatic +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.config.AutowireCapableBeanFactory +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.BatchStubRunner +import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration +import org.springframework.cloud.stream.config.BindingProperties +import org.springframework.cloud.stream.config.ChannelBindingServiceProperties +import org.springframework.context.Lifecycle +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.integration.dsl.FilterEndpointSpec +import org.springframework.integration.dsl.GenericEndpointSpec +import org.springframework.integration.dsl.IntegrationFlowBuilder +import org.springframework.integration.dsl.IntegrationFlows +import org.springframework.messaging.Message +/** + * Spring Cloud Stream configuration that iterates over the downloaded Groovy DSLs + * and registers a flow for each DSL. + * + * @author Marcin Grzejszczak + */ +@Configuration +@Import(StubRunnerConfiguration) +@CompileStatic +class StubRunnerStreamConfiguration { + + private static final Logger log = LoggerFactory.getLogger(StubRunnerStreamConfiguration) + + @Bean + FlowRegistrar flowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) { + Map> contracts = batchStubRunner.contracts + contracts.each { StubConfiguration key, Collection value -> + String name = "${key.groupId}_${key.artifactId}" + value.findAll { it?.input?.messageFrom?.clientValue }.each { Contract dsl -> + String flowName = "${name}_${dsl.label}_${dsl.hashCode()}" + String from = resolvedDestination(beanFactory, dsl.input.messageFrom.clientValue) + IntegrationFlowBuilder builder = IntegrationFlows.from(from) + .filter(new StubRunnerStreamMessageSelector(dsl), { FilterEndpointSpec e -> e.id("${flowName}.filter") } ) + .transform(new StubRunnerStreamTransformer(dsl), { GenericEndpointSpec e -> e.id("${flowName}.transformer") }) + if (dsl.outputMessage?.sentTo) { + builder = builder.channel(resolvedDestination(beanFactory, dsl.outputMessage.sentTo.clientValue)) + } else { + builder = builder.handle(new DummyMessageHandler(), "handle") + } + beanFactory.initializeBean(builder.get(), flowName) + beanFactory.getBean("${flowName}.filter", Lifecycle.class).start(); + beanFactory.getBean("${flowName}.transformer", Lifecycle.class).start(); + } + } + return new FlowRegistrar() + } + + private String resolvedDestination(AutowireCapableBeanFactory context, String destination) { + ChannelBindingServiceProperties channelBindingServiceProperties = context.getBean(ChannelBindingServiceProperties.class); + String resolvedDestination = destination; + for (Map.Entry entry : channelBindingServiceProperties.getBindings().entrySet()) { + if (entry.getValue().getDestination().equals(destination)) { + log.debug("Found a channel named [{}] with destination [{}]", entry.getKey(), destination); + return entry.getKey(); + } + } + log.debug("No destination named [{}] was found. Assuming that the destination equals the channel name", destination); + return resolvedDestination; + } + + @CompileStatic + private static class DummyMessageHandler { + void handle(Message message) {} + } + + static class FlowRegistrar {} +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.groovy new file mode 100644 index 0000000000..2bbe85eb21 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.groovy @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +import com.toomuchcoding.jsonassert.JsonVerifiable +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.integration.core.MessageSelector +import org.springframework.messaging.Message + +import java.util.regex.Pattern + +/** + * Passes through a message that matches the one defined in the DSL + * + * @author Marcin Grzejszczak + */ +@CompileStatic +class StubRunnerStreamMessageSelector implements MessageSelector { + + private final Contract groovyDsl + private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper() + + StubRunnerStreamMessageSelector(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + boolean accept(Message message) { + if(!headersMatch(message)){ + return false + } + Object inputMessage = message.getPayload() + JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValues(groovyDsl.input.messageBody) + DocumentContext parsedJson = JsonPath.parse(objectMapper.writeValueAsString(inputMessage)) + return jsonPaths.every { matchesJsonPath(parsedJson, it) } + } + + private boolean matchesJsonPath(DocumentContext parsedJson, JsonVerifiable jsonVerifiable) { + try { + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonVerifiable.jsonPath()) + return true + } catch (Exception e) { + return false + } + } + + private boolean headersMatch(Message message) { + Map headers = message.getHeaders() + return groovyDsl.input.messageHeaders.entries.every { + String name = it.name + Object value = it.clientValue + Object valueInHeader = headers.get(name) + return value instanceof Pattern ? + value.matcher(valueInHeader.toString()).matches() : + valueInHeader == value + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformer.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformer.groovy new file mode 100644 index 0000000000..c40595e4be --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformer.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import org.springframework.cloud.contract.verifier.util.BodyExtractor +import org.springframework.cloud.contract.spec.Contract +import org.springframework.integration.transformer.GenericTransformer +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder + +/** + * Sends forward a message defined in the DSL. + * + * @author Marcin Grzejszczak + */ +class StubRunnerStreamTransformer implements GenericTransformer, Message> { + + private final Contract groovyDsl + + StubRunnerStreamTransformer(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + @Override + Message transform(Message source) { + if (!groovyDsl.outputMessage) { + return source + } + String payload = BodyExtractor.extractStubValueFrom(groovyDsl.outputMessage.body) + Map headers = groovyDsl.outputMessage.headers.asStubSideMap() + return MessageBuilder.createMessage(payload, new MessageHeaders(headers)) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..35f2e3fddc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.messaging.stream.StubRunnerStreamConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/BookReturned.groovy new file mode 100644 index 0000000000..49dea202eb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/BookReturned.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode + +@CompileStatic +@EqualsAndHashCode +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy new file mode 100644 index 0000000000..0860db7335 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy @@ -0,0 +1,226 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.cloud.stream.messaging.Sink +import org.springframework.cloud.stream.messaging.Source +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import java.util.concurrent.TimeUnit +/** + * @author Marcin Grzejszczak + */ +@Configuration +@ComponentScan +@EnableAutoConfiguration +@ContextConfiguration(classes = StreamStubRunnerSpec, loader = SpringApplicationContextLoader) +@EnableBinding([Sink, Source]) +class StreamStubRunnerSpec extends Specification { + + @Autowired StubFinder stubFinder + @Autowired ContractVerifierMessaging messaging + + def setup() { + // ensure that message were taken from the queue + messaging.receiveMessage('returnBook', 100, TimeUnit.MILLISECONDS) + } + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage') + // end::client_send[] + then: + // tag::client_receive[] + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook') + // end::client_receive[] + and: + // tag::client_receive_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_receive_message[] + } + + def 'should trigger a message by label'() { + when: + // tag::client_trigger[] + stubFinder.trigger('return_book_1') + // end::client_trigger[] + then: + // tag::client_trigger_receive[] + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook') + // end::client_trigger_receive[] + and: + // tag::client_trigger_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_trigger_message[] + } + + def 'should trigger a label for the existing groupId:artifactId'() { + when: + // tag::trigger_group_artifact[] + stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1') + // end::trigger_group_artifact[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label for the existing artifactId'() { + when: + // tag::trigger_artifact[] + stubFinder.trigger('streamService', 'return_book_1') + // end::trigger_artifact[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should throw exception when missing label is passed'() { + when: + stubFinder.trigger('missing label') + then: + thrown(IllegalArgumentException) + } + + def 'should throw exception when missing label and artifactid is passed'() { + when: + stubFinder.trigger('some:service', 'return_book_1') + then: + thrown(IllegalArgumentException) + } + + def 'should trigger messages by running all triggers'() { + when: + // tag::trigger_all[] + stubFinder.trigger() + // end::trigger_all[] + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label with no output message'() { + when: + // tag::trigger_no_output[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete') + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + def 'should not trigger a message that does not match input'() { + when: + messaging.send(new BookReturned('not_matching'), [wrong: 'header_value'], 'bookStorage') + then: + ContractVerifierMessage receivedMessage = messaging.receiveMessage('returnBook', 100, TimeUnit.MILLISECONDS) + and: + receivedMessage == null + } + + private boolean assertJsons(Object payload) { + String objectAsString = payload instanceof String ? payload : + JsonOutput.toJson(payload) + def json = new JsonSlurper().parseText(objectAsString) + return json.bookName == 'foo' + } + + Contract dsl = + // tag::sample_dsl[] + Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('returnBook') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl[] + + Contract dsl2 = + // tag::sample_dsl_2[] + Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('returnBook') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl_2[] + + Contract dsl3 = + // tag::sample_dsl_3[] + Contract.make { + label 'delete_book' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + // end::sample_dsl_3[] + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformerSpec.groovy new file mode 100644 index 0000000000..a948936dad --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamTransformerSpec.groovy @@ -0,0 +1,132 @@ +package org.springframework.cloud.contract.stubrunner.messaging.stream + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import spock.lang.Specification + +class StubRunnerStreamTransformerSpec extends Specification { + + Message message = MessageBuilder.withPayload("hello").build() + + def noOutputMessageContract = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + } + + def 'should not transform the message if there is no output message'() { + given: + StubRunnerStreamTransformer streamTransformer = new StubRunnerStreamTransformer(noOutputMessageContract) + when: + def result = streamTransformer.transform(message) + then: + result.is(message) + } + + def dsl = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('returnBook') + body([ + responseId: $(producer(regex('[0-9]+')), consumer('123')) + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + def 'should convert dsl into message'() { + given: + StubRunnerStreamTransformer streamTransformer = new StubRunnerStreamTransformer(dsl) + when: + def result = streamTransformer.transform(message) + then: + result.payload == '{"responseId":"123"}' + } + + def dslWithRegexInGString = Contract.make { + // Human readable description + description 'Should produce valid sensor data' + // Label by means of which the output message can be triggered + label 'sensor1' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createSensorData()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'sensor-data' + headers { + header('contentType': 'application/json') + } + // the body of the output message + body("""{"id":"${value(producer(regex('[0-9]+')), consumer('99'))}","temperature":"123.45"}""") + } + } + + def 'should convert dsl into message with regex in GString'() { + given: + StubRunnerStreamTransformer streamTransformer = new StubRunnerStreamTransformer(dslWithRegexInGString) + when: + def result = streamTransformer.transform(message) + then: + result.payload == '''{"id":"99","temperature":"123.45"}''' + } + + def 'should parse dsl without DslProperty'() { + given: + Contract contract = Contract.make { + // Human readable description + description 'Sends an order message' + // Label by means of which the output message can be triggered + label 'send_order' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('orderTrigger()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('orders') + // any headers for the output message + headers { + header('contentType': 'application/json') + } + // the body of the output message + body( + orderId: value( + consumer('40058c70-891c-4176-a033-f70bad0c5f77'), + producer(regex('([0-9|a-f]*-*)*'))), + description: "This is the order description" + ) + } + } + StubRunnerStreamTransformer streamTransformer = new StubRunnerStreamTransformer(contract) + when: + def result = streamTransformer.transform(message) + then: + result.payload == '''{"orderId":"40058c70-891c-4176-a033-f70bad0c5f77","description":"This is the order description"}''' + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/application.yml new file mode 100644 index 0000000000..f7c3f12f4a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/application.yml @@ -0,0 +1,11 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs + +spring: + cloud: + stream: + bindings: + output: + destination: returnBook + input: + destination: bookStorage \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..f14e885df3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + + + true + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..8613d57be0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata-local.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata-local.xml new file mode 100644 index 0000000000..001cf26899 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml new file mode 100644 index 0000000000..001cf26899 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + streamService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/README.adoc new file mode 100644 index 0000000000..7ea59a2a37 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/README.adoc @@ -0,0 +1,27 @@ +=== Stub Runner Spring Cloud + +Registers the stubs in the provided Service Discovery. It's enough to add the jar + +[source,groovy,indent=0] +---- +org.springframework.cloud.contract:stub-runner-spring-cloud +---- + +and the Stub Runner autoconfiguration should be picked up. + +==== Stubbing Service Discovery + +The most important feature of `Stub Runner Spring Cloud` is the fact that it's stubbing + +- `DiscoveryClient` +- `Ribbon` `ServerList` + +that means that regardles of the fact whether you're using Zookeeper, Consul, Eureka or anything else, you don't need that in your tests. +We're starting WireMock instances of your dependencies and we're telling your application whenever you're using `Feign`, load balanced `RestTemplate` +or `DiscoveryClient` directly, to call those stubbed servers instead of calling the real Service Discovery tool. + +==== Additional Configuration + +You can match the artifactId of the stub with the name of your app by using the `stubrunner.stubs.idsToServiceIds:` map. +You can disable Stub Runner Ribbon support by providing: `stubrunner.cloud.ribbon.enabled` equal to `false` +You can disable Stub Runner support by providing: `stubrunner.cloud.enabled` equal to `false` \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/build.gradle new file mode 100644 index 0000000000..0484ddae5c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner-spring") + + compile localGroovy() + // TODO: should be compile only + compile "org.springframework.cloud:spring-cloud-starter:${springCloudVersion}" + compile "org.springframework.cloud:spring-cloud-starter-ribbon:${springCloudVersion}" + + testCompile('org.spockframework:spock-core:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'cglib:cglib-nodep:2.2' + testCompile 'org.objenesis:objenesis:2.1' + testCompile "org.springframework.cloud:spring-cloud-starter-zookeeper-discovery:${springCloudZookeeperVersion}" + testCompile 'org.apache.curator:curator-test:2.9.1' + testCompile 'io.reactivex:rxjava:1.1.1' + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile "org.springframework:spring-web:${springVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'ch.qos.logback:logback-classic:1.1.3' +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubMapperProperties.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubMapperProperties.java new file mode 100644 index 0000000000..79d405b9b0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubMapperProperties.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Maps Ivy based ids to service Ids. You might want to name the service you're calling + * in another way than artifact id. If that's the case then this class should be used + * to change do the proper mapping. + * + * Just provide in your properties file for example: + * + * stubrunner.stubs.idsToServiceIds: + * ivyNotation: someValueInsideYourCode + * fraudDetectionServer: someNameThatShouldMapFraudDetectionServer + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +@ConfigurationProperties("stubrunner.stubs") +public class StubMapperProperties { + + /** + * Mapping of Ivy notation based ids to serviceIds + * inside your application + * + * Example + * + * "a:b" -> "myService" + * "artifactId" -> "myOtherService" + */ + private Map idsToServiceIds = new HashMap<>(); + + public Map getIdsToServiceIds() { + return this.idsToServiceIds; + } + + public void setIdsToServiceIds(Map idsToServiceIds) { + this.idsToServiceIds = idsToServiceIds; + } + + public String fromIvyNotationToId(String ivyNotation) { + return idsToServiceIds.get(ivyNotation); + } + + public String fromServiceIdToIvyNotation(String serviceId) { + for (Map.Entry entry : idsToServiceIds.entrySet()) { + if (entry.getValue().equals(serviceId)) { + return entry.getKey(); + } + } + return null; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java new file mode 100644 index 0000000000..e4905c7814 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud; + +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClient; + +import org.springframework.cloud.contract.stubrunner.RunningStubs; +import org.springframework.cloud.contract.stubrunner.StubFinder; +import org.springframework.cloud.contract.stubrunner.util.StringUtils; + +/** + * Custom version of {@link DiscoveryClient} that tries to find an instance + * in one of the started WireMock servers + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +public class StubRunnerDiscoveryClient implements DiscoveryClient { + + private final DiscoveryClient delegate; + private final StubFinder stubFinder; + private final StubMapperProperties stubMapperProperties; + + public StubRunnerDiscoveryClient(DiscoveryClient delegate, StubFinder stubFinder, + StubMapperProperties stubMapperProperties, String springAppName) { + this.delegate = delegate instanceof StubRunnerDiscoveryClient ? + noOpDiscoveryClient(springAppName) : delegate; + this.stubFinder = stubFinder; + this.stubMapperProperties = stubMapperProperties; + } + + private NoopDiscoveryClient noOpDiscoveryClient(String springAppName) { + return new NoopDiscoveryClient(new DefaultServiceInstance(springAppName, "localhost", 0, false)); + } + + @Override + public String description() { + return delegate.description(); + } + + @Override + public ServiceInstance getLocalServiceInstance() { + return delegate.getLocalServiceInstance(); + } + + @Override + public List getInstances(String serviceId) { + String ivyNotation = stubMapperProperties.fromServiceIdToIvyNotation(serviceId); + String serviceToFind = StringUtils.hasText(ivyNotation) ? ivyNotation : serviceId; + URL stubUrl = stubFinder.findStubUrl(serviceToFind); + if (stubUrl == null) { + return delegate.getInstances(serviceId); + } + return Collections.singletonList( + new StubRunnerServiceInstance(serviceId, stubUrl.getHost(), stubUrl.getPort(), toUri(stubUrl)) + ); + } + + private URI toUri(URL url) { + try { + return url.toURI(); + } catch (Exception e) { + return null; + } + } + + @Override + public List getServices() { + List services = delegate.getServices(); + RunningStubs runningStubs = stubFinder.findAllRunningStubs(); + services.addAll(runningStubs.getAllServicesNames()); + return services; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerServiceInstance.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerServiceInstance.java new file mode 100644 index 0000000000..d34340f98e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerServiceInstance.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.cloud.client.ServiceInstance; + +/** + * {@link ServiceInstance} with a helpful constructor + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +public class StubRunnerServiceInstance implements ServiceInstance { + + private final String serviceId; + private final String host; + private final int port; + private final URI uri; + + public StubRunnerServiceInstance(String serviceId, String host, int port, URI uri) { + this.serviceId = serviceId; + this.host = host; + this.port = port; + this.uri = uri; + } + + @Override + public String getServiceId() { + return serviceId; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public Map getMetadata() { + return new HashMap<>(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java new file mode 100644 index 0000000000..6a5ec3f72d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +import org.springframework.cloud.contract.stubrunner.StubFinder; + +/** + * Wraps {@link DiscoveryClient} in a Stub Runner implementation that tries to find + * a corresponding WireMock server for a searched dependency + * + * @since 1.0.0 + */ +@Configuration +@EnableConfigurationProperties +@ConditionalOnClass(DiscoveryClient.class) +@Import(StubRunnerConfiguration.class) +@ConditionalOnProperty(value = "stubrunner.cloud.enabled", matchIfMissing = true) +public class StubRunnerSpringCloudAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public StubMapperProperties stubMapperProperties() { + return new StubMapperProperties(); + } + + @Bean + @Primary + public DiscoveryClient stubRunnerDiscoveryClient(DiscoveryClient discoveryClient, + StubFinder stubFinder, + StubMapperProperties stubMapperProperties, + @Value("${spring.application.name:unknown}") String springAppName) { + return new StubRunnerDiscoveryClient(discoveryClient, stubFinder, stubMapperProperties, springAppName); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonAutoConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonAutoConfiguration.java new file mode 100644 index 0000000000..7e886e6eac --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon; + +import com.netflix.loadbalancer.ServerList; +import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass(ServerList.class) +@ConditionalOnBean(StubMapperProperties.class) +@AutoConfigureAfter(RibbonAutoConfiguration.class) +@RibbonClients(defaultConfiguration = StubRunnerRibbonConfiguration.class) +@ConditionalOnProperty(value = "stubrunner.cloud.ribbon.enabled", matchIfMissing = true) +public class StubRunnerRibbonAutoConfiguration { + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonBeanPostProcessor.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonBeanPostProcessor.java new file mode 100644 index 0000000000..9ba1002603 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonBeanPostProcessor.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ServerList; +import org.springframework.cloud.contract.stubrunner.StubFinder; +import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * Ribbon AutoConfiguration that manipulates the service id to make the service + * be picked from the list of available WireMock instance if one is available. + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class StubRunnerRibbonBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + private StubFinder stubFinder; + private StubMapperProperties stubMapperProperties; + private IClientConfig clientConfig; + + StubRunnerRibbonBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + private StubFinder stubFinder() { + if (stubFinder == null) { + stubFinder = this.beanFactory.getBean(StubFinder.class); + } + return stubFinder; + } + + private StubMapperProperties stubMapperProperties() { + if (stubMapperProperties == null) { + stubMapperProperties = this.beanFactory.getBean(StubMapperProperties.class); + } + return stubMapperProperties; + } + + private IClientConfig clientConfig() { + if (clientConfig == null) { + clientConfig = this.beanFactory.getBean(IClientConfig.class); + } + return clientConfig; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ServerList && !(bean instanceof StubRunnerRibbonServerList)) { + return new StubRunnerRibbonServerList(stubFinder(), stubMapperProperties(), clientConfig(), (ServerList) bean); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonConfiguration.java new file mode 100644 index 0000000000..b03d729a5f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon; + +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +class StubRunnerRibbonConfiguration { + @Bean + StubRunnerRibbonBeanPostProcessor stubRunnerRibbonBeanPostProcessor(BeanFactory beanFactory) { + return new StubRunnerRibbonBeanPostProcessor(beanFactory); + } + + @Bean + @ConditionalOnMissingBean + public ServerList noOpServerList() { + return new ServerList() { + @Override + public List getInitialListOfServers() { + return new ArrayList<>(); + } + + @Override + public List getUpdatedListOfServers() { + return new ArrayList<>(); + } + }; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonServerList.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonServerList.java new file mode 100644 index 0000000000..e0efe4bc8d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/ribbon/StubRunnerRibbonServerList.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; + +import org.springframework.cloud.contract.stubrunner.RunningStubs; +import org.springframework.cloud.contract.stubrunner.StubConfiguration; +import org.springframework.cloud.contract.stubrunner.StubFinder; +import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties; +import org.springframework.cloud.contract.stubrunner.util.StringUtils; + +/** + * Stub Runner representation of a server list + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class StubRunnerRibbonServerList implements ServerList { + + private final ServerList serverList; + + @SuppressWarnings("unchecked") + StubRunnerRibbonServerList(final StubFinder stubFinder, + final StubMapperProperties stubMapperProperties, + final IClientConfig clientConfig, + final ServerList delegate) {; + String serviceName = clientConfig.getClientName(); + String mappedServiceName = StringUtils + .hasText(stubMapperProperties.fromServiceIdToIvyNotation(serviceName)) ? + stubMapperProperties.fromServiceIdToIvyNotation(serviceName) : serviceName; + RunningStubs runningStubs = stubFinder.findAllRunningStubs(); + final Map.Entry entry = runningStubs.getEntry(mappedServiceName); + final Collection servers = new ArrayList<>(); + if (entry != null) { + servers.add(new Server("localhost", entry.getValue()) { + @Override + public MetaInfo getMetaInfo() { + return new MetaInfo() { + @Override + public String getAppName() { + return stubMapperProperties.fromIvyNotationToId(entry.getKey().toColonSeparatedDependencyNotation()); + } + + @Override + public String getServerGroup() { + return null; + } + + @Override + public String getServiceIdForDiscovery() { + return stubMapperProperties.fromIvyNotationToId(entry.getKey().getArtifactId()); + } + + @Override + public String getInstanceId() { + return stubMapperProperties.fromIvyNotationToId(entry.getKey().getArtifactId()); + } + }; + } + }); + } + serverList = new ServerList() { + @Override + public List getInitialListOfServers() { + List combinedList = new ArrayList<>(); + combinedList.addAll(servers); + combinedList.addAll(delegate.getInitialListOfServers()); + return combinedList; + } + + @Override + public List getUpdatedListOfServers() { + List combinedList = new ArrayList<>(); + combinedList.addAll(servers); + combinedList.addAll(delegate.getUpdatedListOfServers()); + return combinedList; + } + }; + } + + @Override + public List getInitialListOfServers() { + return serverList.getInitialListOfServers(); + } + + @Override + public List getUpdatedListOfServers() { + return serverList.getUpdatedListOfServers(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..fd191f3591 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.spring.cloud.StubRunnerSpringCloudAutoConfiguration,\ +org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon.StubRunnerRibbonAutoConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfigurationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfigurationSpec.groovy new file mode 100644 index 0000000000..8a3c6f886e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfigurationSpec.groovy @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring.cloud + +import org.apache.curator.test.TestingServer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.boot.test.WebIntegrationTest +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.cloud.client.discovery.EnableDiscoveryClient +import org.springframework.cloud.client.loadbalancer.LoadBalanced +import org.springframework.cloud.zookeeper.discovery.ZookeeperServiceDiscovery +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ContextConfiguration +import org.springframework.web.client.RestTemplate +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader) +@WebIntegrationTest(randomPort = true) +class StubRunnerSpringCloudAutoConfigurationSpec extends Specification { + + @Autowired StubFinder stubFinder + @Autowired @LoadBalanced RestTemplate restTemplate + @Autowired ZookeeperServiceDiscovery zookeeperServiceDiscovery + @Autowired ConfigurableApplicationContext applicationContext + @Shared @AutoCleanup TestingServer testingServer = new TestingServer(2181) + + def 'should make service discovery work'() { + expect: 'WireMocks are running' + "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + and: 'Stubs can be reached via load service discovery' + restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance' + restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer' + } + + def cleanup() { + zookeeperServiceDiscovery.serviceDiscovery.close() + applicationContext.close() + } + + @Configuration + @EnableAutoConfiguration + @EnableDiscoveryClient + static class Config { + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplate() + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/application.yml new file mode 100644 index 0000000000..629b688f80 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/application.yml @@ -0,0 +1,6 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer + +stubrunner.stubs.idsToServiceIds: + ivyNotation: someValueInsideYourCode + fraudDetectionServer: someNameThatShouldMapFraudDetectionServer \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/logback.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/logback.xml new file mode 100644 index 0000000000..7eecabf2f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..40610c8cdb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml new file mode 100644 index 0000000000..281b9b3b80 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..59db343686 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml new file mode 100644 index 0000000000..615428dae6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062111 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/README.adoc new file mode 100644 index 0000000000..38169894bf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/README.adoc @@ -0,0 +1,21 @@ +=== Stub Runner Spring + +Sets up Spring configuration of the Stub Runner project. + +By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs. + +If you want to find the URL of your stubbed dependency you can autowire the `StubFinder` interface and use +its methods as presented below: + +[source,groovy,indent=0] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfigurationSpec.groovy[tags=test] +---- + +for the following configuration file: + +[source,yml,indent=0] +---- +include::src/test/resources/application.yml[] +---- \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/build.gradle new file mode 100644 index 0000000000..32586d07e3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/build.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = 'Spring configuration for stub-runner' + +String stubRunner = "spring-cloud-contract-stub-runner" + +dependencies { + compile project(":$stubRunner-root:$stubRunner") + + compile localGroovy() + compile "org.springframework:spring-context:${springVersion}" + + testCompile('org.spockframework:spock-core:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'cglib:cglib-nodep:2.2' + testCompile 'org.objenesis:objenesis:2.1' + testCompile "org.springframework.boot:spring-boot-starter:${springBootVersion}" + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile('org.spockframework:spock-spring:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'ch.qos.logback:logback-classic:1.1.3' +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java new file mode 100644 index 0000000000..bea711f6f1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.contract.stubrunner.AetherStubDownloader; +import org.springframework.cloud.contract.stubrunner.BatchStubRunner; +import org.springframework.cloud.contract.stubrunner.BatchStubRunnerFactory; +import org.springframework.cloud.contract.stubrunner.StubDownloader; +import org.springframework.cloud.contract.stubrunner.StubRunner; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptions; +import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder; +import org.springframework.cloud.contract.stubrunner.StubRunning; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +/** + * Configuration that initializes a {@link BatchStubRunner} that runs {@link StubRunner} instance for each stub + */ +@Configuration +public class StubRunnerConfiguration { + + @Autowired(required = false) ContractVerifierMessaging contractVerifierMessaging; + @Autowired(required = false) StubDownloader stubDownloader; + + /** + * Bean that initializes stub runners, runs them and on shutdown closes them. Upon its instantiation + * JAR with stubs is downloaded and unpacked to a temporary folder and WireMock server are started + * for each of those stubs + * + * @param minPortValue min port value of the WireMock instance for stubs + * @param maxPortValue max port value of the WireMock instance for stubs + * @param stubRepositoryRoot root URL from where the JAR with stub mappings will be downloaded + * @param stubsSuffix classifier for the jar containing stubs + * @param workOffline forces offline work + * @param stubs comma separated list of stubs presented in Ivy notation + */ + @Bean(destroyMethod = "close") + public StubRunning batchStubRunner( + @Value("${stubrunner.port.range.min:10000}") Integer minPortValue, + @Value("${stubrunner.port.range.max:15000}") Integer maxPortValue, + @Value("${stubrunner.stubs.repository.root:}") Resource stubRepositoryRoot, + @Value("${stubrunner.stubs.classifier:stubs}") String stubsSuffix, + @Value("${stubrunner.work-offline:false}") boolean workOffline, + @Value("${stubrunner.stubs.ids:}") String stubs) throws IOException { + StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder() + .withMinMaxPort(minPortValue, maxPortValue) + .withStubRepositoryRoot(uriStringOrEmpty(stubRepositoryRoot)) + .withWorkOffline(stubRepositoryRoot == null || workOffline) + .withStubsClassifier(stubsSuffix) + .withStubs(stubs) + .build(); + BatchStubRunner batchStubRunner = new BatchStubRunnerFactory(stubRunnerOptions, + stubDownloader != null ? stubDownloader : new AetherStubDownloader(stubRunnerOptions), + contractVerifierMessaging != null ? contractVerifierMessaging : new NoOpContractVerifierMessaging()).buildBatchStubRunner(); + // TODO: Consider running it in a separate thread + batchStubRunner.runStubs(); + return batchStubRunner; + } + + private String uriStringOrEmpty(Resource stubRepositoryRoot) throws IOException { + return stubRepositoryRoot != null ? stubRepositoryRoot.getURI().toString() : ""; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..d39588e8dc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfigurationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfigurationSpec.groovy new file mode 100644 index 0000000000..4d9b01856d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfigurationSpec.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.spring + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +// tag::test[] +@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader) +class StubRunnerConfigurationSpec extends Specification { + + @Autowired StubFinder stubFinder + + def 'should start WireMock servers'() { + expect: 'WireMocks are running' + stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null + stubFinder.findStubUrl('loanIssuance') != null + stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') + stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null + and: + stubFinder.findAllRunningStubs().isPresent('loanIssuance') + stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer') + stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') + and: 'Stubs were registered' + "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + } + + @Configuration + @Import(StubRunnerConfiguration) + @EnableAutoConfiguration + static class Config {} +} +// end::test[] \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/application.yml new file mode 100644 index 0000000000..4f618057e1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/application.yml @@ -0,0 +1,2 @@ +stubrunner.stubs.repository.root: classpath:m2repo/repository/ +stubrunner.stubs.ids: org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/logback.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/logback.xml new file mode 100644 index 0000000000..7eecabf2f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..40610c8cdb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml new file mode 100644 index 0000000000..281b9b3b80 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + fraudDetectionServer + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..59db343686 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + pom + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml new file mode 100644 index 0000000000..615428dae6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + loanIssuance + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062111 + + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/README.adoc new file mode 100644 index 0000000000..fd0ace56e4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/README.adoc @@ -0,0 +1,99 @@ +=== Stub Runner Core + +Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +http://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven Contracts]. + +Stub Runner allows you to automatically download the stubs of the provided dependencies, start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined. + +==== Running stubs + +===== Running using main app + +You can set the following options to the main class: + +[source,groovy,indent=0] +---- + -maxp (--maxPort) N : Maximum port value to be assigned to the + Wiremock instance. Defaults to 15000 + (default: 15000) + -minp (--minPort) N : Minimal port value to be assigned to the + Wiremock instance. Defaults to 10000 + (default: 10000) + -s (--stubs) VAL : Comma separated list of Ivy representation of + jars with stubs. Eg. groupid:artifactid1,group + id2:artifactid2:version:classifier + -sr (--stubRepositoryRoot) VAL : Location of a Jar containing server where you + keep your stubs (e.g. http://nexus.net/content + /repositories/repository) + -ss (--stubsSuffix) VAL : Suffix for the jar containing stubs (e.g. + 'stubs' if the stub jar would have a 'stubs' + classifier for stubs: foobar-stubs ). + Defaults to 'stubs' (default: stubs) + -wo (--workOffline) : Switch to work offline. Defaults to 'false' + (default: false) +---- + + +===== Building a Fat Jar + +Just call the following command: + +[source,groovy,indent=0] +---- +./gradlew spring-cloud-contract-stub-runner-root:spring-cloud-contract-stub-runner:shadowJar -PfatJar +---- + +and inside the `build/lib` there will be a Fat Jar with classifier `fatJar` waiting for you to execute. E.g. + +[source,groovy,indent=0] +---- +java -jar spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/build/libs/spring-cloud-contract-stub-runner-1.0.0-SNAPSHOT-fatJar.jar -sr http://a.b.com -s a:b:c,d:e,f:g:h:i +---- + +==== Stub runner configuration + +You can configure the stub runner by either passing the full arguments list with the `-Pargs` like this: + +[source,groovy,indent=0] +---- +./gradlew spring-cloud-contract-stub-runner-root:spring-cloud-contract-stub-runner:run -Pargs="-c pl -minp 10000 -maxp 10005 -s a:b:c,d:e,f:g:h" +---- + +or each parameter separately with a `-P` prefix and without the hyphen `-` in the name of the param + +[source,groovy,indent=0] +---- +./gradlew spring-cloud-contract-stub-runner-root:spring-cloud-contract-stub-runner:run -Pc=pl -Pminp=10000 -Pmaxp=10005 -Ps=a:b:c,d:e,f:g:h +---- + +===== HTTP Stubs + +Stubs are defined in JSON documents, whose syntax is defined in http://wiremock.org/stubbing.html[WireMock documentation] + +Example: + +[source,javascript,indent=0] +---- +{ + "request": { + "method": "GET", + "url": "/ping" + }, + "response": { + "status": 200, + "body": "pong", + "headers": { + "Content-Type": "text/plain" + } + } +} +---- + +===== Viewing registered mappings + +Every stubbed collaborator exposes list of defined mappings under `__/admin/` endpoint. + +===== Messaging Stubs + +Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up. \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/build.gradle new file mode 100644 index 0000000000..dc362bdbdb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/build.gradle @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = 'Runs stubs for service collaborators' + +apply plugin: 'application' +mainClassName = 'org.springframework.cloud.stubrunner.StubRunnerMain' + +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-core") + compile project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + compile 'org.codehaus.groovy:groovy-all:2.4.4' + compile "com.github.tomakehurst:wiremock:$wiremockVersion" + compile 'javax.servlet:javax.servlet-api:3.1.0' + compile 'args4j:args4j:2.32' + compile "org.eclipse.aether:aether-api:${aetherVersion}" + compile "org.eclipse.aether:aether-impl:${aetherVersion}" + compile "org.eclipse.aether:aether-transport-file:${aetherVersion}" + compile "org.eclipse.aether:aether-transport-http:${aetherVersion}" + compile "org.eclipse.aether:aether-connector-basic:${aetherVersion}" + compile 'org.apache.maven:maven-aether-provider:3.3.9' + + if (rootProject.hasProperty("fatJar")) runtime 'ch.qos.logback:logback-classic:1.1.3' + + testCompile('org.spockframework:spock-core:1.0-groovy-2.4') { + exclude(group: 'org.codehaus.groovy') + } + testCompile 'cglib:cglib-nodep:2.2' + testCompile 'org.objenesis:objenesis:2.1' + testCompile 'ch.qos.logback:logback-classic:1.1.3' +} + +ext { + stubRepositoryRoot = getPropertyByEither('sr', 'stubRepositoryRoot') + stubsSuffix = getPropertyByEither('ss', 'stubsSuffix') + minPortValue = getPropertyByEither('minp', 'minPort') + maxPortValue = getPropertyByEither('maxp', 'maxPort') + skipLocalRepo = getPropertyByEither('wo', 'workOffline') + stubs = getPropertyByEither('s', 'stubs') + arguments = project.hasProperty('args') ? project.property('args') : [] + /* example of args: + '-minp 10000 -maxp 10005 -s ""groupid:artifactid1,groupid2:artifactid2" -sr http://dl.bintray.com/somelink/micro' + */ +} + +Object getPropertyByEither(String paramName1, String paramName2, Object defaultValue = null) { + return project.hasProperty(paramName1) ? project.property(paramName1) : + project.hasProperty(paramName2) ? project.property(paramName2) : defaultValue +} + +run { + main = 'org.springframework.cloud.stubrunner.StubRunnerMain' + List argumentList + if (arguments) { + argumentList = (arguments.split(' ') as List).findAll { it != null } + } else { + argumentList = parseArguments() + } + args = argumentList + if (args) { + println "Running task with args $args" + } +} + +List parseArguments() { + List arguments = [] + appendToListIfNotNull(stubRepositoryRoot, '-sr', arguments) + appendToListIfNotNull(stubsSuffix, '-ss', arguments) + appendToListIfNotNull(minPortValue, '-minp', arguments) + appendToListIfNotNull(maxPortValue, '-maxp', arguments) + appendToListIfNotNull(skipLocalRepo, '-wo', arguments) + appendToListIfNotNull(stubs, '-s', arguments) + return arguments +} + +Object appendToListIfNotNull(String argument, String prefix, List list) { + if (argument != null) { + list << "$prefix" + if (!argument.isAllWhitespace()) { + list << argument + } + } +} + +if (rootProject.hasProperty("fatJar")) { + + apply plugin: 'com.github.johnrengelman.shadow' + apply plugin: 'maven-publish' + + publishing { + publications { + shadow(MavenPublication) { + from components.java + artifact shadowJar + } + } + } + + shadowJar { + classifier = 'fatJar' + dependsOn build + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherFactories.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherFactories.groovy new file mode 100644 index 0000000000..df67a84c25 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherFactories.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import org.apache.maven.repository.internal.MavenRepositorySystemUtils +import org.eclipse.aether.DefaultRepositorySystemSession +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory +import org.eclipse.aether.impl.DefaultServiceLocator +import org.eclipse.aether.repository.LocalRepository +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.repository.RepositoryPolicy +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory +import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.transport.file.FileTransporterFactory +import org.eclipse.aether.transport.http.HttpTransporterFactory + +class AetherFactories { + + private static final String MAVEN_LOCAL_REPOSITORY_LOCATION = 'maven.repo.local' + + static RepositorySystem newRepositorySystem() { + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator() + locator.addService(RepositoryConnectorFactory, BasicRepositoryConnectorFactory) + locator.addService(TransporterFactory, FileTransporterFactory) + locator.addService(TransporterFactory, HttpTransporterFactory) + return locator.getService(RepositorySystem) + } + + static RepositorySystemSession newSession(RepositorySystem system, boolean workOffline) { + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession() + session.setOffline(workOffline) + if (!workOffline) { + session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS) + } + LocalRepository localRepo = new LocalRepository(localRepositoryDirectory()) + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)) + return session + } + + static List newRepositories(List repositories) { + return repositories.withIndex() + .findAll { String repo, int index -> repo } + .collect { String repo, int index -> + new RemoteRepository.Builder('remote' + index, 'default', repo).build() + } + } + + static String localRepositoryDirectory() { + System.getProperty(MAVEN_LOCAL_REPOSITORY_LOCATION, "${System.getProperty("user.home")}/.m2/repository") + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloader.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloader.groovy new file mode 100644 index 0000000000..588e26c0b4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloader.groovy @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.ArtifactRequest +import org.eclipse.aether.resolution.ArtifactResult +import org.eclipse.aether.resolution.VersionRangeRequest +import org.eclipse.aether.resolution.VersionRangeResult +import org.eclipse.aether.resolution.VersionRequest +import org.eclipse.aether.resolution.VersionResult + +import static AetherFactories.newRepositories +import static AetherFactories.newRepositorySystem +import static AetherFactories.newSession +import static org.springframework.cloud.contract.stubrunner.util.ZipCategory.unzipTo +import static java.nio.file.Files.createTempDirectory + +/** + * @author Mariusz Smykula + */ +@CompileStatic +@Slf4j +class AetherStubDownloader implements StubDownloader { + + private static final String TEMP_DIR_PREFIX = 'contracts' + private static final String ARTIFACT_EXTENSION = 'jar' + private static final String LATEST_ARTIFACT_VERSION = '(,]' + private static final String LATEST_VERSION_IN_IVY = '+' + + private final List remoteRepos + private final RepositorySystem repositorySystem + private final RepositorySystemSession session + + AetherStubDownloader(StubRunnerOptions stubRunnerOptions) { + this.remoteRepos = remoteRepositories(stubRunnerOptions) + if (!remoteRepos) { + log.error('Remote repositories for stubs are not specified!') + } + this.repositorySystem = newRepositorySystem() + this.session = newSession(this.repositorySystem, stubRunnerOptions.workOffline) + } + + /** + * Used by the Maven Plugin + * + * @param repositorySystem + * @param remoteRepositories - remote artifact repositories + * @param session + * @param workOffline + */ + AetherStubDownloader(RepositorySystem repositorySystem, + List remoteRepositories, + RepositorySystemSession session) { + this.remoteRepos = remoteRepositories + this.repositorySystem = repositorySystem + this.session = session + if (!remoteRepos) { + log.error('Remote remoteRepositories for stubs are not specified!') + } + } + + private List remoteRepositories(StubRunnerOptions stubRunnerOptions) { + return newRepositories(stubRunnerOptions.stubRepositoryRoot.split(',').toList()) + } + + private File unpackedJar(String resolvedVersion, String stubsGroup, String stubsModule, String classifier) { + log.info("Resolved version is [$resolvedVersion]") + if (!resolvedVersion) { + log.warn("Stub for group [$stubsGroup] module [$stubsModule] and classifier [$classifier] not found in $remoteRepos") + return null + } + Artifact artifact = new DefaultArtifact(stubsGroup, stubsModule, classifier, ARTIFACT_EXTENSION, resolvedVersion) + ArtifactRequest request = new ArtifactRequest(artifact: artifact, repositories: remoteRepos) + log.info("Resolving artifact '$artifact' using remote repositories $remoteRepos.url") + try { + ArtifactResult result = repositorySystem.resolveArtifact(session, request) + log.info("Resolved artifact $artifact to ${result.artifact.file}") + File temporaryFile = unpackStubJarToATemporaryFolder(result.artifact.file.toURI()) + log.info("Unpacked file to [$temporaryFile]") + return temporaryFile + } catch (Exception e) { + log.warn("Exception occured while trying to download a stub for group [$stubsGroup] module [$stubsModule] and classifier [$classifier] in $remoteRepos", e) + return null + } + + } + + private String getVersion(String stubsGroup, String stubsModule, String version, String classifier) { + if (!version || LATEST_VERSION_IN_IVY == version) { + log.info("Desired version is [$version] - will try to resolve the latest version") + return resolveHighestArtifactVersion(stubsGroup, stubsModule, classifier) + } + log.info("Will try to resolve version [$version]") + return resolveArtifactVersion(stubsGroup, stubsModule, version, classifier) + } + + @Override + Map.Entry downloadAndUnpackStubJar(StubRunnerOptions options, StubConfiguration stubConfiguration) { + String version = getVersion(stubConfiguration.groupId, stubConfiguration.artifactId, stubConfiguration.version, stubConfiguration.classifier) + File unpackedJar = unpackedJar(version, stubConfiguration.groupId, stubConfiguration.artifactId, + stubConfiguration.classifier) + if (!unpackedJar) { + return null + } + return new AbstractMap.SimpleEntry(new StubConfiguration(stubConfiguration.groupId, stubConfiguration.artifactId, version, stubConfiguration.classifier), + unpackedJar) + } + + private String resolveHighestArtifactVersion(String stubsGroup, String stubsModule, String classifier) { + Artifact artifact = new DefaultArtifact(stubsGroup, stubsModule, classifier, ARTIFACT_EXTENSION, LATEST_ARTIFACT_VERSION) + VersionRangeRequest versionRangeRequest = new VersionRangeRequest(artifact, remoteRepos, null) + VersionRangeResult rangeResult = repositorySystem.resolveVersionRange(session, versionRangeRequest) + if (!rangeResult.highestVersion) { + log.error("Version was not resolved!") + } + return rangeResult.highestVersion ?: '' + } + + private String resolveArtifactVersion(String stubsGroup, String stubsModule, String version, String classifier) { + Artifact artifact = new DefaultArtifact(stubsGroup, stubsModule, classifier, ARTIFACT_EXTENSION, version) + VersionRequest versionRequest = new VersionRequest(artifact, remoteRepos, null) + VersionResult versionResult = repositorySystem.resolveVersion(session, versionRequest) + return versionResult.version ?: '' + } + + private static File unpackStubJarToATemporaryFolder(URI stubJarUri) { + File tmpDirWhereStubsWillBeUnzipped = createTempDirectory(TEMP_DIR_PREFIX).toFile() + tmpDirWhereStubsWillBeUnzipped.deleteOnExit() + log.info("Unpacking stub from JAR [URI: ${stubJarUri}]") + unzipTo(new File(stubJarUri), tmpDirWhereStubsWillBeUnzipped) + return tmpDirWhereStubsWillBeUnzipped + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/Arguments.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/Arguments.groovy new file mode 100644 index 0000000000..254bc0634b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/Arguments.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.transform.ToString + +/** + * Arguments passed to the {@link StubRunner} application + * + * @see StubRunner + */ +@CompileStatic +@ToString(includeNames = true) +@PackageScope +class Arguments { + final StubRunnerOptions stubRunnerOptions + final String context + final String repositoryPath + final StubConfiguration stub + + Arguments(StubRunnerOptions stubRunnerOptions, String repositoryPath = "", StubConfiguration stub = null) { + this.stubRunnerOptions = stubRunnerOptions + this.context = context + this.repositoryPath = repositoryPath + this.stub = stub + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScanner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScanner.groovy new file mode 100644 index 0000000000..d493199106 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScanner.groovy @@ -0,0 +1,89 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j + +/** + * Tries to execute a closure with an available port from the given range + */ +@CompileStatic +@Slf4j +@PackageScope +class AvailablePortScanner { + + private static final int MAX_RETRY_COUNT = 1000 + + private final int minPortNumber + private final int maxPortNumber + private final int maxRetryCount + + AvailablePortScanner(int minPortNumber, int maxPortNumber, int maxRetryCount = MAX_RETRY_COUNT) { + checkPortRanges(minPortNumber, maxPortNumber) + this.minPortNumber = minPortNumber + this.maxPortNumber = maxPortNumber + this.maxRetryCount = maxRetryCount + } + + private void checkPortRanges(int minPortNumber, int maxPortNumber) { + if (minPortNumber > maxPortNumber) { + throw new InvalidPortRange(minPortNumber, maxPortNumber) + } + } + + public T tryToExecuteWithFreePort(Closure closure) { + for (i in (1..maxRetryCount)) { + try { + int numberOfPortsToBind = maxPortNumber - minPortNumber + 1 + int portToScan = new Random().nextInt(numberOfPortsToBind) + minPortNumber + checkIfPortIsAvailable(portToScan) + return executeLogicForAvailablePort(portToScan, closure) + } catch (BindException exception) { + log.debug("Failed to execute closure (try: $i/$maxRetryCount)", exception) + } + } + throw new NoPortAvailableException(minPortNumber, maxPortNumber) + } + + private T executeLogicForAvailablePort(int portToScan, Closure closure) { + log.debug("Trying to execute closure with port [$portToScan]") + return closure(portToScan) + } + + private void checkIfPortIsAvailable(int portToScan) { + ServerSocket socket = null + try { + socket = new ServerSocket(portToScan) + } finally { + socket?.close() + } + } + + static class NoPortAvailableException extends RuntimeException { + protected NoPortAvailableException(int lowerBound, int upperBound) { + super("Could not find available port in range $lowerBound:$upperBound") + } + } + + static class InvalidPortRange extends RuntimeException { + protected InvalidPortRange(int lowerBound, int upperBound) { + super("Invalid bounds exceptions, min port [$lowerBound] is greater to max port [$upperBound]") + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunner.groovy new file mode 100644 index 0000000000..a39d74fbe6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunner.groovy @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +/** + * Manages lifecycle of multiple {@link StubRunner} instances. + * + * @see StubRunner + */ +@CompileStatic +class BatchStubRunner implements StubRunning { + + private final Iterable stubRunners + + BatchStubRunner(Iterable stubRunners) { + this.stubRunners = stubRunners + } + + @Override + RunningStubs runStubs() { + Map appsAndPorts = stubRunners.inject([:]) { Map acc, StubRunner value -> + acc.putAll(value.runStubs().namesAndPorts) + return acc + } as Map + return new RunningStubs(appsAndPorts) + } + + @Override + URL findStubUrl(String groupId, String artifactId) { + return stubRunners.findResult(null) { StubRunner stubRunner -> + return stubRunner.findStubUrl(groupId, artifactId) + } as URL + } + + @Override + URL findStubUrl(String ivyNotation) { + String[] splitString = ivyNotation.split(":") + if (splitString.length > 3) { + throw new IllegalArgumentException("$ivyNotation is invalid") + } else if (splitString.length == 2) { + return findStubUrl(splitString[0], splitString[1]) + } + return findStubUrl(null, splitString[0]) + } + + @Override + RunningStubs findAllRunningStubs() { + return new RunningStubs(stubRunners.collect { StubRunner runner -> runner.findAllRunningStubs() }) + } + + @Override + Map> getContracts() { + return stubRunners.inject([:]) { Map> map, StubRunner stubRunner -> + map.putAll(stubRunner.contracts ?: [:]) + return map + } as Map> + } + + @Override + boolean trigger(String ivyNotation, String labelName) { + boolean triggered = stubRunners.inject(false) { boolean acc, StubRunner stubRunner -> + boolean success = stubRunner.trigger(ivyNotation, labelName) + if (acc) { + return true + } + return success + } + if (!triggered) { + throw new IllegalArgumentException("No label with name [$labelName] for " + + "dependency [$ivyNotation] was found. Here you have the list of dependencies " + + "and their labels [${ivyToLabels()}]") + } + return triggered + } + + private String ivyToLabels() { + return labels().entrySet().collect { + "Dependency [${it.key}] has labels ${it.value}]" + }.join('\n') + } + + @Override + boolean trigger(String labelName) { + boolean triggered = stubRunners.inject(false) { boolean acc, StubRunner stubRunner -> + boolean success = stubRunner.trigger(labelName) + if (acc) { + return true + } + return success + } + if (!triggered) { + throw new IllegalArgumentException("No label with name [$labelName] was found. " + + "Here you have the list of dependencies and their labels [${ivyToLabels()}") + } + return triggered + } + + @Override + boolean trigger() { + return stubRunners.inject(false) { boolean acc, StubRunner stubRunner -> + boolean success = stubRunner.trigger() + if (acc) { + return true + } + return success + } + } + + @Override + Map> labels() { + return stubRunners.inject([:]) { Map> map, StubRunner stubRunner -> + if (stubRunner) { + map.putAll(stubRunner.labels() ?: [:]) + } + return map + } as Map> + } + + @Override + void close() throws IOException { + stubRunners.each { + it.close() + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.groovy new file mode 100644 index 0000000000..6261afe289 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging + +/** + * Manages lifecycle of multiple {@link StubRunner} instances. + * + * @see StubRunner + * @see BatchStubRunner + */ +@CompileStatic +class BatchStubRunnerFactory { + + private final StubRunnerOptions stubRunnerOptions + private final StubDownloader stubDownloader + private final ContractVerifierMessaging contractVerifierMessaging + + BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions) { + this(stubRunnerOptions, new AetherStubDownloader(stubRunnerOptions), new NoOpContractVerifierMessaging()) + } + + BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, ContractVerifierMessaging contractVerifierMessaging) { + this(stubRunnerOptions, new AetherStubDownloader(stubRunnerOptions), contractVerifierMessaging) + } + + BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader) { + this(stubRunnerOptions, stubDownloader, new NoOpContractVerifierMessaging()) + } + + BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader, ContractVerifierMessaging contractVerifierMessaging) { + this.stubRunnerOptions = stubRunnerOptions + this.stubDownloader = stubDownloader + this.contractVerifierMessaging = contractVerifierMessaging + } + + BatchStubRunner buildBatchStubRunner() { + StubRunnerFactory stubRunnerFactory = new StubRunnerFactory(stubRunnerOptions, stubDownloader, contractVerifierMessaging) + return new BatchStubRunner(stubRunnerFactory.createStubsFromServiceConfiguration()) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/GroovyDslWrapper.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/GroovyDslWrapper.groovy new file mode 100644 index 0000000000..2f2ca4f0b0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/GroovyDslWrapper.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.springframework.cloud.contract.spec.Contract + +/** + * @author Marcin Grzejszczak + */ +@PackageScope +@CompileStatic +class GroovyDslWrapper { + + @Delegate final Contract groovyDsl + + GroovyDslWrapper(Contract groovyDsl) { + this.groovyDsl = groovyDsl + } + + boolean hasHttpPart() { + return groovyDsl.request + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/MessageNotMatchingException.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/MessageNotMatchingException.groovy new file mode 100644 index 0000000000..d99d288ee0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/MessageNotMatchingException.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.InheritConstructors + +/** + * Exception thrown when message is not matched + * + * @author Marcin Grzejszczak + */ +@InheritConstructors +class MessageNotMatchingException extends RuntimeException { +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/RunningStubs.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/RunningStubs.groovy new file mode 100644 index 0000000000..d0d0b3f657 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/RunningStubs.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +/** + * Structure representing executed stubs. Contains the configuration of each stub + * together with the port on which its executed. + */ +@EqualsAndHashCode +@CompileStatic +class RunningStubs { + final Map namesAndPorts = [:] + + RunningStubs(Map map) { + this.namesAndPorts.putAll(map) + } + + RunningStubs(Collection runningStubs) { + runningStubs.each { + this.namesAndPorts.putAll(it.namesAndPorts) + } + } + + Integer getPort(String artifactId) { + return getEntry(artifactId)?.value + } + + Map.Entry getEntry(String artifactId) { + return namesAndPorts.entrySet().find { + it.key.matchesIvyNotation(artifactId) + } + } + + Integer getPort(String groupId, String artifactId) { + return namesAndPorts.entrySet().find { + it.key.matchesIvyNotation("$groupId:$artifactId") + }?.value + } + + boolean isPresent(String artifactId) { + return getEntry(artifactId) + } + + boolean isPresent(String groupId, String artifactId) { + return namesAndPorts.entrySet().find { + it.key.matchesIvyNotation("$groupId:$artifactId") + } + } + + Set getAllServices() { + return namesAndPorts.keySet() + } + + Set getAllServicesNames() { + return namesAndPorts.keySet().collect { it.artifactId } as Set + } + + Map toIvyToPortMapping() { + return namesAndPorts.collectEntries { [(it.key.toColonSeparatedDependencyNotation()) : it.value] } as Map + } + + @Override + String toString() { + return namesAndPorts.collect { + "Stub [${it.key.toColonSeparatedDependencyNotation()}] is running on port [${it.value}]" + }.join("\n") + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubConfiguration.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubConfiguration.groovy new file mode 100644 index 0000000000..82daa2d0cc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubConfiguration.groovy @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import org.springframework.cloud.contract.stubrunner.util.StringUtils + +/** + * Represents a configuration of a single stub. The stub can be described + * by groupId:artifactId:classifier notation + */ +@CompileStatic +@EqualsAndHashCode +public class StubConfiguration { + private static final String STUB_COLON_DELIMITER = ":" + private static final String DEFAULT_VERSION = "+" + private static final String DEFAULT_CLASSIFIER = "stubs" + + final String groupId + final String artifactId + final String version + final String classifier + + public StubConfiguration(String groupId, String artifactId, String version) { + this.groupId = groupId + this.artifactId = artifactId + this.version = version + this.classifier = DEFAULT_CLASSIFIER + } + + public StubConfiguration(String groupId, String artifactId, String version, String classifier) { + this.groupId = groupId + this.artifactId = artifactId + this.version = version + this.classifier = classifier + } + + public StubConfiguration(String stubPath, String defaultClassifier) { + String[] parsedPath = parsedPathEmptyByDefault(stubPath, STUB_COLON_DELIMITER, defaultClassifier) + this.groupId = parsedPath[0] + this.artifactId = parsedPath[1] + this.version = parsedPath[2] + this.classifier = parsedPath[3] + } + + public StubConfiguration(String stubPath) { + String[] parsedPath = parsedPathEmptyByDefault(stubPath, STUB_COLON_DELIMITER, DEFAULT_CLASSIFIER) + this.groupId = parsedPath[0] + this.artifactId = parsedPath[1] + this.version = parsedPath[2] + this.classifier = parsedPath[3] + } + + private List parsedPathEmptyByDefault(String path, String delimiter, String defaultClassifier) { + String[] splitPath = path.split(delimiter) + String stubsGroupId = "" + String stubsArtifactId = "" + String stubsVersion = "" + String stubsClassifier = "" + if (splitPath.length >= 2) { + stubsGroupId = splitPath[0] + stubsArtifactId = splitPath[1] + stubsVersion = splitPath.length >= 3 ? splitPath[2] : DEFAULT_VERSION + stubsClassifier = splitPath.length == 4 ? splitPath[3] : defaultClassifier + } + return [stubsGroupId, stubsArtifactId, stubsVersion, stubsClassifier] + } + + private boolean isDefined() { + return StringUtils.hasText(groupId) && StringUtils.hasText(this.artifactId) + } + + String toColonSeparatedDependencyNotation() { + if(!isDefined()) { + return "" + } + return [groupId, artifactId, version, classifier].join(STUB_COLON_DELIMITER) + } + + @CompileDynamic + boolean groupIdAndArtifactMatches(String ivyNotationAsString) { + def (String groupId, String artifactId) = ivyNotationFrom(ivyNotationAsString) + if (!groupId) { + return this.artifactId == artifactId + } + return this.groupId == groupId && this.artifactId == artifactId + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + StubConfiguration that = (StubConfiguration) o + + if (artifactId != that.artifactId) return false + if (groupId != that.groupId) return false + + return true + } + + int hashCode() { + int result + result = (groupId != null ? groupId.hashCode() : 0) + result = 31 * result + (artifactId != null ? artifactId.hashCode() : 0) + return result + } + + boolean matchesIvyNotation(String ivyNotationAsString) { + def strings = ivyNotationAsString.split(':') + if (strings.length == 1) { + return artifactId == ivyNotationAsString + } else if(strings.length == 2) { + return groupId == strings[0] && + artifactId == strings[1] + } else if(strings.length == 3) { + return groupId == strings[0] && + artifactId == strings[1] && + (strings[2] == DEFAULT_VERSION || version == strings[2]) + } + return groupId == strings[0] && + artifactId == strings[1] && + (strings[2] == DEFAULT_VERSION || version == strings[2]) && + classifier == strings[3] + } + + private String[] ivyNotationFrom(String ivyNotation) { + String[] splitString = ivyNotation.split(":") + if (splitString.length == 1) { + // assuming that ivy notation represents artifactId only + return [null, splitString[0]] as String[] + } + return [splitString[0], splitString[1]] as String[] + } + + + @Override + public String toString() { + return toColonSeparatedDependencyNotation() + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubData.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubData.groovy new file mode 100644 index 0000000000..e66fbb3217 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubData.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import org.springframework.cloud.contract.spec.Contract + +/** + * @author Marcin Grzejszczak + */ +@CompileStatic +@EqualsAndHashCode +class StubData { + final Integer port + final List contracts +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubDownloader.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubDownloader.groovy new file mode 100644 index 0000000000..fba3a207a1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubDownloader.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +interface StubDownloader { + + /** + * Returns a mapping of updated StubConfiguration (it will contain the resolved version) and the location of the downloaded JAR. + * If there was no artifact this method will return {@code null}. + */ + Map.Entry downloadAndUnpackStubJar(StubRunnerOptions options, StubConfiguration stubConfiguration) + + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubFinder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubFinder.groovy new file mode 100644 index 0000000000..d2ba01caa9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubFinder.groovy @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import org.springframework.cloud.contract.spec.Contract + +interface StubFinder extends StubTrigger { + /** + * For the given groupId and artifactId tries to find the matching + * URL of the running stub. + * + * @param groupId - might be null. In that case a search only via artifactId takes place + * @return URL of a running stub or null if not found + */ + URL findStubUrl(String groupId, String artifactId) + + /** + * For the given Ivy notation {@code groupId:artifactId} tries to find the matching + * URL of the running stub. You can also pass only {@code artifactId}. + * + * @param ivyNotation - Ivy representation of the Maven artifact + * @return URL of a running stub or null if not found + */ + URL findStubUrl(String ivyNotation) + + /** + * Returns all running stubs + */ + RunningStubs findAllRunningStubs() + + /** + * Returns the list of Contracts + */ + Map> getContracts() +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRepository.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRepository.groovy new file mode 100644 index 0000000000..2f7f77c74a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRepository.groovy @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter + +/** + * Wraps the folder with WireMock mappings. + */ +@CompileStatic +@PackageScope +@Slf4j +class StubRepository { + + private final File path + final List projectDescriptors + final Collection contracts + + StubRepository(File repository) { + if (!repository.isDirectory()) { + throw new FileNotFoundException("Missing descriptor repository under path [$path]") + } + this.path = repository + this.projectDescriptors = projectDescriptors() + this.contracts = contracts() + } + + /** + * Returns a list of {@link Contract} + */ + private Collection contracts() { + List contracts = [] + contracts.addAll(contractDescriptors()) + return contracts + } + + /** + * Returns the list of WireMock JSON files wrapped in {@link WiremockMappingDescriptor} + */ + private List projectDescriptors() { + List mappingDescriptors = [] + mappingDescriptors.addAll(contextDescriptors()) + return mappingDescriptors + } + + private List contextDescriptors() { + return path.exists() ? collectMappingDescriptors(path) : [] + } + + private List collectMappingDescriptors(File descriptorsDirectory) { + List mappingDescriptors = [] + descriptorsDirectory.eachFileRecurse { File file -> + if (isMappingDescriptor(file)) { + mappingDescriptors << new WiremockMappingDescriptor(file) + } + } + return mappingDescriptors + } + + private Collection contractDescriptors() { + return path.exists() ? collectContractDescriptors(path) : [] + } + + private Collection collectContractDescriptors(File descriptorsDirectory) { + List mappingDescriptors = [] + descriptorsDirectory.eachFileRecurse { File file -> + if (isContractDescriptor(file)) { + try { + mappingDescriptors << ContractVerifierDslConverter.convert(file) + } catch (Exception e) { + log.warn("Exception occurred while trying to parse file [$file]", e) + } + } + } + return mappingDescriptors + } + + private static boolean isMappingDescriptor(File file) { + return file.isFile() && file.name.endsWith('.json') + } + + private static boolean isContractDescriptor(File file) { + //TODO: Consider script injections implications... + return file.isFile() && file.name.endsWith('.groovy') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunner.groovy new file mode 100644 index 0000000000..7b460ebce7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunner.groovy @@ -0,0 +1,122 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging + +/** + * Represents a single instance of ready-to-run stubs. + * Can run the stubs and then will return the name of the collaborator together with + * its URI. + * Can also be queried if the current groupid and artifactid are matching the + * corresponding running stub. + */ +@Slf4j +@CompileStatic +class StubRunner implements StubRunning { + + private final StubRepository stubRepository + private final StubConfiguration stubsConfiguration + private final StubRunnerOptions stubRunnerOptions + private final StubRunnerExecutor localStubRunner + private final ContractVerifierMessaging contractVerifierMessaging + + @Deprecated + StubRunner(Arguments arguments) { + this(arguments.stubRunnerOptions, arguments.repositoryPath, arguments.stub) + } + + StubRunner(StubRunnerOptions stubRunnerOptions, String repositoryPath, StubConfiguration stubsConfiguration) { + this(stubRunnerOptions, repositoryPath, stubsConfiguration, new NoOpContractVerifierMessaging()) + } + + StubRunner(StubRunnerOptions stubRunnerOptions, String repositoryPath, StubConfiguration stubsConfiguration, + ContractVerifierMessaging contractVerifierMessaging) { + this.stubsConfiguration = stubsConfiguration + this.stubRunnerOptions = stubRunnerOptions + this.stubRepository = new StubRepository(new File(repositoryPath)) + AvailablePortScanner portScanner = new AvailablePortScanner(stubRunnerOptions.minPortValue, + stubRunnerOptions.maxPortValue) + this.contractVerifierMessaging = contractVerifierMessaging + this.localStubRunner = new StubRunnerExecutor(portScanner, contractVerifierMessaging) + } + + @Override + RunningStubs runStubs() { + registerShutdownHook() + return localStubRunner.runStubs(stubRunnerOptions,stubRepository, stubsConfiguration) + } + + @Override + URL findStubUrl(String groupId, String artifactId) { + return localStubRunner.findStubUrl(groupId, artifactId) + } + + @Override + URL findStubUrl(String ivyNotation) { + String[] splitString = ivyNotation.split(":") + if (splitString.length == 1) { + // assuming that ivy notation represents artifactId only + return findStubUrl(null, splitString[0]) + } + return findStubUrl(splitString[0], splitString[1]) + } + + @Override + RunningStubs findAllRunningStubs() { + return localStubRunner.findAllRunningStubs() + } + + @Override + Map> getContracts() { + return localStubRunner.getContracts() + } + + @Override + boolean trigger(String ivyNotation, String labelName) { + return localStubRunner.trigger(ivyNotation, labelName) + } + + @Override + boolean trigger(String labelName) { + return localStubRunner.trigger(labelName) + } + + @Override + boolean trigger() { + return localStubRunner.trigger() + } + + @Override + Map> labels() { + return localStubRunner.labels() + } + + private void registerShutdownHook() { + Runnable stopAllServers = { this.close() } + Runtime.runtime.addShutdownHook(new Thread(stopAllServers)) + } + + @Override + void close() throws IOException { + localStubRunner?.shutdown() + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.groovy new file mode 100644 index 0000000000..7e49ad4ee0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.groovy @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.json.JsonOutput +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging +import org.springframework.cloud.contract.verifier.util.BodyExtractor + +/** + * Runs stubs for a particular {@link StubServer} + */ +@CompileStatic +@Slf4j +class StubRunnerExecutor implements StubFinder { + + private final AvailablePortScanner portScanner + private final ContractVerifierMessaging contractVerifierMessaging + private StubServer stubServer + + StubRunnerExecutor(AvailablePortScanner portScanner, ContractVerifierMessaging contractVerifierMessaging) { + this.portScanner = portScanner + this.contractVerifierMessaging = contractVerifierMessaging + } + + StubRunnerExecutor(AvailablePortScanner portScanner) { + this.portScanner = portScanner + this.contractVerifierMessaging = new NoOpContractVerifierMessaging() + } + + RunningStubs runStubs(StubRunnerOptions stubRunnerOptions, StubRepository repository, StubConfiguration stubConfiguration) { + startStubServers(stubRunnerOptions, stubConfiguration, repository) + RunningStubs runningCollaborators = + new RunningStubs([(stubServer.stubConfiguration): stubServer.port]) + log.info("All stubs are now running [${runningCollaborators.toString()}") + return runningCollaborators + } + + void shutdown() { + stubServer?.stop() + } + + @Override + URL findStubUrl(String groupId, String artifactId) { + if (!groupId) { + return returnStubUrlIfMatches(stubServer.stubConfiguration.artifactId == artifactId) + } + return returnStubUrlIfMatches(stubServer.stubConfiguration.artifactId == artifactId && + stubServer.stubConfiguration.groupId == groupId) + } + + @Override + URL findStubUrl(String ivyNotation) { + String[] splitString = ivyNotation.split(":") + if (splitString.length == 1) { + throw new IllegalArgumentException("$ivyNotation is invalid") + } + return findStubUrl(splitString[0], splitString[1]) + } + + @Override + RunningStubs findAllRunningStubs() { + return new RunningStubs([(stubServer.stubConfiguration) : stubServer.port]) + } + + @Override + Map> getContracts() { + return [(stubServer.stubConfiguration): stubServer.contracts] + } + + @Override + boolean trigger(String ivyNotationAsString, String labelName) { + Collection matchingContracts = getContracts().findAll { + it.key.groupIdAndArtifactMatches(ivyNotationAsString) + }.values().flatten() as Collection + return triggerForDsls(matchingContracts, labelName) + } + + @Override + boolean trigger(String labelName) { + return triggerForDsls(getContracts().values().flatten() as Collection, labelName) + } + + private boolean triggerForDsls(Collection dsls, String labelName) { + Collection matchingDsls = dsls.findAll { it.label == labelName } + if (matchingDsls.empty) { + return false + } + matchingDsls.each { + sendMessageIfApplicable(it) + } + return true + } + + @Override + boolean trigger() { + (getContracts().values().flatten() as Collection).each { Contract groovyDsl -> + sendMessageIfApplicable(groovyDsl) + } + return true + } + + @Override + Map> labels() { + return getContracts().collectEntries { + [(it.key.toColonSeparatedDependencyNotation()) : it.value.collect { it.label }] + } as Map> + } + + private void sendMessageIfApplicable(Contract groovyDsl) { + if (!groovyDsl.outputMessage) { + return + } + ContractVerifierMessage message = contractVerifierMessaging.create( + new JsonOutput().toJson(BodyExtractor.extractClientValueFromBody(groovyDsl.outputMessage?.body?.clientValue)), + groovyDsl.outputMessage?.headers?.asStubSideMap()) + contractVerifierMessaging.send(message, groovyDsl.outputMessage.sentTo.clientValue) + } + + private URL returnStubUrlIfMatches(boolean condition) { + return condition ? stubServer.stubUrl : null + } + + private void startStubServers(StubRunnerOptions stubRunnerOptions, StubConfiguration stubConfiguration, StubRepository repository) { + List mappings = repository.getProjectDescriptors() + Collection contracts = repository.contracts + Integer port = stubRunnerOptions.port(stubConfiguration) + if (port) { + stubServer = new StubServer(port, stubConfiguration, mappings, contracts).start() + } else { + stubServer = portScanner.tryToExecuteWithFreePort { int availablePort -> + return new StubServer(availablePort, stubConfiguration, mappings, contracts).start() + } + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.groovy new file mode 100644 index 0000000000..6429e56252 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.groovy @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging + +/** + * Factory of StubRunners. Basing on the options and passed collaborators + * downloads the stubs and returns a list of corresponding stub runners. + */ +@Slf4j +@CompileStatic +class StubRunnerFactory { + + private final StubRunnerOptions stubRunnerOptions + private final StubDownloader stubDownloader + private final ContractVerifierMessaging contractVerifierMessaging + + StubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader, ContractVerifierMessaging contractVerifierMessaging) { + this.stubRunnerOptions = stubRunnerOptions + this.stubDownloader = stubDownloader + this.contractVerifierMessaging = contractVerifierMessaging + } + + Collection createStubsFromServiceConfiguration() { + return stubRunnerOptions.getDependencies().collect { StubConfiguration stubsConfiguration -> + Map.Entry entry = stubDownloader.downloadAndUnpackStubJar(stubRunnerOptions, stubsConfiguration) + if (!entry) { + return null + } + return createStubRunner(entry.key, entry.value) + }.findAll { it != null } + } + + private StubRunner createStubRunner(StubConfiguration stubsConfiguration, File unzipedStubDir) { + if (!unzipedStubDir) { + return null + } + return createStubRunner(unzipedStubDir, stubsConfiguration, stubRunnerOptions) + } + + private StubRunner createStubRunner(File unzippedStubsDir, StubConfiguration stubsConfiguration, + StubRunnerOptions stubRunnerOptions) { + return new StubRunner(stubRunnerOptions, unzippedStubsDir.path, stubsConfiguration, contractVerifierMessaging) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMain.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMain.groovy new file mode 100644 index 0000000000..3fc959780a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMain.groovy @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.kohsuke.args4j.CmdLineException +import org.kohsuke.args4j.CmdLineParser +import org.kohsuke.args4j.Option + +import static org.kohsuke.args4j.OptionHandlerFilter.ALL + +@Slf4j +@CompileStatic +class StubRunnerMain { + + @Option(name = "-sr", aliases = ['--stubRepositoryRoot'], usage = "Location of a Jar containing server where you keep your stubs (e.g. http://nexus.net/content/repositories/repository)", required = true) + private String stubRepositoryRoot + + @Option(name = "-ss", aliases = ['--stubsSuffix'], usage = "Suffix for the jar containing stubs (e.g. 'stubs' if the stub jar would have a 'stubs' classifier for stubs: foobar-stubs ). Defaults to 'stubs'") + private String stubsSuffix = 'stubs' + + @Option(name = "-minp", aliases = ['--minPort'], usage = "Minimal port value to be assigned to the WireMock instance. Defaults to 10000") + private Integer minPortValue = 10000 + + @Option(name = "-maxp", aliases = ['--maxPort'], usage = "Maximum port value to be assigned to the WireMock instance. Defaults to 15000") + private Integer maxPortValue = 15000 + + @Option(name = "-wo", aliases = ['--workOffline'], usage = "Switch to work offline. Defaults to 'false'") + private Boolean workOffline = Boolean.FALSE + + @Option(name = "-s", aliases = ['--stubs'], usage = 'Comma separated list of Ivy representation of jars with stubs. Eg. groupid:artifactid1,groupid2:artifactid2:classifier') + private String stubs + + private final Arguments arguments + + StubRunnerMain(String[] args) { + CmdLineParser parser = new CmdLineParser(this) + try { + parser.parseArgument(args) + StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder() + .withMinMaxPort(minPortValue, maxPortValue) + .withStubRepositoryRoot(stubRepositoryRoot) + .withWorkOffline(workOffline) + .withStubsClassifier(stubsSuffix) + .withStubs(stubs) + .build() + this.arguments = new Arguments(stubRunnerOptions) + } catch (CmdLineException e) { + printErrorMessage(e, parser) + throw e + } + } + + private void printErrorMessage(CmdLineException e, CmdLineParser parser) { + System.err.println(e.getMessage()) + System.err.println("java -jar stub-runner.jar [options...] ") + parser.printUsage(System.err) + System.err.println() + System.err.println("Example: java -jar stub-runner.jar ${parser.printExample(ALL)}") + } + + static void main(String[] args) { + new StubRunnerMain(args).execute() + } + + private void execute() { + try { + log.debug("Launching StubRunner with args: $arguments") + // TODO: Pass StubsToRun either from String or File + BatchStubRunner stubRunner = new BatchStubRunnerFactory(arguments.stubRunnerOptions).buildBatchStubRunner() + RunningStubs runningCollaborators = stubRunner.runStubs() + log.info(runningCollaborators.toString()) + } catch (Exception e) { + log.error("An exception occurred while trying to execute the stubs", e) + throw e + } + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMessagingTrigger.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMessagingTrigger.groovy new file mode 100644 index 0000000000..594dd1c1ce --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerMessagingTrigger.groovy @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.PackageScope +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging + +/** + * @author Marcin Grzejszczak + */ +@PackageScope +class StubRunnerMessagingTrigger { + + private final ContractVerifierMessaging contractVerifierMessaging + + StubRunnerMessagingTrigger(ContractVerifierMessaging contractVerifierMessaging) { + this.contractVerifierMessaging = contractVerifierMessaging + } + + void trigger +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.groovy new file mode 100644 index 0000000000..d93f8ea034 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.groovy @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.transform.ToString + +/** + * Technical options related to running StubRunner + * + * Use {@class StubRunnerOptionsBuilder} to build this object. + * + * @see StubRunnerOptionsBuilder + */ +@ToString(includeNames = true) +@CompileStatic +class StubRunnerOptions { + + /** + * min port value of the WireMock instance for the given collaborator + */ + final Integer minPortValue + + /** + * max port value of the WireMock instance for the given collaborator + */ + final Integer maxPortValue + + /** + * root URL from where the JAR with stub mappings will be downloaded + */ + final String stubRepositoryRoot + + /** + * avoids local repository in dependency resolution + */ + final boolean workOffline + + /** + * stub definition classifier + */ + final String stubsClassifier + + final Collection dependencies + + /** + * colon separated list of ids to the desired port + */ + final Map stubIdsToPortMapping + + @PackageScope + StubRunnerOptions(Integer minPortValue, Integer maxPortValue, String stubRepositoryRoot, + boolean workOffline, String stubsClassifier, Collection dependencies, Map stubIdsToPortMapping) { + this.minPortValue = minPortValue + this.maxPortValue = maxPortValue + this.stubRepositoryRoot = stubRepositoryRoot + this.workOffline = workOffline + this.stubsClassifier = stubsClassifier + this.dependencies = dependencies + this.stubIdsToPortMapping = stubIdsToPortMapping + } + + Integer port(StubConfiguration stubConfiguration) { + if (stubIdsToPortMapping) { + return stubIdsToPortMapping[stubConfiguration] + } else { + return null + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.groovy new file mode 100644 index 0000000000..78b98c9186 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.groovy @@ -0,0 +1,135 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.stubrunner.util.StubsParser + +@CompileStatic +class StubRunnerOptionsBuilder { + private static final String DELIMITER = ':' + private LinkedList stubs = new LinkedList<>() + private Map stubIdsToPortMapping = [:] + + private Integer minPortValue = 10000 + private Integer maxPortValue = 15000 + private String stubRepositoryRoot + private boolean workOffline = false + private String stubsClassifier = 'stubs' + + StubRunnerOptionsBuilder() { + } + + StubRunnerOptionsBuilder(StubRunnerOptions options) { + withOptions(options) + } + + StubRunnerOptionsBuilder withStubs(String stubs) { + addStub(stubsToList(stubs)) + return this + } + + StubRunnerOptionsBuilder withStubs(List stubs) { + for (String stub : stubs) { + withStubs(stub) + } + return this + } + + StubRunnerOptionsBuilder withMinMaxPort(Integer minPortValue, Integer maxPortValue) { + this.minPortValue = minPortValue + this.maxPortValue = maxPortValue + return this + } + + StubRunnerOptionsBuilder withMinPort(int minPortValue) { + this.minPortValue = minPortValue + return this + } + + StubRunnerOptionsBuilder withMaxPort(int maxPortValue) { + this.maxPortValue = maxPortValue + return this + } + + StubRunnerOptionsBuilder withStubRepositoryRoot(String stubRepositoryRoot) { + this.stubRepositoryRoot = stubRepositoryRoot + return this + } + + StubRunnerOptionsBuilder withWorkOffline(boolean workOffline) { + this.workOffline = workOffline + return this + } + + StubRunnerOptionsBuilder withStubsClassifier(String stubsClassifier) { + this.stubsClassifier = stubsClassifier + return this + } + + StubRunnerOptionsBuilder withPort(Integer port) { + String lastStub = stubs.peekLast() + println "PORT $lastStub -> $port" + addPort(lastStub + DELIMITER + port) + return this + } + + StubRunnerOptionsBuilder withOptions(StubRunnerOptions options) { + this.minPortValue = options.minPortValue + this.maxPortValue = options.maxPortValue + this.stubRepositoryRoot = options.stubRepositoryRoot + this.workOffline = options.workOffline + this.stubsClassifier = options.stubsClassifier + return this + } + + StubRunnerOptions build() { + return new StubRunnerOptions(minPortValue, maxPortValue, stubRepositoryRoot, workOffline, stubsClassifier, buildDependencies(), stubIdsToPortMapping) + } + + private Collection buildDependencies() { + return StubsParser.fromString(stubs, stubsClassifier) + } + + private static List stubsToList(String stubIdsToPortMapping) { + return stubIdsToPortMapping.split(',').collect { it } + } + + private void addStub(List notations) { + for (String notation : notations) { + addStub(notation) + } + } + + private void addStub(String notation) { + if (StubsParser.hasPort(notation)) { + addPort(notation) + stubs.add(StubsParser.ivyFromStringWithPort(notation)) + } else { + stubs.add(notation) + } + } + + private void addPort(String notation) { + putStubIdsToPortMapping(StubsParser.fromStringWithPort(notation)) + } + + private void putStubIdsToPortMapping(Map stubIdsToPortMapping) { + this.stubIdsToPortMapping.putAll(stubIdsToPortMapping) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunning.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunning.groovy new file mode 100644 index 0000000000..08758b541e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubRunning.groovy @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +interface StubRunning extends Closeable, StubFinder { + /** + * Runs the stubs and returns the {@link RunningStubs} + */ + RunningStubs runStubs() + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubServer.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubServer.groovy new file mode 100644 index 0000000000..5ef2d0968d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubServer.groovy @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.Contract + +@CompileStatic +@Slf4j +@PackageScope +class StubServer { + private WireMockServer wireMockServer + final StubConfiguration stubConfiguration + final Collection mappings + final Collection contracts + + StubServer(int port, StubConfiguration stubConfiguration, Collection mappings, + Collection contracts) { + this.stubConfiguration = stubConfiguration + this.mappings = mappings + this.wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(port)) + this.contracts = contracts + } + + StubServer start() { + wireMockServer.start() + log.info("Started stub server for project ${stubConfiguration.toColonSeparatedDependencyNotation()} on port ${wireMockServer.port()}") + registerStubMappings() + return this + } + + void stop() { + wireMockServer.stop() + } + + int getPort() { + return wireMockServer.port() + } + + URL getStubUrl() { + return new URL("http://localhost:$port") + } + + private void registerStubMappings() { + WireMock wireMock = new WireMock('localhost', wireMockServer.port()) + registerDefaultHealthChecks(wireMock) + registerStubs(mappings, wireMock) + } + + private void registerDefaultHealthChecks(WireMock wireMock) { + registerHealthCheck(wireMock, '/ping') + registerHealthCheck(wireMock, '/health') + } + + private void registerStubs(Collection sortedMappings, WireMock wireMock) { + sortedMappings.each { WiremockMappingDescriptor mappingDescriptor -> + try { + wireMock.register(mappingDescriptor.mapping) + log.debug("Registered stub mappings from $mappingDescriptor.descriptor") + } catch (Exception e) { + log.warn("Failed to register the stub mapping [$mappingDescriptor]", e) + } + } + } + + private void registerHealthCheck(WireMock wireMock, String url, String body = 'OK') { + wireMock.register(WireMock.get(WireMock.urlEqualTo(url)).willReturn(WireMock.aResponse().withBody(body).withStatus(200))) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubTrigger.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubTrigger.groovy new file mode 100644 index 0000000000..c9ef71bafa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/StubTrigger.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +interface StubTrigger { + + /** + * Triggers an event by a given label for a given {@code groupid:artifactid} notation. You can use only {@code artifactId} too. + * + * Feature related to messaging. + * + * @return true - if managed to run a trigger + */ + boolean trigger(String ivyNotation, String labelName) + + /** + * Triggers an event by a given label. + * + * Feature related to messaging. + * + * @return true - if managed to run a trigger + */ + boolean trigger(String labelName) + + /** + * Triggers all possible events. + * + * Feature related to messaging. + * + * @return true - if managed to run a trigger + */ + boolean trigger() + + /** + * Returns a mapping of ivy notation of a dependency to all the labels it has. + * + * Feature related to messaging. + */ + Map> labels() +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/WiremockMappingDescriptor.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/WiremockMappingDescriptor.groovy new file mode 100644 index 0000000000..e3bdba3f9b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/WiremockMappingDescriptor.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.PackageScope +import groovy.transform.ToString + +/** + * Represents a single JSON file that was found in the folder with + * potential WireMock stubs + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +@PackageScope +class WiremockMappingDescriptor { + final File descriptor + + WiremockMappingDescriptor(File mappingDescriptor) { + this.descriptor = mappingDescriptor + } + + StubMapping getMapping() { + return StubMapping.buildFrom(descriptor.getText('UTF-8')) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StringUtils.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StringUtils.groovy new file mode 100644 index 0000000000..5382f8ae57 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StringUtils.groovy @@ -0,0 +1,140 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.util + +/** + * Utils ported from Apache Commons + * + * @author Marcin Grzejszczak + */ +class StringUtils { + public static String EMPTY = "" + private static int INDEX_NOT_FOUND = -1 + + // Empty checks + //----------------------------------------------------------------------- + /** + *

Checks if a String is empty ("") or null.

+ * + *
+	 * StringUtils.isEmpty(null)      = true
+	 * StringUtils.isEmpty("")        = true
+	 * StringUtils.isEmpty(" ")       = false
+	 * StringUtils.isEmpty("bob")     = false
+	 * StringUtils.isEmpty("  bob  ") = false
+	 * 
+ * + *

NOTE: This method changed in Lang version 2.0. + * It no longer trims the String. + * That functionality is available in isBlank().

+ * + * @param str the String to check, may be null + * @return true if the String is empty or null + */ + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + static boolean isNotEmpty(String string) { + return string != null && !string.empty + } + + static boolean hasText(String string) { + return isNotEmpty(string) && !string.allWhitespace + } + + /** + *

Gets the substring before the last occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * An empty or null separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+	 * StringUtils.substringBeforeLast(null, *)      = null
+	 * StringUtils.substringBeforeLast("", *)        = ""
+	 * StringUtils.substringBeforeLast("abcba", "b") = "abc"
+	 * StringUtils.substringBeforeLast("abc", "c")   = "ab"
+	 * StringUtils.substringBeforeLast("a", "a")     = ""
+	 * StringUtils.substringBeforeLast("a", "z")     = "a"
+	 * StringUtils.substringBeforeLast("a", null)    = "a"
+	 * StringUtils.substringBeforeLast("a", "")      = "a"
+	 * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the last occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringBeforeLast(String str, String separator) { + if (isEmpty(str) || isEmpty(separator)) { + return str; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the last occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * An empty or null separator will return the empty string if + * the input string is not null.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+	 * StringUtils.substringAfterLast(null, *)      = null
+	 * StringUtils.substringAfterLast("", *)        = ""
+	 * StringUtils.substringAfterLast(*, "")        = ""
+	 * StringUtils.substringAfterLast(*, null)      = ""
+	 * StringUtils.substringAfterLast("abc", "a")   = "bc"
+	 * StringUtils.substringAfterLast("abcba", "b") = "a"
+	 * StringUtils.substringAfterLast("abc", "c")   = ""
+	 * StringUtils.substringAfterLast("a", "a")     = ""
+	 * StringUtils.substringAfterLast("a", "z")     = ""
+	 * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the last occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringAfterLast(String str, String separator) { + if (isEmpty(str)) { + return str; + } + if (isEmpty(separator)) { + return EMPTY; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND || pos == (str.length() - separator.length())) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StubsParser.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StubsParser.groovy new file mode 100644 index 0000000000..bb45d160fe --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/StubsParser.groovy @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.util + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.stubrunner.StubConfiguration + +/** + * Utility to parse string into a list of configuration of stubs + */ +@CompileStatic +class StubsParser { + + /** + * The string is expected to be a map with entry called "stubs" + * that contains a list of Strings in the format + * + *
    + *
  • groupid:artifactid:classifier
  • + *
  • groupid:artifactid
  • + *
+ * + * In the latter case the provided default stub classifier will be passed. + * + * Example: + * + * "a:b,c:d:e" + */ + static Set fromString(String list, String defaultClassifier) { + def splitList = list.split(',').findAll { it } + return fromString(splitList, defaultClassifier) + } + + static Set fromString(Collection collection, String defaultClassifier) { + return collection.findAll { it }.collect { String entry -> + def splitEntry = entry.split(':') + if (splitEntry.last().isInteger()) { + String id = entry - ":${splitEntry.last()}" + new StubConfiguration(id, defaultClassifier) + } + new StubConfiguration(entry, defaultClassifier) + } as Set + } + + static Map fromStringWithPort(String notation) { + def splitEntry = notation.split(':') + if (!splitEntry.last().isInteger()) { + return [:] + } + Integer port = splitEntry.last().toInteger() + String id = notation - ":${splitEntry.last()}" + return [(new StubConfiguration(id)): port] + } + + static String ivyFromStringWithPort(String notation) { + def splitEntry = notation.split(':') + if (!splitEntry.last().isInteger()) { + return '' + } + return notation - ":${splitEntry.last()}" + } + + static boolean hasPort(String notation) { + def splitEntry = notation.split(':') + return splitEntry.last().isInteger() + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategory.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategory.groovy new file mode 100644 index 0000000000..105298b558 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategory.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.util + +import groovy.transform.CompileStatic + +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +/** + * Based on https://github.com/timyates/groovy-common-extensions. + * + * Category for {@link File} that adds a method that allows you to unzip + * a given file to a specified location + * + */ +@CompileStatic +class ZipCategory { + + /** + * Unzips this file. If the destination + * directory is not provided, it will fall back to this file's parent directory. + * + * @param self + * @param destination (optional), the destination directory where this file's content will be unzipped to. + * @return a {@link Collection} of unzipped {@link File} objects. + */ + static Collection unzipTo(File self, File destination) { + checkUnzipDestination(destination) + // if destination directory is not given, we'll fall back to the parent directory of 'self' + if (destination == null) destination = new File(self.parent) + List unzippedFiles = [] + final ZipInputStream zipInput = new ZipInputStream(new FileInputStream(self)) + zipInput.withStream { + ZipEntry entry + while (entry = zipInput.nextEntry) { + if (!entry.isDirectory()) { + final File file = new File(destination, entry.name) + file.parentFile?.mkdirs() + FileOutputStream output = new FileOutputStream(file) + output.withStream { + output << zipInput + } + unzippedFiles << file + } else { + final File dir = new File(destination, entry.name) + dir.mkdirs() + unzippedFiles << dir + } + } + } + return unzippedFiles + } + + private static void checkUnzipDestination(File file) { + if (file && !file.isDirectory()) throw new IllegalArgumentException("'destination' has to be a directory.") + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/resources/accurestStubrunnerGrapeConfig.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/resources/accurestStubrunnerGrapeConfig.xml new file mode 100644 index 0000000000..cbbd28cea5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/main/resources/accurestStubrunnerGrapeConfig.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScannerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScannerSpec.groovy new file mode 100644 index 0000000000..6a7703ee84 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AvailablePortScannerSpec.groovy @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class AvailablePortScannerSpec extends Specification { + + private static final int MIN_PORT = 8989 + private static final int MAX_PORT = 8990 + private static final int MAX_RETRY_COUNT_FOR_NEGATIVE_SCENARIOS = 2 + + def 'should execute given closure with the next available port number'() { + given: + AvailablePortScanner portScanner = new AvailablePortScanner(MIN_PORT, MAX_PORT) + when: + int usedPort = portScanner.tryToExecuteWithFreePort { int port -> port } + then: + noExceptionThrown() + usedPort == MIN_PORT || MAX_PORT + } + + def 'should execute given closure with the available port from specified range'() { + given: + AvailablePortScanner portScanner = new AvailablePortScanner(MIN_PORT, MIN_PORT) + when: + int usedPort = portScanner.tryToExecuteWithFreePort { int port -> port } + then: + noExceptionThrown() + usedPort == MIN_PORT + } + + def 'should throw exception when improper range has been provided'() { + when: + new AvailablePortScanner(minPort, maxPort, MAX_RETRY_COUNT_FOR_NEGATIVE_SCENARIOS) + then: + def ex = thrown(AvailablePortScanner.InvalidPortRange) + ex.message == "Invalid bounds exceptions, min port [$minPort] is greater to max port [$maxPort]" + where: + minPort | maxPort + MAX_PORT | MIN_PORT + + } + + def 'should throw exception when there is no available port'() { + given: + AvailablePortScanner portScanner = new AvailablePortScanner(MIN_PORT, MAX_PORT, MAX_RETRY_COUNT_FOR_NEGATIVE_SCENARIOS) + when: + portScanner.tryToExecuteWithFreePort { + throw new BindException('Bind exception from closure') + } + then: + def ex = thrown(AvailablePortScanner.NoPortAvailableException) + ex.message == "Could not find available port in range $MIN_PORT:$MAX_PORT" + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerSpec.groovy new file mode 100644 index 0000000000..83c46a48d5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/BatchStubRunnerSpec.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class BatchStubRunnerSpec extends Specification { + + static final String KNOWN_STUB_PATH = 'group:knownArtifact' + static final String UNKNOWN_STUB_PATH = 'group:unknownArtifact' + static final URL KNOWN_STUB_URL = new URL('http://localhost:8080') + + def 'should provide stub URL from enclosed stub runner'() { + given: + BatchStubRunner batchStubRunner = new BatchStubRunner(runners()) + expect: + batchStubRunner.findStubUrl(KNOWN_STUB_PATH) == KNOWN_STUB_URL + } + + def 'should return empty optional for unknown stub path'() { + given: + BatchStubRunner batchStubRunner = new BatchStubRunner(runners()) + expect: + !batchStubRunner.findStubUrl(UNKNOWN_STUB_PATH) + } + + def 'should throw exception if trying to execute not available trigger'() { + given: + BatchStubRunner batchStubRunner = new BatchStubRunner(runners()) + when: + batchStubRunner.trigger('non existing label') + then: + IllegalArgumentException exception = thrown(IllegalArgumentException) + exception.message == "No label with name [non existing label] was found. Here you have the list of dependencies and their labels [Dependency [a:b:c] has labels [foo]]" + } + + Collection runners() { + StubRunner runner = Mock(StubRunner) + runner.findStubUrl("group", "knownArtifact") >> KNOWN_STUB_URL + runner.findStubUrl("group", "unknownArtifact") >> null + runner.labels() >> ['a:b:c' : ['foo']] + return [runner] + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/MappingDescriptorSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/MappingDescriptorSpec.groovy new file mode 100644 index 0000000000..2fddae9d96 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/MappingDescriptorSpec.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import com.github.tomakehurst.wiremock.http.RequestMethod +import spock.lang.Specification + +class MappingDescriptorSpec extends Specification { + public static + final File MAPPING_DESCRIPTOR = new File('src/test/resources/repository/mappings/spring/cloud/ping/ping.json') + + def 'should describe stub mapping'() { + given: + WiremockMappingDescriptor mappingDescriptor = new WiremockMappingDescriptor(MAPPING_DESCRIPTOR) + + expect: + with(mappingDescriptor.mapping) { + request.method == RequestMethod.GET + request.url == '/ping' + response.status == 200 + response.body == 'pong' + response.headers.contentTypeHeader.mimeTypePart() == 'text/plain' + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/RunningStubsSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/RunningStubsSpec.groovy new file mode 100644 index 0000000000..57eac143b8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/RunningStubsSpec.groovy @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class RunningStubsSpec extends Specification { + RunningStubs runningStubs = new RunningStubs([(new StubConfiguration('group', 'artifact', 'version', 'classifier')) : 100]) + + def "should get port by group and artifact id"() { + expect: + runningStubs.getPort('group', 'artifact') == 100 + } + + def "should get port by artifact id"() { + expect: + runningStubs.getPort('artifact') == 100 + } + + def "should get port by group and artifact id in Ivy notation"() { + expect: + runningStubs.getPort('group:artifact') == 100 + } + + def "should get port by group, artifact id and version in Ivy notation"() { + expect: + runningStubs.getPort('group:artifact:version') == 100 + } + + def "should get port by group, artifact id, version and classifier in Ivy notation"() { + expect: + runningStubs.getPort('group:artifact:version:classifier') == 100 + } + + def "should return null if no stub has been found"() { + expect: + runningStubs.getPort('missing artifact id') == null + } + + def "should find stub by group and artifact id"() { + expect: + runningStubs.isPresent('group', 'artifact') + } + + def "should find stub by artifact id"() { + expect: + runningStubs.isPresent('artifact') + } + + def "should find stub by group and artifact id in Ivy notation"() { + expect: + runningStubs.isPresent('group:artifact') + } + + def "should find stub by group, artifact id and version in Ivy notation"() { + expect: + runningStubs.isPresent('group:artifact:version') + } + + def "should find stub by group, artifact id, version and classifier in Ivy notation"() { + expect: + runningStubs.isPresent('group:artifact:version:classifier') + } + + def "should return false if no stub has been found"() { + expect: + runningStubs.isPresent('missing artifact id') == false + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy new file mode 100644 index 0000000000..0e5d631d8d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubConfigurationSpec extends Specification { + + def 'should parse ivy notation'() { + given: + String ivy = 'group:artifact:version:classifier' + when: + StubConfiguration stubConfiguration = new StubConfiguration(ivy) + then: + stubConfiguration.artifactId == 'artifact' + stubConfiguration.groupId == 'group' + stubConfiguration.classifier == 'classifier' + stubConfiguration.version == 'version' + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRepositorySpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRepositorySpec.groovy new file mode 100644 index 0000000000..bc5614e5a1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRepositorySpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class StubRepositorySpec extends Specification { + public static + final File REPOSITORY_LOCATION = new File('src/test/resources/repository') + + def 'should retrieve all descriptors for given project'() { + given: + StubRepository repository = new StubRepository(REPOSITORY_LOCATION) + int expectedDescriptorsSize = 8 + when: + List descriptors = repository.getProjectDescriptors() + then: + descriptors.size() == expectedDescriptorsSize + } + + def 'should return empty list if files are missing'() { + given: + StubRepository repository = new StubRepository(new File('src/test/resources/emptyrepo')) + when: + List descriptors = repository.getProjectDescriptors() + then: + descriptors.empty + } + + def 'should throw an exception if directory with mappings is missing'() { + when: + new StubRepository(new File('src/test/resources/nonexistingrepo')) + then: + thrown(FileNotFoundException) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy new file mode 100644 index 0000000000..ad8b9bfe41 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy @@ -0,0 +1,130 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import groovy.json.JsonOutput +import org.springframework.cloud.contract.stubrunner.util.StubsParser +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +class StubRunnerExecutorSpec extends Specification { + + static final int MIN_PORT = 8999 + static final int MAX_PORT = 9999 + + private AvailablePortScanner portScanner + private StubRepository repository + private StubConfiguration stub = new StubConfiguration("group:artifact", "stubs") + private StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder().build() + + def setup() { + portScanner = new AvailablePortScanner(MIN_PORT, MAX_PORT) + repository = new StubRepository(new File('src/test/resources/repository')) + } + + def 'should provide URL for given relative path of stub'() { + given: + StubRunnerExecutor executor = new StubRunnerExecutor(portScanner) + when: + executor.runStubs(stubRunnerOptions, repository, stub) + then: + URL url = executor.findStubUrl("group", "artifact") + url.port >= MIN_PORT + url.port <= MAX_PORT + and: + executor.findAllRunningStubs().isPresent('artifact') + executor.findAllRunningStubs().isPresent('group', 'artifact') + executor.findAllRunningStubs().isPresent('group:artifact') + cleanup: + executor.shutdown() + } + + def 'should provide no URL for unknown dependency path'() { + given: + StubRunnerExecutor executor = new StubRunnerExecutor(portScanner) + when: + executor.runStubs(stubRunnerOptions, repository, stub) + then: + !executor.findStubUrl("unkowngroup", "unknownartifact") + } + + def 'should start a stub on a given port'() { + given: + StubRunnerExecutor executor = new StubRunnerExecutor(portScanner) + stubRunnerOptions = new StubRunnerOptionsBuilder(stubIdsToPortMapping: stubIdsWithPortsFromString('group:artifact:12345,someotherartifact:123')) + .build() + when: + executor.runStubs(stubRunnerOptions, repository, stub) + then: + executor.findStubUrl("group", "artifact") == 'http://localhost:12345'.toURL() + } + + def 'should ensure that triggered contracts have properly parsed message body when a message is sent'() { + given: + StubRunnerExecutor executor = new StubRunnerExecutor(portScanner, new AssertingContractVerifierMessaging()) + executor.runStubs(stubRunnerOptions, repository, stub) + when: + executor.trigger('send_order') + then: + noExceptionThrown() + } + + Map stubIdsWithPortsFromString(String stubIdsToPortMapping) { + return stubIdsToPortMapping.split(',').collectEntries { String entry -> + return StubsParser.fromStringWithPort(entry) + } + } + + private class AssertingContractVerifierMessaging implements ContractVerifierMessaging { + + @Override + void send(ContractVerifierMessage message, String destination) { + + } + + @Override + ContractVerifierMessage receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + return null + } + + @Override + ContractVerifierMessage receiveMessage(String destination) { + return null + } + + @Override + void send(Object o, Map headers, String destination) { + + } + + @Override + ContractVerifierMessage create(Object o, Map headers) { + assert !(JsonOutput.toJson(o).contains("serverValue")) + assert headers.entrySet().every { !(it.value.toString().contains("serverValue")) } + return null + } + + @Override + ContractVerifierMessage create(Object o) { + return null + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactorySpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactorySpec.groovy new file mode 100644 index 0000000000..ced2a5f713 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerFactorySpec.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierMessaging +import spock.lang.Specification + +class StubRunnerFactorySpec extends Specification { + + @Rule + TemporaryFolder folder = new TemporaryFolder() + + String stubs = "a:b,c:d" + StubDownloader downloader = Mock(StubDownloader) + StubRunnerOptions stubRunnerOptions + StubRunnerFactory factory + + void setup() { + stubRunnerOptions = new StubRunnerOptionsBuilder() + .withStubRepositoryRoot(folder.root.absolutePath) // FIXME: not used + .withStubs(stubs).build() + factory = new StubRunnerFactory(stubRunnerOptions, downloader, new NoOpContractVerifierMessaging()) + } + + def "Should download stub definitions many times"() { + given: + folder.newFolder("mappings") + 1 * downloader.downloadAndUnpackStubJar(_, _) >> new AbstractMap.SimpleEntry(new StubConfiguration('a:b'), folder.root) + 1 * downloader.downloadAndUnpackStubJar(_, _) >> new AbstractMap.SimpleEntry(new StubConfiguration('c:d'), folder.root) + when: + Collection stubRunners = collectOnlyPresentValues(factory.createStubsFromServiceConfiguration()) + then: + stubRunners.size() == 2 + } + + private List collectOnlyPresentValues(Collection stubRunners) { + return stubRunners.findAll { it != null } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy new file mode 100644 index 0000000000..c0d4aba44c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class StubRunnerOptionsBuilderSpec extends Specification { + + private StubRunnerOptionsBuilder builder = new StubRunnerOptionsBuilder() + + def shouldCreateDependenciesForStub() { + + given: + builder.withStubs('foo:bar') + + when: + StubRunnerOptions options = builder.build() + + then: + options.getDependencies().toString() == '[foo:bar:+:stubs]' + } + + def shouldCreateDependenciesForCommaSeparatedStubs() { + + given: + builder.withStubs('foo:bar,bar:foo') + + when: + StubRunnerOptions options = builder.build() + + then: + options.getDependencies().size() == 2 + } + + def shouldMapStubsWithPort() { + + given: + builder.withStubs('foo:bar:8080') + + when: + StubRunnerOptions options = builder.build() + + then: + options.getStubIdsToPortMapping().toString() == '[foo:bar:+:stubs:8080]' + } + + def shouldCreateDependenciesWithVersion() { + + given: + builder.withStubs('foo:1.2.3:bar') + + when: + StubRunnerOptions options = builder.build() + + then: + options.getDependencies().toString() == '[foo:1.2.3:bar:stubs]' + } + + def shouldCreateDependenciesWithClassifier() { + + given: + builder.withStubs('foo:bar').withStubsClassifier('xxx') + + when: + StubRunnerOptions options = builder.build() + + then: + options.getDependencies().toString() == '[foo:bar:+:xxx]' + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy new file mode 100644 index 0000000000..18fb646e97 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class StubRunnerSpec extends Specification { + + private static final int MIN_PORT = 8111 + private static final int MAX_PORT = 8111 + private static final URL EXPECTED_STUB_URL = new URL("http://localhost:$MIN_PORT") + + def 'should provide stub URL for provided groupid and artifactId'() { + given: + StubRunner runner = new StubRunner(argumentsWithProjectDefinition()) + when: + runner.runStubs() + then: + runner.findStubUrl("groupId", "artifactId") == EXPECTED_STUB_URL + cleanup: + runner.close() + } + + def 'should provide stub URL if only artifactId was passed'() { + given: + StubRunner runner = new StubRunner(argumentsWithProjectDefinition()) + when: + runner.runStubs() + then: + runner.findStubUrl(null, "artifactId") == EXPECTED_STUB_URL + cleanup: + runner.close() + } + + Arguments argumentsWithProjectDefinition() { + StubConfiguration stubConfiguration = new StubConfiguration("groupId", "artifactId", "classifier") + StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder().withMinMaxPort(MIN_PORT, MAX_PORT).build() + return new Arguments(stubRunnerOptions, 'src/test/resources/repository', stubConfiguration) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubServerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubServerSpec.groovy new file mode 100644 index 0000000000..7be6fbaefc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubServerSpec.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner + +import spock.lang.Specification + +class StubServerSpec extends Specification { + static final int STUB_SERVER_PORT = 12180 + static final URL EXPECTED_URL = new URL("http://localhost:$STUB_SERVER_PORT") + + File repository = new File('src/test/resources/repository/mappings/spring/cloud/bye') + StubConfiguration stubConfiguration = new StubConfiguration("a:b") + + def 'should register stub mappings upon server start'() { + given: + List mappingDescriptors = new StubRepository(repository).getProjectDescriptors() + StubServer pingStubServer = new StubServer(STUB_SERVER_PORT, stubConfiguration, mappingDescriptors, []) + when: + pingStubServer.start() + then: + "http://localhost:$pingStubServer.port/bye".toURL().text == 'Goodbye world!' + cleanup: + pingStubServer.stop() + } + + def 'should provide stub server URL'() { + given: + List mappingDescriptors = new StubRepository(repository).getProjectDescriptors() + StubServer pingStubServer = new StubServer(STUB_SERVER_PORT, stubConfiguration, mappingDescriptors, []) + when: + pingStubServer.start() + then: + pingStubServer.stubUrl == EXPECTED_URL + cleanup: + pingStubServer.stop() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategorySpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategorySpec.groovy new file mode 100644 index 0000000000..c3b0834b02 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/util/ZipCategorySpec.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.util + +import groovy.util.logging.Slf4j +import spock.lang.Specification + +@Slf4j +class ZipCategorySpec extends Specification { + + def 'should unzip a file to the specified location'() { + given: + File zipFile = new File(ZipCategorySpec.classLoader.getResource('file.zip').toURI()) + File tempDir = File.createTempDir() + tempDir.deleteOnExit() + when: + use(ZipCategory) { + zipFile.unzipTo(tempDir) + } + then: + tempDir.listFiles().find { + it.name == 'file.txt' + }?.text?.trim() == 'test' + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/bar/bar.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/bar/bar.json new file mode 100644 index 0000000000..0b71cbf35e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/bar/bar.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/bar" + }, + "response": { + "status": 200, + "body": "bar", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/bar/foobar.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/bar/foobar.json new file mode 100644 index 0000000000..e231b2c0c5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/bar/foobar.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/foobar" + }, + "response": { + "status": 200, + "body": "foobar", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/foo.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/foo.json new file mode 100644 index 0000000000..209061ef5e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/mappings/spring/cloud/foo/foo.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/foo" + }, + "response": { + "status": 200, + "body": "foo", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/brokers.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/brokers.json new file mode 100644 index 0000000000..fae6436ad8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/brokers.json @@ -0,0 +1,5 @@ +{ + "pl": [ + "spring/cloud/bar" + ] +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/nested/anotherDescriptor.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/nested/anotherDescriptor.json new file mode 100644 index 0000000000..b3a72bfbcf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/brokers/nested/anotherDescriptor.json @@ -0,0 +1,5 @@ +{ + "pl": [ + "spring/cloud/foo/bar" + ] +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/descriptor.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/descriptor.json new file mode 100644 index 0000000000..a2384b0930 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/anotherRepository/projects/descriptor.json @@ -0,0 +1,5 @@ +{ + "pl": [ + "spring/cloud/foo" + ] +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/emptyrepo/.gitkeep b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/emptyrepo/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/file.zip b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/file.zip new file mode 100644 index 0000000000000000000000000000000000000000..3da3a912405f9967ebedebe02f41ce44b66ddd5e GIT binary patch literal 171 zcmWIWW@h1H0D)76LW?TkIfXruL1mY!)AQr+{R*11^Mh1AZvVqhw0-+y}b_a17 E0OX(`8~^|S literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/logback.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/logback.xml new file mode 100644 index 0000000000..7eecabf2f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy new file mode 100644 index 0000000000..94fc426530 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy @@ -0,0 +1,27 @@ +org.springframework.cloud.contract.spec.Contract.make { + // Human readable description + description 'Sends an order message' + // Label by means of which the output message can be triggered + label 'send_order' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('orderTrigger()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('orders') + // any headers for the output message + headers { + header('contentType': 'application/json') + } + // the body of the output message + body( + orderId: value( + consumer('40058c70-891c-4176-a033-f70bad0c5f77'), + producer(regex('([0-9|a-f]*-*)*'))), + description: "This is the order description" + ) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/lv/spring/cloud/bye/lv_bye.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/lv/spring/cloud/bye/lv_bye.json new file mode 100644 index 0000000000..a062cd4674 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/lv/spring/cloud/bye/lv_bye.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/lv/bye" + }, + "response": { + "status": 200, + "body": "Another goodbye world!", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_bye.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_bye.json new file mode 100644 index 0000000000..b7c5e4ebc9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_bye.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/pl/bye" + }, + "response": { + "status": 200, + "body": "pl-bye", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_overridden_bye.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_overridden_bye.json new file mode 100644 index 0000000000..9c54c98914 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/pl/spring/cloud/bye/pl_overridden_bye.json @@ -0,0 +1,14 @@ +{ + + "request": { + "method": "GET", + "url": "/bye" + }, + "response": { + "status": 200, + "body": "overridden-bye", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/admin/admin.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/admin/admin.json new file mode 100644 index 0000000000..f730b7e3cf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/admin/admin.json @@ -0,0 +1,9 @@ +{ + "request": { + "method": "GET", + "url": "/admin" + }, + "response": { + "status": 401 + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/bye.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/bye.json new file mode 100644 index 0000000000..1a65830d4a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/bye/bye.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/bye" + }, + "response": { + "status": 200, + "body": "Goodbye world!", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/README.md b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/README.md new file mode 100644 index 0000000000..484f8efe77 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/README.md @@ -0,0 +1 @@ +### Hello service stub \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/admin/admin.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/admin/admin.json new file mode 100644 index 0000000000..f730b7e3cf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/admin/admin.json @@ -0,0 +1,9 @@ +{ + "request": { + "method": "GET", + "url": "/admin" + }, + "response": { + "status": 401 + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/hello.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/hello.json new file mode 100644 index 0000000000..3cd9f7374b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/hello/hello.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/hello" + }, + "response": { + "status": 200, + "body": "Hello world!", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/ping/ping.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/ping/ping.json new file mode 100644 index 0000000000..db01229c52 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner/src/test/resources/repository/mappings/spring/cloud/ping/ping.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/ping" + }, + "response": { + "status": 200, + "body": "pong", + "headers": { + "Content-Type": "text/plain" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle new file mode 100644 index 0000000000..b83dd96ee3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/build.gradle @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +String verifier = "spring-cloud-contract-verifier" + +project(":$verifier-root:$verifier-core") { + + dependencies { + compile project(":spring-cloud-contract-spec") + compile 'org.slf4j:slf4j-api:1.6.0' + compile 'commons-io:commons-io:2.0' + compile 'org.apache.commons:commons-lang3:3.3' + compile "com.github.tomakehurst:wiremock:$wiremockVersion" + compile "com.toomuchcoding.jsonassert:jsonassert:$jsonassertVersion" + compile 'org.codehaus.groovy:groovy-all:2.4.4' + testCompile 'cglib:cglib-nodep:2.2' + testCompile 'org.objenesis:objenesis:2.1' + testCompile project(":$verifier-root:$verifier-testing-utils") + } + +} + +project(":$verifier-root:$verifier-testing-utils") { + + dependencies { + compile 'org.skyscreamer:jsonassert:1.2.3' + } + +} + +project(":$verifier-root:$verifier-converters") { + dependencies { + compile project(":$verifier-root:$verifier-core") + compile 'org.apache.commons:commons-lang3:3.0' + compile 'commons-io:commons-io:2.0' + compile 'dk.brics.automaton:automaton:1.11-8' // needed for Xeger + testCompile "com.github.tomakehurst:wiremock:$wiremockVersion" + testCompile 'org.hamcrest:hamcrest-all:1.3' + } +} + +project(":$verifier-root:$verifier-gradle-plugin") { + + ext.messagingLibsDir ="$buildDir/messaging-libs" + ext.contractVerifierGradlePluginLibsDir ="$buildDir/contractVerifier-gradle-plugin-libs" + + ext.testSystemProperties = [ + 'contract-verifier-gradle-plugin-libs-dir': contractVerifierGradlePluginLibsDir, + 'messaging-libs-dir': messagingLibsDir + ] + + dependencies { + compile project(":$verifier-root:$verifier-core") + compile project(":$verifier-root:$verifier-converters") + compile gradleApi() + + testCompile gradleTestKit() + testCompile project(":$verifier-root:$verifier-testing-utils") + } + + configurations { + messagingLibs + contractVerifierGradlePluginLibs + } + + dependencies { + + messagingLibs project(":$verifier-root:$verifier-messaging-root:$verifier-integration") + messagingLibs project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + messagingLibs project(":$verifier-root:$verifier-testing-utils") + messagingLibs 'org.codehaus.groovy:groovy-all:2.4.5' + + contractVerifierGradlePluginLibs project(":$verifier-root:$verifier-gradle-plugin") + } + + test { + systemProperties = testSystemProperties + } + + task archiveMessagingLibsDependencies(type: Sync) { + from configurations.messagingLibs.resolvedConfiguration.resolvedArtifacts.collect { it.file } + into messagingLibsDir + } + + task archiveContractVerifierGradlePluginLibsDependencies(type: Sync) { + from configurations.contractVerifierGradlePluginLibs.resolvedConfiguration.resolvedArtifacts.collect { it.file } + into contractVerifierGradlePluginLibsDir + } + + // archive task needs to have jars ready + archiveMessagingLibsDependencies.dependsOn project(":$verifier-root:$verifier-messaging-root:$verifier-integration").tasks.jar + archiveContractVerifierGradlePluginLibsDependencies.dependsOn project(":$verifier-root:$verifier-gradle-plugin").tasks.jar + + test.dependsOn archiveMessagingLibsDependencies, archiveContractVerifierGradlePluginLibsDependencies + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/nl/flotsam/xeger/Xeger.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/nl/flotsam/xeger/Xeger.java new file mode 100644 index 0000000000..9bd42a674a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/nl/flotsam/xeger/Xeger.java @@ -0,0 +1,118 @@ +/** + * Copyright 2009 Wilfred Springer + * Copyright 2012 Jason Pell + * Copyright 2013 Antonio García-Domínguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * The class is bundled together with our code because it has not been + * released to any central repository. + * + */ +package nl.flotsam.xeger; + +import dk.brics.automaton.Automaton; +import dk.brics.automaton.RegExp; +import dk.brics.automaton.State; +import dk.brics.automaton.Transition; + +import java.util.List; +import java.util.Random; + +/** + * An object that will generate text from a regular expression. In a way, it's the opposite of a regular expression + * matcher: an instance of this class will produce text that is guaranteed to match the regular expression passed in. + */ +public class Xeger { + + private final Automaton automaton; + private Random random; + + /** + * Constructs a new instance, accepting the regular expression and the randomizer. + * + * @param regex The regular expression. (Not null.) + * @param random The object that will randomize the way the String is generated. (Not null.) + * @throws IllegalArgumentException If the regular expression is invalid. + */ + public Xeger(String regex, Random random) { + assert regex != null; + assert random != null; + this.automaton = new RegExp(regex).toAutomaton(); + this.random = random; + } + + /** + * As {@link nl.flotsam.xeger.Xeger#Xeger(String, java.util.Random)}, creating a {@link java.util.Random} instance + * implicityly. + * + * @param regex as string + */ + public Xeger(String regex) { + this(regex, new Random()); + } + + /** + * Generates a random String that is guaranteed to match the regular expression passed to the constructor. + * @return generated regexp + */ + public String generate() { + StringBuilder builder = new StringBuilder(); + generate(builder, automaton.getInitialState()); + return builder.toString(); + } + + private void generate(StringBuilder builder, State state) { + List transitions = state.getSortedTransitions(false); + if (transitions.size() == 0) { + assert state.isAccept(); + return; + } + int nroptions = state.isAccept() ? transitions.size() : transitions.size() - 1; + int option = Xeger.getRandomInt(0, nroptions, random); + if (state.isAccept() && option == 0) { // 0 is considered stop + return; + } + // Moving on to next transition + Transition transition = transitions.get(option - (state.isAccept() ? 1 : 0)); + appendChoice(builder, transition); + generate(builder, transition.getDest()); + } + + private void appendChoice(StringBuilder builder, Transition transition) { + char c = (char) Xeger.getRandomInt(transition.getMin(), transition.getMax(), random); + builder.append(c); + } + + public Random getRandom() { + return random; + } + + public void setRandom(Random random) { + this.random = random; + } + + /** + * Generates a random number within the given bounds. + * + * @param min The minimum number (inclusive). + * @param max The maximum number (inclusive). + * @param random The object used as the randomizer. + * @return A random number in the given range. + */ + static int getRandomInt(int min, int max, Random random) { + // Use random.nextInt as it guarantees a uniform distribution + int maxForRandom=max-min+1; + return random.nextInt(maxForRandom) + min; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/SingleFileConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/SingleFileConverter.groovy new file mode 100644 index 0000000000..ad860b19ea --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/SingleFileConverter.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.converter + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Converts contracts into their stub representation + * + * @since 1.0.0 + */ +@CompileStatic +interface SingleFileConverter { + + /** + * Returns {@code true} if the converter can handle the file + */ + boolean canHandleFileName(String fileName) + + /** + * Returns the content of the converted file + */ + String convertContent(String rootName, ContractMetadata content) + + /** + * Returns the name of the converted file + */ + String generateOutputFileNameForInput(String inputFileName) +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/ConversionContractVerifierException.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/ConversionContractVerifierException.groovy new file mode 100644 index 0000000000..f6f9d5a6ee --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/ConversionContractVerifierException.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.ContractVerifierException + +/** + * Thrown when a a DSL can't be properly converted + * + * @since 1.0.0 + */ +@CompileStatic +class ConversionContractVerifierException extends ContractVerifierException { + + ConversionContractVerifierException(String message, Throwable cause) { + super(message, cause) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.groovy new file mode 100644 index 0000000000..3fe90afb25 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +import java.nio.charset.StandardCharsets + +/** + * Converts DSLs to WireMock stubs + * + * @since 1.0.0 + */ +@CompileStatic +class DslToWireMockClientConverter extends DslToWireMockConverter { + + @Override + String convertContent(String rootName, ContractMetadata contract) { + String dslContent = contract.path.getText(StandardCharsets.UTF_8.toString()) + return new WireMockStubStrategy(rootName, contract, createGroovyDSLFromStringContent(dslContent)).toWireMockClientStub() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy new file mode 100644 index 0000000000..7cb8172674 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.converter.SingleFileConverter +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter + +/** + * WireMock implementation of the {@link SingleFileConverter} + * + * @since 1.0.0 + */ +@CompileStatic +abstract class DslToWireMockConverter implements SingleFileConverter { + + @Override + boolean canHandleFileName(String fileName) { + return fileName.endsWith('.groovy') + } + + @Override + String generateOutputFileNameForInput(String inputFileName) { + return inputFileName.replaceAll('.groovy', '.json') + } + + protected Contract createGroovyDSLFromStringContent(String groovyDslAsString) { + return ContractVerifierDslConverter.convert(groovyDslAsString) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverter.groovy new file mode 100644 index 0000000000..56ddd3f99e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverter.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import com.google.common.collect.ListMultimap +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.converter.SingleFileConverter +import org.springframework.cloud.contract.verifier.file.ContractFileScanner +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +/** + * Recursively converts contracts into their stub representations + * + * @since 1.0.0 + */ +@Slf4j +@CompileStatic +class RecursiveFilesConverter { + + private final SingleFileConverter singleFileConverter + private final ContractVerifierConfigProperties properties + private final File outMappingsDir + + RecursiveFilesConverter(SingleFileConverter singleFileConverter, ContractVerifierConfigProperties properties) { + this.properties = properties + this.singleFileConverter = singleFileConverter + this.outMappingsDir = properties.stubsOutputDir + } + + RecursiveFilesConverter(SingleFileConverter singleFileConverter, ContractVerifierConfigProperties properties, File outMappingsDir) { + this.properties = properties + this.singleFileConverter = singleFileConverter + this.outMappingsDir = outMappingsDir + } + + void processFiles() { + ContractFileScanner scanner = new ContractFileScanner(properties.contractsDslDir, properties.excludedFiles as Set, [] as Set) + ListMultimap contracts = scanner.findContracts() + log.debug("Found the following contracts $contracts") + contracts.asMap().entrySet().each { entry -> + entry.value.each { ContractMetadata contract -> + File sourceFile = contract.path.toFile() + try { + if (!singleFileConverter.canHandleFileName(sourceFile.name)) { + return + } + String convertedContent = singleFileConverter.convertContent(entry.key.last().toString(), contract) + if (!convertedContent) { + return + } + Path absoluteTargetPath = createAndReturnTargetDirectory(sourceFile) + File newJsonFile = createTargetFileWithProperName(absoluteTargetPath, sourceFile) + newJsonFile.setText(convertedContent, StandardCharsets.UTF_8.toString()) + } catch (Exception e) { + throw new ConversionContractVerifierException("Unable to make conversion of ${sourceFile.name}", e) + } + } + } + } + + private Path createAndReturnTargetDirectory(File sourceFile) { + Path relativePath = Paths.get(properties.contractsDslDir.toURI()).relativize(sourceFile.parentFile.toPath()) + Path absoluteTargetPath = outMappingsDir.toPath().resolve(relativePath) + Files.createDirectories(absoluteTargetPath) + return absoluteTargetPath + } + + private File createTargetFileWithProperName(Path absoluteTargetPath, File sourceFile) { + File newJsonFile = new File(absoluteTargetPath.toFile(), singleFileConverter.generateOutputFileNameForInput(sourceFile.name)) + log.info("Creating new json [$newJsonFile.path]") + return newJsonFile + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy new file mode 100644 index 0000000000..960797316c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy @@ -0,0 +1,209 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import groovy.io.FileType +import groovy.json.JsonOutput +import groovy.json.JsonParserType +import groovy.json.JsonSlurper +import groovy.transform.CompileDynamic +import groovy.xml.XmlUtil +import org.springframework.cloud.contract.spec.Contract +import nl.flotsam.xeger.Xeger + +import java.nio.charset.StandardCharsets + +import static org.apache.commons.lang3.StringEscapeUtils.escapeJava + +/** + * Converts WireMock stubs into the DSL format + * + * @since 1.0.0 + */ +@CompileDynamic +class WireMockToDslConverter { + + /** + * Returns the string content of the contract + * + * @param wireMockStringStub - string content of the WireMock JSON stub + */ + static String fromWireMockStub(String wireMockStringStub) { + return new WireMockToDslConverter().convertFromWireMockStub(wireMockStringStub) + } + + private String convertFromWireMockStub(String wireMockStringStub) { + Object wireMockStub = parseStubDefinition(wireMockStringStub) + Integer priority = wireMockStub.priority + def request = wireMockStub.request + def response = wireMockStub.response + def bodyPatterns = request.bodyPatterns + String urlPattern = request.urlPattern + String urlPathPattern = request.urlPathPattern + return """\ + ${priority ? "priority ${priority}" : ''} + request { + ${request.method ? "method \"\"\"$request.method\"\"\"" : ""} + ${request.url ? "url \"\"\"$request.url\"\"\"" : ""} + ${urlPattern ? "url \$(client(regex('${escapeJava(urlPattern)}')), server('${new Xeger(escapeJava(urlPattern)).generate()}'))" : ""} + ${urlPathPattern ? "urlPath \$(client(regex('${escapeJava(urlPattern)}')), server('${new Xeger(escapeJava(urlPattern)).generate()}'))" : ""} + ${request.urlPath ? "url \"\"\"$request.urlPath\"\"\"" : ""} + ${ + request.headers ? """headers { + ${ + request.headers.collect { + def assertion = it.value + String headerName = it.key as String + def entry = assertion.entrySet().first() + """header(\"\"\"$headerName\"\"\", ${buildHeader(entry.key, entry.value)})\n""" + }.join('') + } + } + """ : "" + } + ${bodyPatterns?.equalTo?.every { it } ? "body('''${bodyPatterns.equalTo[0]}''')" : ''} + ${bodyPatterns?.equalToJson?.every { it } ? "body('''${bodyPatterns.equalToJson[0]}''')" : ''} + ${bodyPatterns?.matches?.every { it } ? "body \$(client(regex('${escapeJava(bodyPatterns.matches[0])}')), server('${new Xeger(escapeJava(bodyPatterns.matches[0])).generate()}'))" : ""} + } + response { + ${response.status ? "status $response.status" : ""} + ${response.body ? "body( ${buildBody(response.body)})" : ""} + ${ + response.headers ? """headers { + ${response.headers.collect { "header('$it.key': '${it.value}')\n" }.join('')} + } + """ : "" + } + } + """ + } + + private Object parseStubDefinition(String wireMockStringStub) { + new JsonSlurper().setType(JsonParserType.LAX).parseText(wireMockStringStub) + } + + private String buildHeader(String method, Object value) { + switch (method) { + case 'equalTo': + return wrapWithMultilineGString(value) + default: + return "regex(${wrapWithMultilineGString(escapeJava(value as String))})" + } + } + + private Object buildBody(Map responseBody) { + return responseBody.entrySet().collectAll(withQuotedMapStringElements()).inject([:], appendToIterable()) + } + + private Object buildBody(List responseBody) { + return responseBody.collectAll(withQuotedStringElements()).inject([], appendToIterable()) + } + + private Object buildBody(Integer responseBody) { + return responseBody + } + + private Object buildBody(String responseBody) { + try { + def json = new JsonSlurper().parseText(responseBody) + return wrapWithMultilineGString(JsonOutput.prettyPrint(responseBody)) + } catch (Exception jsonException) { + try { + def xml = new XmlSlurper().parseText(responseBody) + return wrapWithMultilineGString(XmlUtil.serialize(responseBody)) + } catch (Exception xmlException) { + return wrapWithMultilineGString(responseBody) + } + } + } + + private String wrapWithMultilineGString(String string) { + return """\"\"\"$string\"\"\"""" + } + + private Closure withQuotedMapStringElements() { + return { + [(it.key): convert(it.value)] + } + } + + private Closure withQuotedStringElements() { + return { + convert(it) + } + } + + private Closure appendToIterable() { + return { + acc, el -> acc << el + } + } + + private Object convert(Object element) { + return element + } + + private Object convert(String element) { + return quoteString(element) + } + + private String quoteString(String element) { + if (element =~ /^".*"$/) { + return element + } + return """\"\"\"${escapeJava(element)}\"\"\"""" + } + + private Object convert(List element) { + return element.collect { + convert(it) + } + } + + private Object convert(Map element) { + return element.collectEntries { + [(it.key): convert(it.value)] + } + } + + static void main(String[] args) { + String rootOfFolderWithStubs = args[0] + new File(rootOfFolderWithStubs).eachFileRecurse(FileType.FILES) { + try { + if (!it.name.endsWith('json')) { + return + } + String dslFromWireMockStub = fromWireMockStub(it.getText(StandardCharsets.UTF_8.toString())) + String dslWrappedWithFactoryMethod = wrapWithFactoryMethod(dslFromWireMockStub) + File newGroovyFile = new File(it.parent, it.name.replaceAll('json', 'groovy')) + println("Creating new groovy file [$newGroovyFile.path]") + newGroovyFile.setText(dslWrappedWithFactoryMethod, StandardCharsets.UTF_8.toString()) + } catch (Exception e) { + System.err.println(e) + } + + } + } + + static String wrapWithFactoryMethod(String dslFromWireMockStub) { + return """\ +${Contract.name}.make { + $dslFromWireMockStub +} +""" + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerTest.java new file mode 100644 index 0000000000..f60e4bf5db --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerTest.java @@ -0,0 +1,64 @@ +/** + * Copyright 2009 Wilfred Springer + * Copyright 2012 Jason Pell + * Copyright 2013 Antonio García-Domínguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package nl.flotsam.xeger; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class XegerTest { + + @Test + public void shouldGenerateTextCorrectly() { + String regex = "[ab]{4,6}c"; + Xeger generator = new Xeger(regex); + for (int i = 0; i < 100; i++) { + String text = generator.generate(); + assertTrue(text.matches(regex)); + } + } + + @Test + public void testRepeatableRegex() { + for (int x = 0; x < 1000; x++) { + Xeger generator = new Xeger("[ab]{4,6}c", new Random(1000)); + Xeger generator2 = new Xeger("[ab]{4,6}c", new Random(1000)); + + List firstRegexList = generateRegex(generator, 100); + List secondRegexList = generateRegex(generator2, 100); + + for (int i = 0; i < firstRegexList.size(); i++) { + assertEquals("Index mismatch: " + i, firstRegexList.get(i), + secondRegexList.get(i)); + } + } + } + + private List generateRegex(Xeger generator, int count) { + List regexList = new ArrayList(); + for (int i = 0; i < count; i++) { + regexList.add(generator.generate()); + } + return regexList; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerUtilsTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerUtilsTest.java new file mode 100644 index 0000000000..ccabfeb54d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/nl/flotsam/xeger/XegerUtilsTest.java @@ -0,0 +1,39 @@ +/** + * Copyright 2009 Wilfred Springer + * Copyright 2012 Jason Pell + * Copyright 2013 Antonio García-Domínguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package nl.flotsam.xeger; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import java.util.Random; + +import static org.junit.Assert.assertThat; + +public class XegerUtilsTest { + + @Test + public void shouldGenerateRandomNumberCorrectly() { + Random random = new Random(); + for (int i = 0; i < 100; i++) { + int number = Xeger.getRandomInt(3, 7, random); + assertThat(number, Matchers.greaterThanOrEqualTo(3)); + assertThat(number, Matchers.lessThanOrEqualTo(7)); + } + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy new file mode 100755 index 0000000000..dbd37a54f3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -0,0 +1,284 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.skyscreamer.jsonassert.JSONAssert +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import spock.lang.Issue +import spock.lang.Specification + +class DslToWireMockClientConverterSpec extends Specification { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + def "should convert DSL file to WireMock JSON"() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl1.groovy") + file.write(""" + org.springframework.cloud.contract.spec.Contract.make { + request { + method('PUT') + url \$(client(~/\\/[0-9]{2}/), server('/12')) + } + response { + status 200 + } + } +""") + when: + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals(''' +{"request":{"method":"PUT","urlPattern":"/[0-9]{2}"},"response":{"status":200}} +''', json, false) + } + + @Issue("196") + def "should creation of delayed stub responses be possible"() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl-delay.groovy") + file.write(""" + org.springframework.cloud.contract.spec.Contract.make { + request { + } + response { + status 200 + fixedDelayMilliseconds 1000 + } + } +""") + when: + String json = converter.convertContent("test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals(''' +{"request":{},"response":{"status":200,"fixedDelayMilliseconds":1000}} +''', json, false) + } + + def "should convert DSL file with a nested list to WireMock JSON"() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl2.groovy") + file.write(""" + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/api/12' + headers { + header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json' + + } + body ''' + [{ + "created_at": "Sat Jul 26 09:38:57 +0000 2014", + "id": 492967299297845248, + "id_str": "492967299297845248", + "text": "Gonna see you at Warsaw", + "place": + { + "attributes":{}, + "bounding_box": + { + "coordinates": + [[ + [-77.119759,38.791645], + [-76.909393,38.791645], + [-76.909393,38.995548], + [-77.119759,38.995548] + ]], + "type":"Polygon" + }, + "country":"United States", + "country_code":"US", + "full_name":"Washington, DC", + "id":"01fbe706f872cb32", + "name":"Washington", + "place_type":"city", + "url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json" + } + }] + ''' + } + response { + status 200 + } + } +""") + when: + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals(''' +{ + "request" : { + "url" : "/api/12", + "method" : "PUT", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[*].place.bounding_box.coordinates[*][*][?(@ == -77.119759)]" + }, { + "matchesJsonPath" : "$[*][?(@.text == 'Gonna see you at Warsaw')]" + }, { + "matchesJsonPath" : "$[*].place[?(@.place_type == 'city')]" + }, { + "matchesJsonPath" : "$[*][?(@.id == 492967299297845248)]" + }, { + "matchesJsonPath" : "$[*].place.bounding_box.coordinates[*][*][?(@ == 38.791645)]" + }, { + "matchesJsonPath" : "$[*].place[?(@.country == 'United States')]" + }, { + "matchesJsonPath" : "$[*][?(@.id_str == '492967299297845248')]" + }, { + "matchesJsonPath" : "$[*].place.bounding_box.coordinates[*][*][?(@ == -76.909393)]" + }, { + "matchesJsonPath" : "$[*].place[?(@.name == 'Washington')]" + }, { + "matchesJsonPath" : "$[*].place.bounding_box[?(@.type == 'Polygon')]" + }, { + "matchesJsonPath" : "$[*].place[?(@.url == 'http://api.twitter.com/1/geo/id/01fbe706f872cb32.json')]" + }, { + "matchesJsonPath" : "$[*].place.bounding_box.coordinates[*][*][?(@ == 38.995548)]" + }, { + "matchesJsonPath" : "$[*].place[?(@.country_code == 'US')]" + }, { + "matchesJsonPath" : "$[*].place[?(@.full_name == 'Washington, DC')]" + }, { + "matchesJsonPath" : "$[*][?(@.created_at == 'Sat Jul 26 09:38:57 +0000 2014')]" + }, { + "matchesJsonPath" : "$[*].place[?(@.id == '01fbe706f872cb32')]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json" + } + } + }, + "response" : { + "status" : 200 + } +} +''', json, false) + } + + + @Issue("262") + def "should create stub with map inside list"() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl-mapinlist.groovy") + file.write(""" + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath '/foos' + } + response { + status 200 + body([[id: value( + client('123'), + server(regex('[0-9]+')) + )], [id: value( + client('567'), + server(regex('[0-9]+')) + )]]) + headers { + header 'Content-Type': 'application/json' + } + } + } +""") + when: + String json = converter.convertContent("test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals(''' +{"request":{"urlPath":"/foos","method":"GET"},"response":{"body":"[{\\"id\\":\\"123\\"},{\\"id\\":\\"567\\"}]"}} +''', json, false) + } + + def 'should convert dsl to wiremock to show it in the docs'() { + given: + def converter = new DslToWireMockClientConverter() + and: + File file = tmpFolder.newFile("dsl_from_docs.groovy") + file.write(''' + org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(stub(optional(regex(email()))), test('abc@abc.com')), + callback_url: $(stub(regex(hostname())), test('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: value(stub("123123"), test(optional("123123"))), + message: "User not found by email == [${value(test(regex(email())), stub('not.existing@user.com'))}]" + ) + } + } + ''') + when: + String json = converter.convertContent("Test", new ContractMetadata(file.toPath(), false, 0, null)) + then: + JSONAssert.assertEquals( // tag::wiremock[] +''' +{ + "request" : { + "url" : "/users/password", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]" + }, { + "matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 404, + "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } + }, + "priority" : 1 +} +''' +// end::wiremock[] + , json, false) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverterSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverterSpec.groovy new file mode 100755 index 0000000000..0e3de65413 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/RecursiveFilesConverterSpec.groovy @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import org.apache.commons.io.FileUtils +import org.apache.commons.io.filefilter.TrueFileFilter +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.converter.SingleFileConverter +import spock.lang.Specification + +import java.nio.file.Path +import java.nio.file.Paths + +class RecursiveFilesConverterSpec extends Specification { + + private static + final Set EXPECTED_TARGET_FILES = [Paths.get("dslRoot.json"), Paths.get("dir1/dsl1.json"), Paths.get("dir1/dsl1b.json"), Paths.get("dir2/dsl2.json")] + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + def "should recursively convert all matching files"() { + given: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() + File originalSourceRootDirectory = new File(this.getClass().getResource("/converter/source").toURI()) + properties.contractsDslDir = tmpFolder.newFolder("source") + properties.stubsOutputDir = tmpFolder.newFolder("target") + FileUtils.copyDirectory(originalSourceRootDirectory, properties.contractsDslDir) + and: + def singleFileConverterStub = Stub(SingleFileConverter) + singleFileConverterStub.canHandleFileName(_) >> { String fileName -> fileName.endsWith(".groovy") } + singleFileConverterStub.convertContent(_, _) >> { "converted" } + singleFileConverterStub.generateOutputFileNameForInput(_) >> { String inputFileName -> inputFileName.replaceAll('.groovy', '.json') } + + RecursiveFilesConverter recursiveFilesConverter = new RecursiveFilesConverter(singleFileConverterStub, properties) + when: + recursiveFilesConverter.processFiles() + then: + Collection createdFiles = FileUtils.listFiles(properties.stubsOutputDir, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE) + Set relativizedCreatedFiles = getRelativePathsForFilesInDirectory(createdFiles, properties.stubsOutputDir) + EXPECTED_TARGET_FILES == relativizedCreatedFiles + and: + createdFiles.each { it.text == "converted" } + } + + def "should recursively convert matching files with exlusions"() { + given: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() + File originalSourceRootDirectory = new File(this.getClass().getResource("/converter/source").toURI()) + properties.contractsDslDir = tmpFolder.newFolder("source") + properties.stubsOutputDir = tmpFolder.newFolder("target") + properties.excludedFiles = ["dir1/**"] + FileUtils.copyDirectory(originalSourceRootDirectory, properties.contractsDslDir) + and: + def singleFileConverterStub = Stub(SingleFileConverter) + singleFileConverterStub.canHandleFileName(_) >> { String fileName -> fileName.endsWith(".groovy") } + singleFileConverterStub.convertContent(_, _) >> { "converted" } + singleFileConverterStub.generateOutputFileNameForInput(_) >> { String inputFileName -> inputFileName.replaceAll('.groovy', '.json') } + + RecursiveFilesConverter recursiveFilesConverter = new RecursiveFilesConverter(singleFileConverterStub, properties) + when: + recursiveFilesConverter.processFiles() + then: + Collection createdFiles = FileUtils.listFiles(properties.stubsOutputDir, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE) + Set relativizedCreatedFiles = getRelativePathsForFilesInDirectory(createdFiles, properties.stubsOutputDir) + [Paths.get("dslRoot.json"), Paths.get("dir2/dsl2.json")] as Set == relativizedCreatedFiles as Set + and: + createdFiles.each { it.text == "converted" } + } + + def "on failure should break processing and throw meaningful exception"() { + given: + def sourceFile = tmpFolder.newFile("test.groovy") + and: + def singleFileConverterStub = Stub(SingleFileConverter) + singleFileConverterStub.canHandleFileName(_) >> { true } + singleFileConverterStub.convertContent(_, _) >> { throw new NullPointerException("Test conversion error") } + singleFileConverterStub.generateOutputFileNameForInput(_) >> { String inputFileName -> "${inputFileName}2" } + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() + properties.contractsDslDir = tmpFolder.root + properties.stubsOutputDir = tmpFolder.root + RecursiveFilesConverter recursiveFilesConverter = new RecursiveFilesConverter(singleFileConverterStub, properties) + when: + recursiveFilesConverter.processFiles() + then: + def e = thrown(ConversionContractVerifierException) + e.message?.contains(sourceFile.name) + e.cause?.message == "Test conversion error" + } + + private + static Set getRelativePathsForFilesInDirectory(Collection createdFiles, File targetRootDirectory) { + Path rootSourcePath = Paths.get(targetRootDirectory.toURI()) + Set relativizedCreatedFiles = createdFiles.collect { File file -> + rootSourcePath.relativize(Paths.get(file.toURI())) + } + return relativizedCreatedFiles + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverterSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverterSpec.groovy new file mode 100755 index 0000000000..9b1e4c2367 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverterSpec.groovy @@ -0,0 +1,559 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter +import org.springframework.cloud.contract.spec.Contract +import spock.lang.Specification + +class WireMockToDslConverterSpec extends Specification { + + def 'should produce a Groovy DSL from WireMock stub'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "GET", + "url": "/path", + "headers" : { + "Accept": { + "matches": "text/.*" + }, + "X-Custom-Header": { + "contains": "2134" + } + } + }, + "response": { + "status": 200, + "body": '{"id": { "value": "132" }, "surname": "Kowalsky", "name": "Jan", "created": "2014-02-02 12:23:43" }', + "headers": { + "Content-Type": "text/plain" + } + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'GET' + url '/path' + headers { + header('Accept': $( + client(regex('text/.*')), + server('text/plain') + )) + header('X-Custom-Header': $( + client(regex('^.*2134.*$')), + server('121345') + )) + + } + } + response { + status 200 + body( + id: [value: '132'], + surname: 'Kowalsky', + name: 'Jan', + created: '2014-02-02 12:23:43' + ) + headers { + header 'Content-Type': 'text/plain' + + } + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") == expectedGroovyDsl + } + + + def 'should convert WireMock stub with response body containing JSON with escaped double quotes'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "DELETE", + "urlPattern": "/credit-card-verification-data/[0-9]+", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.mymoid-adapter.v2+json; charset=UTF-8" + } + } + }, + "response": { + "status": 200, + "body": "{\\"status\\": \\"OK\\"}", + "headers": { + "Content-Type": "application/json" + } + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'DELETE' + url $(client(~/\/credit-card-verification-data\/[0-9]+/), server('/credit-card-verification-data/1')) + headers { + header('Content-Type': 'application/vnd.mymoid-adapter.v2+json; charset=UTF-8') + } + } + response { + status 200 + body("""{ + "status": "OK" +}""") + headers { + header 'Content-Type': 'application/json' + + } + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") == expectedGroovyDsl + } + + def 'should convert WireMock stub with response body containing integer'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "POST", + "url": "/charge/count", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.creditcard-reporter.v1+json" + } + } + }, + "response": { + "status": 200, + "body": 200, + "headers": { + "Content-Type": "application/json" + } + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/charge/count' + headers { + header('Content-Type': 'application/vnd.creditcard-reporter.v1+json') + } + } + response { + status 200 + body(200) + headers { + header 'Content-Type': 'application/json' + + } + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") == expectedGroovyDsl + } + + def 'should convert WireMock stub with response body as a list'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "POST", + "url": "/charge/count", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.creditcard-reporter.v1+json" + } + } + }, + "response": { + "status": 200, + "body": '[ {"a":1, "c":"3"}, "b", "a" ]', + "headers": { + "Content-Type": "application/json" + } + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/charge/count' + headers { + header('Content-Type': 'application/vnd.creditcard-reporter.v1+json') + } + } + response { + status 200 + body([ + [a: 1, c: '3'], + 'b', + 'a' + ]) + headers { + header 'Content-Type': 'application/json' + } + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") == expectedGroovyDsl + } + + def 'should convert WireMock stub with response body containing a nested list'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "POST", + "url": "/charge/search?pageNumber=0&size=2147483647", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.creditcard-reporter.v1+json" + } + } + }, + "response": { + "status": 200, + "body": '[{"amount":1.01, "name":"Name", "info":{"title":"title1", "payload":null}, "booleanvalue":true, "user":null}, {"amount":2.01, "name":"Name2", "info":{"title":"title2", "payload":null}, "booleanvalue":true, "user":null}]' + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/charge/search?pageNumber=0&size=2147483647' + headers { + header('Content-Type': 'application/vnd.creditcard-reporter.v1+json') + } + } + response { + status 200 + body("""[ + { + "amount": 1.01, + "name": "Name", + "info": { + "title": "title1", + "payload": null + }, + "booleanvalue": true, + "user": null + }, + { + "amount": 2.01, + "name": "Name2", + "info": { + "title": "title2", + "payload": null + }, + "booleanvalue": true, + "user": null + } +]""") + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") == expectedGroovyDsl + } + + def 'should convert WireMock stub with request body checking equality to Json'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "POST", + "url": "/test", + "bodyPatterns": [{ + "equalTo": '{"property1":"abc", "property2":"2017-01", "property3":"666", "property4":1428566412}' + }] + }, + "response": { + "status": 200 + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/test' + body ('''{"property1":"abc","property2":"2017-01","property3":"666","property4":1428566412}''') + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + def 'should convert WireMock stub with request body checking matching to Json'() { + given: + String wireMockStub = '''\ +{ + "request": { + "method": "POST", + "url": "/test", + "bodyPatterns": [{ + "matches": "[0-9]{5}" + }] + }, + "response": { + "status": 200 + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/test' + body $(client(~/[0-9]{5}/), server('12345')) + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + def 'should convert WireMock stub with request body with equalToJson'() { + given: + String wireMockStub = '''\ +{ + "request" : { + "url" : "/test", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : '{"pan":"4855141150107894", "expirationDate":"2017-01", "dcvx":"178"}', + "jsonCompareMode" : "LENIENT" + } ] + }, + "response" : { + "status" : 200 + } +} +''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/test' + body '''{"pan":"4855141150107894","expirationDate":"2017-01","dcvx":"178"}''' + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + def 'should convert WireMock stub with request body with equalTo'() { + given: + String wireMockStub = '''\ + { + "request" : { + "url" : "/test", + "method" : "POST", + "bodyPatterns" : [ { + "equalTo" : '{"pan":"4855141150107894", "expirationDate":"2017-01", "dcvx":"178"}' + } ] + }, + "response" : { + "status" : 200 + } + } + ''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/test' + body '''{"pan":"4855141150107894","expirationDate":"2017-01","dcvx":"178"}''' + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + def 'should convert WireMock stub with request body with matches'() { + given: + String wireMockStub = '''\ + { + "request" : { + "url" : "/test", + "method" : "POST", + "bodyPatterns" : [ { + "matches" : "[0-9]{2}" + } ] + }, + "response" : { + "status" : 200 + } + } + ''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + request { + method 'POST' + url '/test' + body $(client(~/[0-9]{2}/), server('12')) + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + def 'should convert WireMock stub with priorities'() { + given: + String wireMockStub = '''\ + { + "priority" : 2, + "request" : { + "url" : "/test", + "method" : "POST" + }, + "response" : { + "status" : 200 + } + } + ''' + and: + stubMappingIsValidWireMockStub(wireMockStub) + and: + Contract expectedGroovyDsl = Contract.make { + priority 2 + request { + method 'POST' + url '/test' + } + response { + status 200 + } + } + when: + String groovyDsl = WireMockToDslConverter.fromWireMockStub(wireMockStub) + then: + Contract evaluatedGroovyDsl = ContractVerifierDslConverter.convert( + """org.springframework.cloud.contract.spec.Contract.make { + $groovyDsl + }""") + and: + evaluatedGroovyDsl == expectedGroovyDsl + } + + void stubMappingIsValidWireMockStub(String mappingDefinition) { + StubMapping.buildFrom(mappingDefinition) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WiremockScenarioConverterSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WiremockScenarioConverterSpec.groovy new file mode 100755 index 0000000000..64f0cbe13b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/WiremockScenarioConverterSpec.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.wiremock + +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import spock.lang.Specification + +import java.nio.file.Path +import java.nio.file.Paths + +class WiremockScenarioConverterSpec extends Specification { + + def "should generate first scenario step"() { + given: + DslToWireMockClientConverter converter = new DslToWireMockClientConverter() + Path dsl = Paths.get(this.getClass().getResource("/converter/scenario/main_scenario/01_login.groovy").toURI()) + when: + String content = converter.convertContent("Test", new ContractMetadata(dsl, false, 3, 0)) + then: + content.contains('"requiredScenarioState" : "Started"') + content.contains('"newScenarioState" : "Step1"') + content.contains('"scenarioName" : "Scenario_Test"') + } + + def "should generate mid scenario step"() { + given: + DslToWireMockClientConverter converter = new DslToWireMockClientConverter() + Path dsl = Paths.get(this.getClass().getResource("/converter/scenario/main_scenario/02_showCart.groovy").toURI()) + when: + String content = converter.convertContent("Test", new ContractMetadata(dsl, false, 3, 1)) + then: + content.contains('"requiredScenarioState" : "Step1"') + content.contains('"newScenarioState" : "Step2"') + content.contains('"scenarioName" : "Scenario_Test"') + } + + def "should generate last scenario step"() { + given: + DslToWireMockClientConverter converter = new DslToWireMockClientConverter() + Path dsl = Paths.get(this.getClass().getResource("/converter/scenario/main_scenario/03_logout.groovy").toURI()) + when: + String content = converter.convertContent("Test", new ContractMetadata(dsl, false, 3, 2)) + then: + content.contains('"requiredScenarioState" : "Step2"') + !content.contains('"newScenarioState"') + content.contains('"scenarioName" : "Scenario_Test"') + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/01_login.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/01_login.groovy new file mode 100644 index 0000000000..2911d82295 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/01_login.groovy @@ -0,0 +1,27 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('POST') + url '/login' + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/02_showCart.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/02_showCart.groovy new file mode 100644 index 0000000000..c7ed09b07c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/02_showCart.groovy @@ -0,0 +1,27 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('GET') + url '/cart' + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/03_logout.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/03_logout.groovy new file mode 100644 index 0000000000..221416f88a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/scenario/main_scenario/03_logout.groovy @@ -0,0 +1,27 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('POST') + url '/logout' + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1.groovy new file mode 100644 index 0000000000..c724017ffd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1.groovy @@ -0,0 +1,30 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('PUT') + headers { + header 'Content-Type': 'application/json' + } + url $(client('/[0-9]{2}'), server('/12')) + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1b.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1b.groovy new file mode 100644 index 0000000000..daaf2950e8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir1/dsl1b.groovy @@ -0,0 +1,30 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('PUT') + headers { + header 'Content-Type': 'application/json' + } + urlPattern $(client('/[0-9]{2}'), server('/12')) + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir2/dsl2.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir2/dsl2.groovy new file mode 100644 index 0000000000..c724017ffd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dir2/dsl2.groovy @@ -0,0 +1,30 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('PUT') + headers { + header 'Content-Type': 'application/json' + } + url $(client('/[0-9]{2}'), server('/12')) + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dslRoot.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dslRoot.groovy new file mode 100644 index 0000000000..c724017ffd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-converters/src/test/resources/converter/source/dslRoot.groovy @@ -0,0 +1,30 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('PUT') + headers { + header 'Content-Type': 'application/json' + } + url $(client('/[0-9]{2}'), server('/12')) + } + response { + status 200 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/FileSaver.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/FileSaver.groovy new file mode 100644 index 0000000000..6b350b34e3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/FileSaver.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.verifier.config.TestFramework + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +import static org.springframework.cloud.contract.verifier.util.NamesUtil.beforeLast +import static org.springframework.cloud.contract.verifier.util.NamesUtil.capitalize +import static org.springframework.cloud.contract.verifier.util.NamesUtil.packageToDirectory + +@CompileStatic +@Slf4j +class FileSaver { + + File targetDirectory + TestFramework framework + + FileSaver(File targetDirectory, TestFramework framework) { + this.targetDirectory = targetDirectory + this.framework = framework + } + + void saveClassFile(String fileName, String basePackageClass, String includedDirectoryRelativePath, byte[] classBytes) { + + Path testBaseDir = Paths.get(targetDirectory.absolutePath, packageToDirectory(basePackageClass), + beforeLast(includedDirectoryRelativePath, File.separator)) + Files.createDirectories(testBaseDir) + Path classPath = Paths.get(testBaseDir.toString(), capitalize(fileName) + framework.classExtension).toAbsolutePath() + log.info("Creating new class file [$classPath]") + Files.write(classPath, classBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy new file mode 100755 index 0000000000..5457f654a7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier + +import com.google.common.collect.ListMultimap +import groovy.transform.PackageScope +import org.apache.commons.lang3.StringUtils +import org.springframework.cloud.contract.spec.ContractVerifierException +import org.springframework.cloud.contract.verifier.builder.SingleTestGenerator +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.file.ContractFileScanner +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicInteger + +import static org.springframework.cloud.contract.verifier.util.NamesUtil.* + +/** + * @author Jakub Kubrynski, codearte.io + */ +class TestGenerator { + + private final ContractVerifierConfigProperties configProperties + private final String DEFAULT_CLASS_PREFIX = "ContractVerifier" + + private AtomicInteger counter = new AtomicInteger() + private SingleTestGenerator generator + private FileSaver saver + private ContractFileScanner contractFileScanner + + TestGenerator(ContractVerifierConfigProperties configProperties) { + this(configProperties, new SingleTestGenerator(configProperties), + new FileSaver(configProperties.generatedTestSourcesDir, configProperties.targetFramework)) + } + + TestGenerator(ContractVerifierConfigProperties configProperties, SingleTestGenerator generator, FileSaver saver) { + this.configProperties = configProperties + if (configProperties.contractsDslDir == null) { + throw new ContractVerifierException("Stubs directory not found under " + configProperties.contractsDslDir) + } + this.generator = generator + this.saver = saver + contractFileScanner = new ContractFileScanner(configProperties.contractsDslDir, + configProperties.excludedFiles as Set, + configProperties.ignoredFiles as Set) + } + + int generate() { + generateTestClasses(configProperties.basePackageForTests) + return counter.get() + } + + @PackageScope + void generateTestClasses(final String basePackageName) { + ListMultimap contracts = contractFileScanner.findContracts() + contracts.asMap().entrySet().each { + Map.Entry> entry -> processIncludedDirectory(relativizeContractPath(entry), entry.getValue(), basePackageName) + } + } + + private String relativizeContractPath(Map.Entry> entry) { + Path relativePath = configProperties.contractsDslDir.toPath().relativize(entry.getKey()) + if (StringUtils.isBlank(relativePath.toString())) { + return DEFAULT_CLASS_PREFIX + } + return relativePath.toString() + } + + private void processIncludedDirectory( + final String includedDirectoryRelativePath, Collection contracts, final String basePackageNameForClass) { + if (contracts.size()) { + def className = afterLast(includedDirectoryRelativePath.toString(), File.separator) + resolveNameSuffix() + def packageName = buildPackage(basePackageNameForClass, includedDirectoryRelativePath) + def classBytes = generator.buildClass(contracts, className, packageName).getBytes(StandardCharsets.UTF_8) + saver.saveClassFile(className, basePackageNameForClass, convertIllegalPackageChars(includedDirectoryRelativePath.toString()), classBytes) + counter.incrementAndGet() + } + } + + private String resolveNameSuffix() { + return configProperties.nameSuffixForTests ?: configProperties.targetFramework.classNameSuffix + } + + private static String buildPackage(final String packageNameForClass, final String includedDirectoryRelativePath) { + String directory = beforeLast(includedDirectoryRelativePath, File.separator) + return !directory.empty ? "$packageNameForClass.${directoryToPackage(convertIllegalPackageChars(directory))}" : packageNameForClass + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy new file mode 100644 index 0000000000..2f0fb85483 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +/** + * Builds a block of code. Allows to start, end, indent etc. pieces of code. + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@PackageScope +@CompileStatic +class BlockBuilder { + + private final StringBuilder builder + private final String spacer + private int indents + + /** + * @param spacer - char used for spacing + */ + BlockBuilder(String spacer) { + this.spacer = spacer + builder = new StringBuilder() + } + + BlockBuilder startBlock() { + indents++ + return this + } + + BlockBuilder endBlock() { + indents-- + return this + } + + BlockBuilder indent() { + startBlock().startBlock() + return this + } + + BlockBuilder unindent() { + endBlock().endBlock() + return this + } + + BlockBuilder addLine(String line) { + addIndentation() + builder << "$line\n" + return this + } + + BlockBuilder addEmptyLine() { + builder << '\n' + return this + } + + @CompileDynamic + private void addIndentation() { + indents.times { + builder << spacer + } + } + + BlockBuilder addBlock(MethodBuilder methodBuilder) { + startBlock() + methodBuilder.appendTo(this) + endBlock() + addEmptyLine() + return this + } + + BlockBuilder addAtTheEnd(String toAdd) { + if (builder.charAt(builder.length() - 1) as String == '\n') { + builder.replace(builder.length() - 1, builder.length(), toAdd) + builder << '\n' + } else { + builder << toAdd + } + return this + } + + @Override + String toString() { + return builder.toString() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy new file mode 100644 index 0000000000..4552ca7f05 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy @@ -0,0 +1,173 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.springframework.cloud.contract.verifier.config.TestFramework +import org.springframework.cloud.contract.verifier.util.NamesUtil +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +/** + * Builds a class. Adds all the imports, static imports etc. + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@CompileStatic +@PackageScope +class ClassBuilder { + + private final String className + private final String packageName + private final String baseClass + private final List imports = [] + private final List staticImports = [] + private final List rules = [] + private final List fields = [] + private final List methods = [] + private final List classLevelAnnotations = [] + private final TestFramework lang + + private ClassBuilder(String className, String packageName, String baseClass, TestFramework lang) { + this.lang = lang + if (baseClass) { + imports << baseClass + } + this.baseClass = NamesUtil.afterLastDot(baseClass) + this.packageName = packageName + this.className = className + } + + /** + * Returns a {@link ClassBuilder} for the given parameters + */ + static ClassBuilder createClass(String className, String classPackage, ContractVerifierConfigProperties properties) { + String baseClassForTests + if (properties.targetFramework == TestFramework.SPOCK && !properties.baseClassForTests) { + baseClassForTests = 'spock.lang.Specification' + } else { + baseClassForTests = properties.baseClassForTests + } + return new ClassBuilder(className, classPackage, baseClassForTests, properties.targetFramework) + } + + ClassBuilder addImport(String importToAdd) { + imports << importToAdd + return this + } + + ClassBuilder addImport(List importsToAdd) { + imports.addAll(importsToAdd) + return this + } + + ClassBuilder addStaticImport(String importToAdd) { + staticImports << importToAdd + return this + } + + ClassBuilder addMethod(MethodBuilder methodBuilder) { + methods << methodBuilder + return this + } + + ClassBuilder addField(String fieldToAdd) { + fields << appendColonIfJUniTest(fieldToAdd) + return this + } + + private String appendColonIfJUniTest(String field) { + if (lang == TestFramework.JUNIT && !field.endsWith(';')) { + return "$field;" + } + return field + } + + ClassBuilder addField(List fieldsToAdd) { + fields.addAll(fieldsToAdd.collect { appendColonIfJUniTest(it) }) + return this + } + + ClassBuilder addRule(String ruleClass) { + imports << ruleClass + rules << NamesUtil.afterLastDot(ruleClass) + return this + } + + String build() { + BlockBuilder clazz = new BlockBuilder("\t") + .addLine("package $packageName$lang.lineSuffix") + .addEmptyLine() + + imports.sort().each { + clazz.addLine("import $it$lang.lineSuffix") + } + if (!imports.empty) { + clazz.addEmptyLine() + } + + staticImports.sort().each { + clazz.addLine("import static $it$lang.lineSuffix") + } + if (!staticImports.empty) { + clazz.addEmptyLine() + } + + classLevelAnnotations.sort().each { + clazz.addLine(it) + } + + def classLine = "${lang.classModifier}class $className" + if (baseClass) { + classLine += " extends $baseClass" + } + clazz.addLine(classLine + ' {') + clazz.addEmptyLine() + + clazz.startBlock() + rules.sort().each { + clazz.addLine("@Rule") + clazz.addLine("public $it ${NamesUtil.camelCase(it)} = new $it()$lang.lineSuffix") + } + clazz.endBlock() + if (!rules.empty) { + clazz.addEmptyLine() + } + + clazz.startBlock() + fields.sort().each { + clazz.addLine(it) + } + if (!fields.empty) { + clazz.addEmptyLine() + } + clazz.endBlock() + + methods.each { + clazz.addBlock(it) + } + + clazz.addLine('}') + clazz.toString() + } + + void addClassLevelAnnotation(String annotation) { + classLevelAnnotations << annotation + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy new file mode 100644 index 0000000000..25063a8d7f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy @@ -0,0 +1,192 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.StringEscapeUtils +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.Input +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT + +/** + * Builds a JUnit method for messaging + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Marcin Grzejszczak + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@PackageScope +@TypeChecked +class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { + + JUnitMessagingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected String getInputString(Input request) { + if (request.triggeredBy) { + return request.triggeredBy.executionCommand + } + return "contractVerifierMessaging.send(inputMessage, \"${request.messageFrom.serverValue}\")" + } + + @Override + protected String getResponseBodyPropertyComparisonString(String property, String value) { + return "responseBody$property == \"${value}\"" + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("parsedJson.read('\\\$$property')")}") + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) { + processBodyElement(blockBuilder, property + "." + entry.key, entry.value) + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("assertThat(response.getHeader(\"$property\")).${createHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern pattern) { + blockBuilder.addLine("assertThat(response.getHeader(\"$property\")).${createHeaderComparison(pattern)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.getHeader(\"$property\")")};") + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + bb.addLine("""ContractVerifierMessage response = contractVerifierMessaging.receiveMessage("${outputMessage.sentTo.serverValue}");""") + bb.addLine("""assertThat(response).isNotNull();""") + outputMessage.headers?.executeForEachHeader { Header header ->\ + processHeaderElement(bb, header.name, header.serverValue) + } + } + + @Override + protected String getResponseAsString() { + return 'contractVerifierObjectMapper.writeValueAsString(response.getPayload())' + } + + @Override + protected String addCommentSignIfRequired(String baseString) { + return "// $baseString" + } + + @Override + protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { + blockBuilder.addAtTheEnd(JUNIT.lineSuffix) + return blockBuilder + } + + @Override + protected String getPropertyInListString(String property, Integer listIndex) { + return "$property[$listIndex]" ?: '' + } + + @Override + protected String convertUnicodeEscapesIfRequired(String json) { + return StringEscapeUtils.unescapeJavaScript(json) + } + + @Override + protected String getParsedXmlResponseBodyString(String responseString) { + return "Object responseBody = new XmlSlurper().parseText($responseString);" + } + + @Override + protected String getSimpleResponseBodyString(String responseString) { + return "Object responseBody = ($responseString);" + } + + @Override + protected String getInputString() { + String request = 'ContractVerifierMessage inputMessage = contractVerifierMessaging.create(' + if (inputMessage.messageBody) { + request = "${request}\n \"${StringEscapeUtils.escapeJava(bodyAsString)}\"\n " + } + if (inputMessage.messageHeaders) { + request = "${request}, headers()\n" + } + inputMessage.messageHeaders?.executeForEachHeader { Header header -> + request = "${request} ${getHeaderString(header)}" + } + return "${request})" + } + + @Override + protected String getHeaderString(Header header) { + return ".header(\"${getTestSideValue(header.name)}\", \"${getTestSideValue(header.serverValue)}\")" + } + + @Override + protected String getBodyString(String bodyAsString) { + return '' + } + + @Override + protected String getMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return "" + } + + @Override + protected String getParameterString(Map.Entry parameter) { + return "" + } + + protected String convertHeaderComparison(String headerValue) { + return " == '$headerValue'" + } + + protected String convertHeaderComparison(Pattern headerValue) { + return "==~ java.util.regex.Pattern.compile('$headerValue')" + } + + protected String createHeaderComparison(Object headerValue) { + String escapedHeader = convertUnicodeEscapesIfRequired("$headerValue") + return "isEqualTo(\"$escapedHeader\");" + } + + protected String createHeaderComparison(Pattern headerValue) { + String escapedHeader = convertUnicodeEscapesIfRequired("$headerValue") + return "matches(\"$escapedHeader\");" + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy new file mode 100644 index 0000000000..730ade10c2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.StringEscapeUtils +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +import static groovy.json.StringEscapeUtils.escapeJava +import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getJavaMultipartFileParameterContent + +/** + * Root class for JUnit method building + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Jakub Kubrynski, codearte.io + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +abstract class JUnitMethodBodyBuilder extends RequestProcessingMethodBodyBuilder { + + JUnitMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected String getResponseAsString() { + return "response.getBody().asString()" + } + + @Override + protected String addCommentSignIfRequired(String baseString) { + return "// $baseString" + } + + @Override + protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { + blockBuilder.addAtTheEnd(JUNIT.lineSuffix) + return blockBuilder + } + + @Override + protected String getResponseBodyPropertyComparisonString(String property, String value) { + return "assertThat(responseBody${property}).isEqualTo(\"${value}\")" + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("parsedJson.read(\"\\\$$property\")")};") + } + + @Override + protected String getPropertyInListString(String property, Integer listIndex) { + return "${property}.get($listIndex)" ?: '' + } + + @Override + protected String convertUnicodeEscapesIfRequired(String json) { + String unescapedJson = StringEscapeUtils.unescapeJavaScript(json) + return escapeJava(unescapedJson) + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) { + processBodyElement(blockBuilder, property + getMapKeyReferenceString(entry), entry.value) + } + + private String getMapKeyReferenceString(Map.Entry entry) { + if (entry.value instanceof ExecutionProperty) { + return "." + entry.key + } + return """.get(\\\"$entry.key\\\")""" + } + + @Override + protected String getParsedXmlResponseBodyString(String responseString) { + return "Object responseBody = new XmlSlurper().parseText($responseString);" + } + + @Override + protected String getSimpleResponseBodyString(String responseString) { + return "Object responseBody = ($responseString);" + } + + @Override + protected String getInputString(Request request) { + def inputString = 'ResponseOptions response = given().spec(request)' + if (response.async){ + inputString = inputString + '.when().async()' + } + return inputString + } + + @Override + protected String getInputString() { + return 'MockMvcRequestSpecification request = given()' + } + + @Override + protected String getHeaderString(Header header) { + return ".header(\"${getTestSideValue(header.name)}\", \"${getTestSideValue(header.serverValue)}\")" + } + + @Override + protected String getBodyString(String bodyAsString) { + return ".body(\"$bodyAsString\")" + } + + @Override + protected String getMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return getJavaMultipartFileParameterContent(propertyName, propertyValue) + } + + @Override + protected String getParameterString(Map.Entry parameter) { + return """.param("${escapeJava(parameter.key)}", "${escapeJava(parameter.value as String)}")""" + } + + protected String createHeaderComparison(Object headerValue) { + String escapedHeader = convertUnicodeEscapesIfRequired("$headerValue") + return "isEqualTo(\"$escapedHeader\");" + } + + protected String createHeaderComparison(Pattern headerValue) { + String escapedHeader = convertUnicodeEscapesIfRequired("$headerValue") + return "matches(\"$escapedHeader\");" + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy new file mode 100644 index 0000000000..61916f3197 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.QueryParameter +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT + +/** + * JaxRs implementation of the {@link JUnitMethodBodyBuilder}. Knows how to build + * a test method for JaxRs. + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +class JaxRsClientJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { + + JaxRsClientJUnitMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected void given(BlockBuilder bb) {} + + @Override + protected void givenBlock(BlockBuilder bb) { + } + + @Override + protected void when(BlockBuilder bb) { + bb.addLine("Response response = webTarget") + bb.indent() + + appendUrlPathAndQueryParameters(bb) + appendRequestWithRequiredResponseContentType(bb) + appendHeaders(bb) + appendMethodAndBody(bb) + bb.addAtTheEnd(JUNIT.lineSuffix) + + bb.unindent() + + bb.addEmptyLine() + bb.addLine("String responseAsString = response.readEntity(String.class);") + } + + protected void appendUrlPathAndQueryParameters(BlockBuilder bb) { + if (request.url) { + bb.addLine(".path(\"$request.url.serverValue\")") + appendQueryParams(request.url.queryParameters, bb) + } else if (request.urlPath) { + bb.addLine(".path(\"$request.urlPath.serverValue\")") + appendQueryParams(request.urlPath.queryParameters, bb) + } + } + + private void appendQueryParams(QueryParameters queryParameters, BlockBuilder bb) { + if (!queryParameters?.parameters) { + return + } + queryParameters.parameters.findAll(this.&allowedQueryParameter).each { QueryParameter param -> + bb.addLine(".queryParam(\"$param.name\", \"${resolveParamValue(param).toString()}\")") + } + } + + protected void appendMethodAndBody(BlockBuilder bb) { + String method = request.method.serverValue.toString().toLowerCase() + if (request.body) { + String contentType = getHeader('Content-Type') ?: getRequestContentType().mimeType + bb.addLine(".method(\"${method.toUpperCase()}\", entity(\"$bodyAsString\", \"$contentType\"))") + } else { + bb.addLine(".method(\"${method.toUpperCase()}\")") + } + } + + protected appendHeaders(BlockBuilder bb) { + request.headers?.executeForEachHeader { Header header -> + if (header.name == 'Content-Type' || header.name == 'Accept') return + bb.addLine(".header(\"${header.name}\", \"${header.serverValue}\")") + } + } + + protected void appendRequestWithRequiredResponseContentType(BlockBuilder bb) { + String acceptHeader = getHeader("Accept") + if (acceptHeader) { + bb.addLine(".request(\"$acceptHeader\")") + } else { + bb.addLine(".request()") + } + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + bb.addLine("assertThat(response.getStatus()).isEqualTo($response.status.serverValue);") + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + response.headers?.executeForEachHeader { Header header -> + processHeaderElement(bb, header.name, header.serverValue) + } + } + + protected String getHeader(String name) { + return request.headers?.entries.find { it.name == name }?.serverValue + } + + @Override + protected String getResponseAsString() { + return 'responseAsString' + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("assertThat(response.getHeaderString(\"$property\")).${createHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern pattern) { + blockBuilder.addLine("assertThat(response.getHeaderString(\"$property\")).${createHeaderComparison(pattern)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.getHeaderString(\"$property\")")};") + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy new file mode 100644 index 0000000000..97283c9596 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.QueryParameter +import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +/** + * Knows how to build a Spock test method for JaxRs. + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@PackageScope +@TypeChecked +class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestProcessingBodyBuilder { + + JaxRsClientSpockMethodRequestProcessingBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected void given(BlockBuilder bb) {} + + @Override + protected void givenBlock(BlockBuilder bb) { + } + + @Override + protected void when(BlockBuilder bb) { + bb.addLine("def response = webTarget") + bb.indent() + + appendUrlPathAndQueryParameters(bb) + appendRequestWithRequiredResponseContentType(bb) + appendHeaders(bb) + appendMethodAndBody(bb) + + bb.unindent() + + bb.addEmptyLine() + bb.addLine("String responseAsString = response.readEntity(String)") + } + + protected void appendRequestWithRequiredResponseContentType(BlockBuilder bb) { + String acceptHeader = getHeader("Accept") + if (acceptHeader) { + bb.addLine(".request('$acceptHeader')") + } else { + bb.addLine(".request()") + } + } + + protected void appendUrlPathAndQueryParameters(BlockBuilder bb) { + if (request.url) { + bb.addLine(".path('$request.url.serverValue')") + appendQueryParams(request.url.queryParameters, bb) + } else if (request.urlPath) { + bb.addLine(".path('$request.urlPath.serverValue')") + appendQueryParams(request.urlPath.queryParameters, bb) + } + } + + private void appendQueryParams(QueryParameters queryParameters, BlockBuilder bb) { + if (!queryParameters?.parameters) { + return + } + queryParameters.parameters.findAll(this.&allowedQueryParameter).each { QueryParameter param -> + bb.addLine(".queryParam('$param.name', '${resolveParamValue(param).toString()}')") + } + } + + protected void appendMethodAndBody(BlockBuilder bb) { + String method = request.method.serverValue.toString().toLowerCase() + if (request.body) { + String contentType = getHeader('Content-Type') ?: getRequestContentType().mimeType + bb.addLine(".method('${method.toUpperCase()}', entity('$bodyAsString', '$contentType'))") + } else { + bb.addLine(".method('${method.toUpperCase()}')") + } + } + + protected appendHeaders(BlockBuilder bb) { + request.headers?.executeForEachHeader { Header header -> + if (header.name == 'Content-Type' || header.name == 'Accept') return // Particular headers are set via 'request' / 'entity' methods + bb.addLine(".header('${header.name}', '${header.serverValue}')") + } + } + + protected String getHeader(String name) { + return request.headers?.entries.find { it.name == name }?.serverValue + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + bb.addLine("response.status == $response.status.serverValue") + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + response.headers?.executeForEachHeader { Header header -> + processHeaderElement(bb, header.name, header.serverValue) + } + } + + @Override + protected String getResponseAsString() { + return 'responseAsString' + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.getHeaderString(\'$property\')")}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("response.getHeaderString('$property') ${convertHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern value) { + blockBuilder.addLine("response.getHeaderString('$property') ${convertHeaderComparison(value)}") + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy new file mode 100644 index 0000000000..88366ce7b8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.JsonOutput +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Input +import org.springframework.cloud.contract.spec.internal.OutputMessage +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.util.ContentType + +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader + +/** + * Root class for messaging method building. + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { + + protected final Input inputMessage + protected final OutputMessage outputMessage + + MessagingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(configProperties) + this.inputMessage = stubDefinition.input + this.outputMessage = stubDefinition.outputMessage + } + + protected abstract String getInputString(Input request) + + @Override + protected boolean hasGivenSection() { + return !inputMessage.triggeredBy + } + + protected void processInput(BlockBuilder bb) { + + } + + protected void when(BlockBuilder bb) { + bb.addLine(getInputString(inputMessage)) + bb.indent() + + addColonIfRequired(bb) + bb.unindent() + } + + protected void then(BlockBuilder bb) { + validateResponseCodeBlock(bb) + if (inputMessage.assertThat) { + bb.addLine(inputMessage.assertThat.executionCommand) + addColonIfRequired(bb) + } + if (outputMessage) { + validateResponseHeadersBlock(bb) + if (outputMessage.body) { + bb.endBlock() + if (outputMessage.headers) { + bb.addLine(addCommentSignIfRequired('and:')).startBlock() + } + validateResponseBodyBlock(bb, outputMessage.body.serverValue) + } + if (outputMessage.assertThat) { + bb.addLine(outputMessage.assertThat.executionCommand) + addColonIfRequired(bb) + } + } + } + + protected ContentType getResponseContentType() { + ContentType contentType = recognizeContentTypeFromHeader(outputMessage.headers) + if (contentType == ContentType.UNKNOWN) { + contentType = recognizeContentTypeFromContent(outputMessage.body.serverValue) + } + return contentType + } + + protected String getBodyAsString() { + Object bodyValue = extractServerValueFromBody(inputMessage.messageBody.serverValue) + String json = new JsonOutput().toJson(bodyValue) + json = convertUnicodeEscapesIfRequired(json) + return trimRepeatedQuotes(json) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy new file mode 100644 index 0000000000..a0b2730efc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -0,0 +1,383 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.internal.* +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.cloud.contract.verifier.util.MapConverter + +import java.util.regex.Pattern + +import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue + +/** + * Main class for building method body. + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +abstract class MethodBodyBuilder { + + protected final ContractVerifierConfigProperties configProperties + + protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties) { + this.configProperties = configProperties + } + + /** + * Builds the response body validation code block + */ + protected abstract void validateResponseCodeBlock(BlockBuilder bb) + + /** + * Builds the response headers validation code block + */ + protected abstract void validateResponseHeadersBlock(BlockBuilder bb) + + /** + * Builds the code that returns response in the string format + */ + protected abstract String getResponseAsString() + + /** + * Returns the given string with comment sign if required by the given implementation + */ + protected abstract String addCommentSignIfRequired(String baseString) + + /** + * Adds a colon sign at the end of each line if necessary + */ + protected abstract BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) + + /** + * Builds the code that for the given {@code property} will compare it to + * the given {@code value} + */ + protected abstract String getResponseBodyPropertyComparisonString(String property, String value) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given body element + */ + protected abstract void processBodyElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given body element + */ + protected abstract void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given header element + */ + protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern pattern) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given header element + */ + protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given header element + */ + protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, String value) + + /** + * Appends to the {@link BlockBuilder} the code to retrieve a value for a property + * from the list with the given index + */ + protected abstract String getPropertyInListString(String property, Integer index) + + protected abstract String convertUnicodeEscapesIfRequired(String json) + + /** + * NOTE: XML support is experimental + */ + protected abstract String getParsedXmlResponseBodyString(String responseString) + + /** + * Builds the code that returns String from a body that is plain text + */ + protected abstract String getSimpleResponseBodyString(String responseString) + + /** + * Builds the code that returns the "message". For messaging it will be an input + * message. For REST it will be an input request. + */ + protected abstract String getInputString() + + /** + * Builds the code to append a header to the request / message + */ + protected abstract String getHeaderString(Header header) + + /** + * Builds the code to append body to the request / message + */ + protected abstract String getBodyString(String bodyAsString) + + /** + * Builds the code to append multipart content to the request. + * Not applicable for messaging. + */ + protected abstract String getMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) + + /** + * Builds the code to append a form parameter to the request. + * Not applicable for messaging. + */ + protected abstract String getParameterString(Map.Entry parameter) + + /** + * Appends the code to {@link BlockBuilder} for initial request processing + */ + protected abstract void processInput(BlockBuilder bb) + + /** + * Appends the code to {@link BlockBuilder} for the {@code //when} block + */ + protected abstract void when(BlockBuilder bb) + + /** + * Appends the code to {@link BlockBuilder} for the {@code //then} block + */ + protected abstract void then(BlockBuilder bb) + + /** + * Returns a {@link org.springframework.cloud.contract.verifier.util.ContentType} for the given request + */ + protected abstract ContentType getResponseContentType() + + /** + * Converts the body into String format + */ + protected abstract String getBodyAsString() + + /** + * Returns {@code true} if given section should be created + */ + protected abstract boolean hasGivenSection() + + /** + * Builds the test contents and appends them to {@link BlockBuilder} + */ + void appendTo(BlockBuilder blockBuilder) { + blockBuilder.startBlock() + + if (hasGivenSection()) { + givenBlock(blockBuilder) + } + whenBlock(blockBuilder) + thenBlock(blockBuilder) + + blockBuilder.endBlock() + } + + /** + * Prepares the {@code given} block section together with comments and indents + */ + protected void givenBlock(BlockBuilder bb) { + bb.addLine(addCommentSignIfRequired('given:')) + bb.startBlock() + given(bb) + bb.endBlock().addEmptyLine() + } + + /** + * Prepares the {@code when} block section together with comments and indents + */ + protected void whenBlock(BlockBuilder bb) { + bb.addLine(addCommentSignIfRequired('when:')) + bb.startBlock() + when(bb) + bb.endBlock().addEmptyLine() + } + + /** + * Prepares the {@code then} block section together with comments and indents + */ + protected void thenBlock(BlockBuilder bb) { + bb.addLine(addCommentSignIfRequired('then:')) + bb.startBlock() + then(bb) + bb.endBlock() + } + + /** + * Builds the {@code given} block section together with comments and indents + */ + protected void given(BlockBuilder bb) { + bb.addLine(getInputString()) + bb.indent() + processInput(bb) + addColonIfRequired(bb) + bb.unindent() + } + + /** + * Builds the response body verification part. The code will differ depending on the + * ContentType, type of response etc. The result will be appended to {@link BlockBuilder} + */ + protected void validateResponseBodyBlock(BlockBuilder bb, Object responseBody) { + ContentType contentType = getResponseContentType() + if (responseBody instanceof GString) { + responseBody = extractValue(responseBody, contentType, { DslProperty dslProperty -> dslProperty.serverValue }) + } + if (contentType == ContentType.JSON) { + appendJsonPath(bb, getResponseAsString()) + JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(responseBody) + jsonPaths.each { + String method = it.method() + String postProcessedMethod = postProcessJsonPathCall(method) + bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) + addColonIfRequired(bb) + } + processBodyElement(bb, "", responseBody) + } else if (contentType == ContentType.XML) { + bb.addLine(getParsedXmlResponseBodyString(getResponseAsString())) + addColonIfRequired(bb) + // TODO xml validation + } else { + bb.addLine(getSimpleResponseBodyString(getResponseAsString())) + processText(bb, "", responseBody as String) + addColonIfRequired(bb) + } + } + + /** + * Post processing of each JSON path entry + */ + protected String postProcessJsonPathCall(String jsonPath) { + return jsonPath + } + + /** + * Appends to {@link BlockBuilder} parsing of the JSON Path document + */ + protected void appendJsonPath(BlockBuilder blockBuilder, String json) { + blockBuilder.addLine(("DocumentContext parsedJson = JsonPath.parse($json)")) + addColonIfRequired(blockBuilder) + } + + /** + * Appends to {@link BlockBuilder} processing of the given String value. + */ + protected void processText(BlockBuilder blockBuilder, String property, String value) { + if (value.startsWith('$')) { + value = stripFirstChar(value).replaceAll('\\$value', "responseBody$property") + blockBuilder.addLine(value) + addColonIfRequired(blockBuilder) + } else { + blockBuilder.addLine(getResponseBodyPropertyComparisonString(property, value)) + } + } + + private String stripFirstChar(String s) { + return s.substring(1); + } + + /** + * Appends to the {@link BlockBuilder} the assertion for the given header element + */ + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Object value) { + } + + /** + * Appends to the {@link BlockBuilder} the assertion for the given body element + */ + protected void processBodyElement(BlockBuilder blockBuilder, String property, Object value) { + } + + /** + * Removes unnecessary quotes + */ + protected String trimRepeatedQuotes(String toTrim) { + return toTrim.startsWith('"') ? toTrim.replaceAll('"', '') : toTrim + } + + /** + * Converts the passed body into ints server side representation. All {@link DslProperty} + * will return their server side values + */ + protected Object extractServerValueFromBody(bodyValue) { + if (bodyValue instanceof GString) { + bodyValue = extractValue(bodyValue, { DslProperty dslProperty -> dslProperty.serverValue }) + } else { + bodyValue = MapConverter.transformValues(bodyValue, { it instanceof DslProperty ? it.serverValue : it }) + } + return bodyValue + } + + /** + * Converts the {@link org.springframework.cloud.contract.spec.internal.QueryParameter} server side value into its String + * representation + */ + protected String resolveParamValue(QueryParameter param) { + return resolveParamValue(param.serverValue) + } + + /** + * Converts the query parameter value into String + */ + protected String resolveParamValue(Object value) { + return value.toString() + } + + /** + * Converts the query parameter value into String + */ + protected String resolveParamValue(MatchingStrategy matchingStrategy) { + return matchingStrategy.serverValue.toString() + } + + /** + * Depending on the object type extracts the test side values and + * combines them into a String representation + */ + protected String getTestSideValue(Object object) { + return MapConverter.getTestSideValues(object).toString() + } + + /** + * Appends to the {@link BlockBuilder} the assertion for the given body element + */ + protected void processBodyElement(BlockBuilder blockBuilder, String property, Map map) { + map.each { + processBodyElement(blockBuilder, property, it) + } + } + + /** + * Appends to the {@link BlockBuilder} the assertion for the given body element + */ + protected void processBodyElement(BlockBuilder blockBuilder, String property, List list) { + list.eachWithIndex { listElement, listIndex -> + String prop = getPropertyInListString(property, listIndex as Integer) + processBodyElement(blockBuilder, prop, listElement) + } + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy new file mode 100644 index 0000000000..0d17899d91 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy @@ -0,0 +1,96 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.config.TestFramework +import org.springframework.cloud.contract.verifier.util.NamesUtil +import org.springframework.cloud.contract.verifier.config.TestMode +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Builds a test method. Adds an ignore annotation on a method if necessary. + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@Slf4j +@CompileStatic +@PackageScope +class MethodBuilder { + + private final String methodName + private final Contract stubContent + private final ContractVerifierConfigProperties configProperties + private final boolean ignored + + private MethodBuilder(String methodName, Contract stubContent, ContractVerifierConfigProperties configProperties, boolean ignored) { + this.ignored = ignored + this.stubContent = stubContent + this.methodName = methodName + this.configProperties = configProperties + } + + /** + * A factory method that creates a {@link MethodBuilder} for the given arguments + */ + static MethodBuilder createTestMethod(ContractMetadata contract, File stubsFile, Contract stubContent, ContractVerifierConfigProperties configProperties) { + log.debug("Stub content Groovy DSL [$stubContent]") + String methodName = NamesUtil.camelCase(NamesUtil.toLastDot(NamesUtil.afterLast(stubsFile.path, File.separator))) + return new MethodBuilder(methodName, stubContent, configProperties, contract.ignored) + } + + /** + * Appends to the {@link BlockBuilder} the contents of the test + */ + void appendTo(BlockBuilder blockBuilder) { + if (configProperties.targetFramework == TestFramework.JUNIT) { + blockBuilder.addLine('@Test') + } + if (ignored) { + blockBuilder.addLine('@Ignore') + } + blockBuilder.addLine(configProperties.targetFramework.methodModifier + "validate_$methodName() throws Exception {") + getMethodBodyBuilder().appendTo(blockBuilder) + blockBuilder.addLine('}') + } + + private MethodBodyBuilder getMethodBodyBuilder() { + if (stubContent.input || stubContent.outputMessage) { + if (configProperties.targetFramework == TestFramework.JUNIT){ + return new JUnitMessagingMethodBodyBuilder(stubContent, configProperties) + } + return new SpockMessagingMethodBodyBuilder(stubContent, configProperties) + } + if (configProperties.testMode == TestMode.MOCKMVC && configProperties.targetFramework == TestFramework.JUNIT){ + return new MockMvcJUnitMethodBodyBuilder(stubContent, configProperties) + } + if (configProperties.testMode == TestMode.JAXRSCLIENT) { + if (configProperties.targetFramework == TestFramework.JUNIT){ + return new JaxRsClientJUnitMethodBodyBuilder(stubContent, configProperties) + } + return new JaxRsClientSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties) + } + return new MockMvcSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcJUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcJUnitMethodBodyBuilder.groovy new file mode 100644 index 0000000000..d4556732aa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcJUnitMethodBodyBuilder.groovy @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +/** + * A {@link JUnitMethodBodyBuilder} implementation that uses MockMvc to send requests. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +class MockMvcJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { + + MockMvcJUnitMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + bb.addLine("assertThat(response.statusCode()).isEqualTo($response.status.serverValue);") + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + response.headers?.executeForEachHeader { Header header ->\ + processHeaderElement(bb, header.name, header.serverValue) + } + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("assertThat(response.header(\"$property\")).${createHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern pattern) { + blockBuilder.addLine("assertThat(response.header(\"$property\")).${createHeaderComparison(pattern)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.header(\"$property\")")};") + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy new file mode 100644 index 0000000000..a1655e52ce --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +/** + * A {@link SpockMethodRequestProcessingBodyBuilder} implementation that uses MockMvc to send requests. + * + * @since 1.0.0 + */ +@PackageScope +@TypeChecked +class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestProcessingBodyBuilder { + + MockMvcSpockMethodRequestProcessingBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + bb.addLine("response.statusCode == $response.status.serverValue") + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + response.headers?.executeForEachHeader { Header header -> + processHeaderElement(bb, header.name, header.serverValue) + } + } + + @Override + protected String getResponseAsString() { + return 'response.body.asString()' + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.header(\'$property\')")}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("response.header('$property') ${convertHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern value) { + blockBuilder.addLine("response.header('$property') ${convertHeaderComparison(value)}") + } + + // #273 - should escape $ for Groovy since it will try to make it a GString + @Override + protected String postProcessJsonPathCall(String jsonPath) { + return jsonPath.replace('$', '\\$') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy new file mode 100644 index 0000000000..f1251acd6f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -0,0 +1,209 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.JsonOutput +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import groovy.transform.TypeCheckingMode +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.QueryParameter +import org.springframework.cloud.contract.spec.internal.Response +import org.springframework.cloud.contract.spec.internal.Url +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.MapConverter + +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader + +/** + * An abstraction for creating a test method that includes processing of an HTTP request + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @author Olga Maciaszek-Sharma, codearte.io + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { + + protected final Request request + protected final Response response + + RequestProcessingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(configProperties) + this.request = stubDefinition.request + this.response = stubDefinition.response + } + + /** + * Returns code used to retrieve a response for the given {@link Request} + */ + protected abstract String getInputString(Request request) + + @Override + protected boolean hasGivenSection() { + return true + } + + /** + * Returns {@code true} if the query parameter is allowed + */ + protected boolean allowedQueryParameter(QueryParameter param) { + return allowedQueryParameter(param.serverValue) + } + + /** + * Returns {@code true} if the query parameter is allowed + */ + protected boolean allowedQueryParameter(MatchingStrategy matchingStrategy) { + return matchingStrategy.type != MatchingStrategy.Type.ABSENT + } + + /** + * Returns {@code true} if the query parameter is allowed + */ + protected boolean allowedQueryParameter(Object o) { + return true + } + + @Override + protected void processInput(BlockBuilder bb) { + request.headers?.executeForEachHeader { Header header -> + bb.addLine(getHeaderString(header)) + } + if (request.body) { + bb.addLine(getBodyString(bodyAsString)) + } + if (request.multipart) { + multipartParameters?.each { Map.Entry entry -> bb.addLine(getMultipartParameterLine(entry)) } + } + } + + @Override + protected void when(BlockBuilder bb) { + bb.addLine(getInputString(request)) + bb.indent() + + String url = buildUrl(request) + String method = request.method.serverValue.toString().toLowerCase() + + bb.addLine(/.${method}("$url")/) + addColonIfRequired(bb) + bb.unindent() + } + + @Override + protected void then(BlockBuilder bb) { + validateResponseCodeBlock(bb) + if (response.headers) { + validateResponseHeadersBlock(bb) + } + if (response.body) { + bb.endBlock() + bb.addLine(addCommentSignIfRequired('and:')).startBlock() + validateResponseBodyBlock(bb, response.body.serverValue) + } + } + + @Override + protected ContentType getResponseContentType() { + ContentType contentType = recognizeContentTypeFromHeader(response.headers) + if (contentType == ContentType.UNKNOWN) { + contentType = recognizeContentTypeFromContent(response.body.serverValue) + } + return contentType + } + + @Override + protected String getBodyAsString() { + Object bodyValue = extractServerValueFromBody(request.body.serverValue) + String json = new JsonOutput().toJson(bodyValue) + json = convertUnicodeEscapesIfRequired(json) + return trimRepeatedQuotes(json) + } + + /** + * Returns a map of server side multipart parameters + */ + protected Map getMultipartParameters() { + return (Map) request?.multipart?.serverValue + } + + /** + * Maps the {@link Request} into a {@link ContentType} + */ + protected ContentType getRequestContentType() { + ContentType contentType = recognizeContentTypeFromHeader(request.headers) + if (contentType == ContentType.UNKNOWN) { + contentType = recognizeContentTypeFromContent(request.body.serverValue) + } + return contentType + } + + /** + * Builds a String URL from {@link Request}'s test side values. It can be + * a concrete value of the URL or a path. + */ + protected String buildUrl(Request request) { + if (request.url) + return getTestSideValue(buildUrlFromUrlPath(request.url)) + if (request.urlPath) + return getTestSideValue(buildUrlFromUrlPath(request.urlPath)) + throw new IllegalStateException("URL is not set!") + } + + /** + * Depending on the presence of query parameters builds the String value + * of the URL. Retrieves any present test side values + */ + @TypeChecked(TypeCheckingMode.SKIP) + protected String buildUrlFromUrlPath(Url url) { + if (hasQueryParams(url)) { + String params = url.queryParameters.parameters + .findAll(this.&allowedQueryParameter) + .inject([] as List) { List result, QueryParameter param -> + result << "${param.name}=${resolveParamValue(param).toString()}" + } + .join('&') + return "${MapConverter.getTestSideValues(url.serverValue)}?$params" + } + return MapConverter.getTestSideValues(url.serverValue) + } + + /** + * Returns a line of code to send a multi part parameter in the request + */ + protected String getMultipartParameterLine(Map.Entry parameter) { + if (parameter.value instanceof NamedProperty) { + return ".multiPart(${getMultipartFileParameterContent(parameter.key, (NamedProperty) parameter.value)})" + } + return getParameterString(parameter) + } + + + private boolean hasQueryParams(Url url) { + return url.queryParameters + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy new file mode 100644 index 0000000000..dcfa2d4757 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy @@ -0,0 +1,176 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.Canonical +import groovy.transform.EqualsAndHashCode +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.config.TestFramework +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter +import org.springframework.cloud.contract.verifier.config.TestMode + +import static ClassBuilder.createClass +import static MethodBuilder.createTestMethod +import static org.springframework.cloud.contract.verifier.util.NamesUtil.capitalize + +/** + * Builds a single test for the given {@link ContractVerifierConfigProperties properties} + * + * @since 1.0.0 + */ +@Slf4j +class SingleTestGenerator { + + private static final String JSON_ASSERT_STATIC_IMPORT = 'com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson' + private static final String JSON_ASSERT_CLASS = 'com.toomuchcoding.jsonassert.JsonAssertion' + + private final ContractVerifierConfigProperties configProperties + + SingleTestGenerator(ContractVerifierConfigProperties configProperties) { + this.configProperties = configProperties + } + + /** + * Returns String code representing a test class with test methods for + * each {@link ContractMetadata} + */ + @PackageScope + String buildClass(Collection listOfFiles, String className, String classPackage) { + ClassBuilder clazz = createClass(capitalize(className), classPackage, configProperties) + + if (configProperties.imports) { + configProperties.imports.each { + clazz.addImport(it) + } + } + + if (listOfFiles.ignored.find { it }) { + clazz.addImport(configProperties.targetFramework.getIgnoreClass()) + } + + if (configProperties.staticImports) { + configProperties.staticImports.each { + clazz.addStaticImport(it) + } + } + + if (isScenarioClass(listOfFiles)) { + clazz.addImport(configProperties.targetFramework.getOrderAnnotationImport()) + clazz.addClassLevelAnnotation(configProperties.targetFramework.getOrderAnnotation()) + } + + addJsonPathRelatedImports(clazz) + + Map contracts = mapContractsToTheirTestTypes(listOfFiles) + + boolean conditionalImportsAdded = false + contracts.each { ParsedDsl key, TestType value -> + if (!conditionalImportsAdded) { + if (contracts.values().contains(TestType.HTTP)) { + if (configProperties.testMode == TestMode.JAXRSCLIENT) { + clazz.addStaticImport('javax.ws.rs.client.Entity.*') + if (configProperties.targetFramework == TestFramework.JUNIT) { + clazz.addImport('javax.ws.rs.core.Response') + } + } else if (configProperties.testMode == TestMode.MOCKMVC) { + clazz.addStaticImport('com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*') + } else { + clazz.addStaticImport('com.jayway.restassured.RestAssured.*') + } + } + if (configProperties.targetFramework == TestFramework.JUNIT) { + if (contracts.values().contains(TestType.HTTP) && configProperties.testMode == TestMode.MOCKMVC) { + clazz.addImport('com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification') + clazz.addImport('com.jayway.restassured.response.ResponseOptions') + } + clazz.addImport('org.junit.Test') + clazz.addStaticImport('org.assertj.core.api.Assertions.assertThat') + } + if (configProperties.ruleClassForTests) { + clazz.addImport('org.junit.Rule').addRule(configProperties.ruleClassForTests) + } + if (contracts.values().contains(TestType.MESSAGING)) { + addMessagingRelatedEntries(clazz) + } + conditionalImportsAdded = true + } + clazz.addMethod(createTestMethod(key.contract, key.stubsFile, key.groovyDsl, configProperties)) + } + return clazz.build() + } + + private Map mapContractsToTheirTestTypes(Collection listOfFiles) { + return listOfFiles.collectEntries { + File stubsFile = it.path.toFile() + log.debug("Stub content from file [${stubsFile.text}]") + org.springframework.cloud.contract.spec.Contract stubContent = ContractVerifierDslConverter.convert(stubsFile) + TestType testType = (stubContent.input || stubContent.outputMessage) ? TestType.MESSAGING : TestType.HTTP + return [(new ParsedDsl(it, stubContent, stubsFile)): testType] + } + } + + @Canonical + @EqualsAndHashCode + private static class ParsedDsl { + ContractMetadata contract + org.springframework.cloud.contract.spec.Contract groovyDsl + File stubsFile + } + + private static enum TestType { + MESSAGING, HTTP + } + + private boolean isScenarioClass(Collection listOfFiles) { + listOfFiles.find({ it.order != null }) != null + } + + private ClassBuilder addJsonPathRelatedImports(ClassBuilder clazz) { + clazz.addImport(['com.jayway.jsonpath.DocumentContext', + 'com.jayway.jsonpath.JsonPath', + ]) + if (jsonAssertPresent()) { + clazz.addStaticImport(JSON_ASSERT_STATIC_IMPORT) + } + } + + private ClassBuilder addMessagingRelatedEntries(ClassBuilder clazz) { + clazz.addField(['@Inject ContractVerifierMessaging contractVerifierMessaging', + 'ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper()' + ]) + clazz.addImport([ 'javax.inject.Inject', + 'org.springframework.cloud.contract.verifier.messaging.ContractVerifierObjectMapper', + 'org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage', + 'org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging', + ]) + clazz.addStaticImport('org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessagingUtil.headers') + } + + private static boolean jsonAssertPresent() { + try { + Class.forName(JSON_ASSERT_CLASS) + return true + } catch (ClassNotFoundException e) { + log.debug("JsonAssert is not present on classpath. Will not add a static import.") + return false + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy new file mode 100644 index 0000000000..639d28bbd6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy @@ -0,0 +1,185 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.StringEscapeUtils +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.Input +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern +/** + * @author Jakub Kubrynski, codearte.io + */ +@PackageScope +@TypeChecked +class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { + + SpockMessagingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected String getInputString(Input request) { + if (request.triggeredBy) { + return request.triggeredBy.executionCommand + } + return "contractVerifierMessaging.send(inputMessage, '${request.messageFrom.serverValue}')" + } + + @Override + protected String getResponseBodyPropertyComparisonString(String property, String value) { + return "responseBody$property == \"${value}\"" + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("parsedJson.read('\\\$$property')")}") + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) { + processBodyElement(blockBuilder, property + "." + entry.key, entry.value) + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("response.getHeader(\'$property\')")}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, String value) { + blockBuilder.addLine("response.getHeader('$property') ${convertHeaderComparison(value)}") + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, Pattern value) { + blockBuilder.addLine("response.getHeader('$property') ${convertHeaderComparison(value)}") + } + + @Override + protected void validateResponseCodeBlock(BlockBuilder bb) { + if (outputMessage) { + bb.addLine("""def response = contractVerifierMessaging.receiveMessage('${outputMessage.sentTo.serverValue}')""") + bb.addLine("""assert response != null""") + } else { + bb.addLine('noExceptionThrown()') + } + } + + @Override + protected void validateResponseHeadersBlock(BlockBuilder bb) { + outputMessage.headers?.executeForEachHeader { Header header -> + processHeaderElement(bb, header.name, header.serverValue) + } + } + + @Override + protected String getResponseAsString() { + return 'contractVerifierObjectMapper.writeValueAsString(response.payload)' + } + + @Override + protected String addCommentSignIfRequired(String baseString) { + return baseString + } + + @Override + protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { + return blockBuilder + } + + @Override + protected String getPropertyInListString(String property, Integer listIndex) { + "$property[$listIndex]" ?: '' + } + + @Override + protected String convertUnicodeEscapesIfRequired(String json) { + return StringEscapeUtils.unescapeJavaScript(json) + } + + @Override + protected String getParsedXmlResponseBodyString(String responseString) { + return "def responseBody = new XmlSlurper().parseText($responseString)" + } + + @Override + protected String getSimpleResponseBodyString(String responseString) { + return "def responseBody = ($responseString)" + } + + @Override + protected String getInputString() { + String request = 'def inputMessage = contractVerifierMessaging.create(' + if (inputMessage.messageBody) { + request = "${request}'''${bodyAsString}'''\n " + } + if (inputMessage.messageHeaders) { + request = "${request},[\n" + } + def headers = [] + inputMessage.messageHeaders?.executeForEachHeader { Header header -> + headers << " ${getHeaderString(header)}" + } + request = "${request}${headers.join(',\n')}" + if (inputMessage.messageHeaders) { + request = "${request}\n ]" + } + return "${request})" + } + + @Override + protected String getHeaderString(Header header) { + return "'${getTestSideValue(header.name)}': '${getTestSideValue(header.serverValue)}'" + } + + @Override + protected String getBodyString(String bodyAsString) { + return '' + } + + @Override + protected String getMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return '' + } + + @Override + protected String getParameterString(Map.Entry parameter) { + return '' + } + + protected String convertHeaderComparison(String headerValue) { + return " == '$headerValue'" + } + + protected String convertHeaderComparison(Pattern headerValue) { + return "==~ java.util.regex.Pattern.compile('$headerValue')" + } + + // #273 - should escape $ for Groovy since it will try to make it a GString + @Override + protected String postProcessJsonPathCall(String method) { + return method.replace('$', '\\$') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy new file mode 100644 index 0000000000..a9d8274509 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy @@ -0,0 +1,135 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.StringEscapeUtils +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getGroovyMultipartFileParameterContent + +/** + * A {@link RequestProcessingMethodBodyBuilder} implementation that uses Spock + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@PackageScope +@TypeChecked +abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessingMethodBodyBuilder { + + SpockMethodRequestProcessingBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { + super(stubDefinition, configProperties) + } + + @Override + protected String getResponseBodyPropertyComparisonString(String property, String value) { + return "responseBody$property == \"${value}\"" + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { + blockBuilder.addLine("${exec.insertValue("parsedJson.read('\\\$$property')")}") + } + + @Override + protected void processBodyElement(BlockBuilder blockBuilder, String property, Map.Entry entry) { + processBodyElement(blockBuilder, property + "." + entry.key, entry.value) + } + + @Override + protected String addCommentSignIfRequired(String baseString) { + return baseString + } + + @Override + protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { + return blockBuilder + } + + @Override + protected String getPropertyInListString(String property, Integer listIndex) { + "$property[$listIndex]" ?: '' + } + + @Override + protected String convertUnicodeEscapesIfRequired(String json) { + return StringEscapeUtils.unescapeJavaScript(json) + } + + @Override + protected String getParsedXmlResponseBodyString(String responseString) { + return "def responseBody = new XmlSlurper().parseText($responseString)" + } + + @Override + protected String getSimpleResponseBodyString(String responseString) { + return "def responseBody = ($responseString)" + } + + @Override + protected String getInputString(Request request) { + def inputString = 'def response = given().spec(request)' + if (response.async){ + inputString = inputString + '.when().async()' + } + return inputString + } + + @Override + protected String getInputString() { + return 'def request = given()' + } + + @Override + protected String getHeaderString(Header header) { + return ".header('${getTestSideValue(header.name)}', '${getTestSideValue(header.serverValue)}')" + } + + @Override + protected String getBodyString(String bodyAsString) { + return ".body('''$bodyAsString''')" + } + + @Override + protected String getMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return getGroovyMultipartFileParameterContent(propertyName, propertyValue) + } + + @Override + protected String getParameterString(Map.Entry parameter) { + return ".param('$parameter.key', '$parameter.value')" + } + + protected String convertHeaderComparison(String headerValue) { + return " == '$headerValue'" + } + + protected String convertHeaderComparison(Pattern headerValue) { + return "==~ java.util.regex.Pattern.compile('$headerValue')" + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy new file mode 100644 index 0000000000..366c9c4c96 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.config + +/** + * Represents Contract Verifier configuration properties + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +class ContractVerifierConfigProperties { + + /** + * For which unit test library tests should be generated + */ + TestFramework targetFramework = TestFramework.JUNIT + + /** + * Which mechanism should be used to invoke REST calls during tests + */ + TestMode testMode = TestMode.MOCKMVC + + /** + * Base package for generated tests + */ + String basePackageForTests + + /** + * Class which all generated tests should extend + */ + String baseClassForTests + + /** + * Suffix for generated test classes, like Spec or Test + */ + String nameSuffixForTests + + /** + * Rule class that should be added to generated tests + */ + String ruleClassForTests + + /** + * Patterns that should not be taken into account for processing + */ + List excludedFiles = [] + + /** + * Patterns for which generated tests should be @Ignored + */ + List ignoredFiles = [] + + /** + * Imports that should be added to generated tests + */ + String[] imports = [] + + /** + * Static imports that should be added to generated tests + */ + String[] staticImports = [] + + /** + * Directory containing contracts written using the GroovyDSL + */ + File contractsDslDir + + /** + * Test source directory where tests generated from Groovy DSL should be placed + */ + File generatedTestSourcesDir + + /** + * Dir where the generated WireMock stubs from Groovy DSL should be placed. + * You can then mention them in your packaging task to create jar with stubs + */ + File stubsOutputDir + + /** + * Suffix for the generated Stubs Jar task + */ + String stubsSuffix = 'stubs' + + /** + * Incubating feature. You can check the size of JSON arrays. If not turned on + * explicitly will be disabled. + */ + Boolean assertJsonSize + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestFramework.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestFramework.groovy new file mode 100644 index 0000000000..8b5f14e707 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestFramework.groovy @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.config + +/** + * Contains main building blocks for a test class for the given framework + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +enum TestFramework { + JUNIT("public ", "public void ", ";", ".java", "Test", "org.junit.Ignore", ["org.junit.FixMethodOrder", "org.junit.runners.MethodSorters"], "@FixMethodOrder(MethodSorters.NAME_ASCENDING)"), + SPOCK("", "def ", "", ".groovy", "Spec", "spock.lang.Ignore", ["spock.lang.Stepwise"], "@Stepwise") + + private final String classModifier + private final String methodModifier + private final String lineSuffix + private final String classExtension; + private final String classNameSuffix; + private final String ignoreClass + private final List orderAnnotationImports + private final String orderAnnotation + + TestFramework(String classModifier, String methodModifier, String lineSuffix, String classExtension, String classNameSuffix, + String ignoreClass, List orderAnnotationImports, String orderAnnotation) { + this.classModifier = classModifier + this.lineSuffix = lineSuffix + this.methodModifier = methodModifier + this.classExtension = classExtension + this.classNameSuffix = classNameSuffix + this.ignoreClass = ignoreClass + this.orderAnnotationImports = orderAnnotationImports + this.orderAnnotation = orderAnnotation + } + + String getClassModifier() { + return classModifier + } + + String getMethodModifier() { + return methodModifier + } + + String getLineSuffix() { + return lineSuffix + } + + String getClassExtension() { + return classExtension + } + + String getClassNameSuffix() { + return classNameSuffix + } + + String getIgnoreClass() { + return ignoreClass + } + + List getOrderAnnotationImport() { + return orderAnnotationImports + } + + String getOrderAnnotation() { + return orderAnnotation + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.groovy new file mode 100644 index 0000000000..a6df25c9e3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.config + +/** + * Provides different testing modes + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +enum TestMode { + /** + * Uses Spring's MockMvc + */ + MOCKMVC, + + /** + * Uses direct HTTP invocations + */ + EXPLICIT, + + /** + * Uses JAX-RS client + */ + JAXRSCLIENT +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy new file mode 100755 index 0000000000..1226c39bf1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -0,0 +1,126 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import groovy.json.JsonBuilder +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.internal.Headers +import org.springframework.cloud.contract.verifier.util.MapConverter +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.ContentUtils + +import static ContentUtils.extractValue +import static MapConverter.transformValues + +/** + * Common abstraction over WireMock Request / Response conversion implementations + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +abstract class BaseWireMockStubStrategy { + + /** + * Returns the stub side values from the object + */ + protected getStubSideValue(Object object) { + return MapConverter.getStubSideValues(object) + } + + private static Closure transform = { + it instanceof DslProperty ? transformValues(it.clientValue, transform) : it + } + + /** + * For the given {@link org.springframework.cloud.contract.verifier.util.ContentType} returns the String version of the body + */ + String parseBody(Object value, ContentType contentType) { + return parseBody(value.toString(), contentType) + } + + /** + * For the given {@link ContentType} returns the Boolean version of the body + */ + Boolean parseBody(Boolean value, ContentType contentType) { + return value + } + + /** + * For the given {@link ContentType} returns the String version of the body + */ + String parseBody(Map map, ContentType contentType) { + def transformedMap = MapConverter.getStubSideValues(map) + return parseBody(toJson(transformedMap), contentType) + } + + /** + * For the given {@link ContentType} returns the String version of the body + */ + String parseBody(List list, ContentType contentType) { + List result = [] + list.each { + if (it instanceof Map) { + result += MapConverter.getStubSideValues(it) + } else { + result += parseBody(it, contentType) + } + } + return parseBody(toJson(result), contentType) + } + + /** + * For the given {@link ContentType} returns the String version of the body + */ + String parseBody(GString value, ContentType contentType) { + Object processedValue = extractValue(value, contentType, { DslProperty dslProperty -> dslProperty.clientValue }) + if (processedValue instanceof GString) { + return parseBody(processedValue.toString(), contentType) + } + return parseBody(processedValue, contentType) + } + + /** + * For the given {@link ContentType} returns the String version of the body + */ + String parseBody(String value, ContentType contentType) { + return value + } + + private static toJson(Object value) { + return new JsonBuilder(value).toString() + } + + /** + * Attempts to guess the {@link ContentType} from body and headers. Returns + * {@link ContentType#UNKNOWN} if it fails to guess. + */ + protected ContentType tryToGetContentType(Object body, Headers headers) { + ContentType contentType = ContentUtils.recognizeContentTypeFromHeader(headers) + if (contentType == ContentType.UNKNOWN) { + if (!body) { + return ContentType.UNKNOWN + } + return ContentUtils.getClientContentType(body) + } + return contentType + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy new file mode 100755 index 0000000000..68606b280c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -0,0 +1,292 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import com.github.tomakehurst.wiremock.http.RequestMethod +import com.github.tomakehurst.wiremock.matching.RequestPattern +import com.github.tomakehurst.wiremock.matching.ValuePattern +import groovy.json.JsonOutput +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import groovy.transform.TypeCheckingMode +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Body +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.verifier.util.ContentUtils +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.MapConverter +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.OptionalProperty +import org.springframework.cloud.contract.spec.internal.RegexPatterns +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.verifier.util.ContentType + +import java.util.regex.Pattern + +import static ContentUtils.getEqualsTypeFromContentType +import static ContentUtils.recognizeContentTypeFromContent +import static ContentUtils.recognizeContentTypeFromHeader +import static ContentUtils.recognizeContentTypeFromMatchingStrategy +import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildGStringRegexpForStubSide +import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildJSONRegexpMatch + +/** + * Converts a {@link Request} into {@link RequestPattern} + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { + + private final Request request + + WireMockRequestStubStrategy(Contract groovyDsl) { + this.request = groovyDsl.request + } + + @PackageScope + RequestPattern buildClientRequestContent() { + if(!request) { + return null + } + RequestPattern requestPattern = new RequestPattern() + appendMethod(requestPattern) + appendHeaders(requestPattern) + appendUrl(requestPattern) + appendQueryParameters(requestPattern) + appendBody(requestPattern) + appendMultipart(requestPattern) + return requestPattern + } + + private void appendMethod(RequestPattern requestPattern) { + if(!request.method) { + return + } + requestPattern.setMethod(RequestMethod.fromString(request.method.clientValue?.toString())) + } + + private void appendBody(RequestPattern requestPattern) { + if (!request.body) { + return + } + ContentType contentType = tryToGetContentType(request.body.clientValue, request.headers) + if (contentType == ContentType.JSON) { + JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValues(getMatchingStrategyFromBody(request.body)?.clientValue) + if (values.empty) { + requestPattern.bodyPatterns = [new ValuePattern(jsonCompareMode: org.skyscreamer.jsonassert.JSONCompareMode.LENIENT, + equalToJson: JsonOutput.toJson(getMatchingStrategy(request.body.clientValue).clientValue) ) ] + } else { + requestPattern.bodyPatterns = values.findAll{ !it.assertsSize() }.collect { new ValuePattern(matchesJsonPath: it.jsonPath().replace("\\\\", "\\")) } ?: null as List + } + } else if (contentType == ContentType.XML) { + requestPattern.bodyPatterns = [new ValuePattern(equalToXml: getMatchingStrategy(request.body.clientValue).clientValue.toString())] + } else if (containsPattern(request?.body)) { + MatchingStrategy matchingStrategy = appendBodyRegexpMatchPattern(request.body) + requestPattern.bodyPatterns = [convertToValuePattern(matchingStrategy)] + } else { + requestPattern.bodyPatterns = [convertToValuePattern(getMatchingStrategy(request.body.clientValue))] + } + } + + private void appendMultipart(RequestPattern requestPattern) { + if (!request.multipart) { + return + } + + if (request.multipart.clientValue instanceof Map) { + List multipartPatterns = (request.multipart.clientValue as Map).collect { + (it.value instanceof NamedProperty + ? ValuePattern.matches(RegexPatterns.multipartFile(it.key, (it.value as NamedProperty).name.clientValue, (it.value as NamedProperty).value.clientValue)) + : ValuePattern.matches(RegexPatterns.multipartParam(it.key, it.value)) ) + } + + requestPattern.bodyPatterns ? requestPattern.bodyPatterns.addAll(multipartPatterns) : (requestPattern.bodyPatterns = multipartPatterns) + } + } + + private void appendHeaders(RequestPattern requestPattern) { + if(!request.headers) { + return + } + request.headers.entries.each { + requestPattern.addHeader(it.name, convertToValuePattern(it.clientValue)) + } + } + + private void appendUrl(RequestPattern requestPattern) { + Object urlPath = request?.urlPath?.clientValue + if (urlPath) { + if(urlPath instanceof Pattern) { + requestPattern.setUrlPathPattern(getStubSideValue(urlPath.toString()).toString()) + } else { + requestPattern.setUrlPath(getStubSideValue(urlPath.toString()).toString()) + } + } + if(!request.url) { + return + } + Object url = getUrlIfGstring(request?.url?.clientValue) + if (url instanceof Pattern) { + requestPattern.setUrlPattern(url.pattern()) + } else { + requestPattern.setUrl(url.toString()) + } + } + + private Object getUrlIfGstring(Object clientSide) { + if (clientSide instanceof GString) { + if (clientSide.values.any { getStubSideValue(it) instanceof Pattern }) { + return Pattern.compile(getStubSideValue(clientSide).toString()) + } else { + return getStubSideValue(clientSide).toString() + } + } + return clientSide + } + + private void appendQueryParameters(RequestPattern requestPattern) { + QueryParameters queryParameters = request?.urlPath?.queryParameters ?: request?.url?.queryParameters + queryParameters?.parameters?.each { + requestPattern.addQueryParam(it.name, convertToValuePattern(it.clientValue)) + } + } + + @TypeChecked(TypeCheckingMode.SKIP) + private static ValuePattern convertToValuePattern(Object object) { + switch (object) { + case Pattern: + Pattern value = object as Pattern + return ValuePattern.matches(value.pattern()) + case OptionalProperty: + OptionalProperty value = object as OptionalProperty + return ValuePattern.matches(value.optionalPattern()) + case MatchingStrategy: + MatchingStrategy value = object as MatchingStrategy + switch (value.type) { + case MatchingStrategy.Type.NOT_MATCHING: + return new ValuePattern(doesNotMatch: value.clientValue) + case MatchingStrategy.Type.ABSENT: + return ValuePattern.absent() + default: + return ValuePattern."${value.type.name}"(value.clientValue) + } + default: + return ValuePattern.equalTo(object.toString()) + } + } + + private MatchingStrategy getMatchingStrategyFromBody(Body body) { + if(!body) { + return null + } + return getMatchingStrategy(body.clientValue) + } + + private MatchingStrategy getMatchingStrategy(MatchingStrategy matchingStrategy) { + return getMatchingStrategyIncludingContentType(matchingStrategy) + } + private MatchingStrategy getMatchingStrategy(GString gString) { + if (!gString) { + return new MatchingStrategy("", MatchingStrategy.Type.EQUAL_TO) + } + def extractedValue = ContentUtils.extractValue(gString) { + it instanceof DslProperty ? it.clientValue : getStringFromGString(it) + } + def value = getStringFromGString(extractedValue) + return getMatchingStrategy(value) + } + + private def getStringFromGString(Object object) { + return object instanceof GString ? object.toString() : object + } + + private MatchingStrategy getMatchingStrategy(Object bodyValue) { + return tryToFindMachingStrategy(bodyValue) + } + + private MatchingStrategy tryToFindMachingStrategy(Object bodyValue) { + return new MatchingStrategy(MapConverter.transformToClientValues(bodyValue), getEqualsTypeFromContentTypeHeader()) + } + + private MatchingStrategy getMatchingStrategyIncludingContentType(MatchingStrategy matchingStrategy) { + MatchingStrategy.Type type = matchingStrategy.type + Object value = matchingStrategy.clientValue + ContentType contentType = recognizeContentTypeFromMatchingStrategy(type) + if (contentType == ContentType.UNKNOWN && type == MatchingStrategy.Type.EQUAL_TO) { + contentType = recognizeContentTypeFromContent(value) + type = getEqualsTypeFromContentType(contentType) + } + return new MatchingStrategy(parseBody(value, contentType), type) + } + + private MatchingStrategy appendBodyRegexpMatchPattern(Object value, ContentType contentType) { + switch (contentType) { + case ContentType.JSON: + return new MatchingStrategy(buildJSONRegexpMatch(value), MatchingStrategy.Type.MATCHING) + case ContentType.UNKNOWN: + return new MatchingStrategy(buildGStringRegexpForStubSide(value), MatchingStrategy.Type.MATCHING) + case ContentType.XML: + throw new IllegalStateException("XML pattern matching is not implemented yet") + } + } + + private MatchingStrategy appendBodyRegexpMatchPattern(Object value) { + return appendBodyRegexpMatchPattern(value, ContentType.UNKNOWN) + } + + private boolean containsPattern(GString bodyAsValue) { + return containsPattern(bodyAsValue.values) + } + + private boolean containsPattern(Map map) { + return containsPattern(map.entrySet()) + } + + private boolean containsPattern(Collection collection) { + return collection.collect(this.&containsPattern).inject('') { a, b -> a || b } + } + + private boolean containsPattern(Object[] objects) { + return containsPattern(objects.toList()) + } + + private boolean containsPattern(Map.Entry entry) { + return containsPattern(entry.value) + } + + private boolean containsPattern(DslProperty dslProperty) { + return containsPattern(dslProperty.clientValue) + } + + private boolean containsPattern(Pattern pattern) { + return true + } + + private boolean containsPattern(Object o) { + return false + } + + private MatchingStrategy.Type getEqualsTypeFromContentTypeHeader() { + return getEqualsTypeFromContentType(recognizeContentTypeFromHeader(request.headers)) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy new file mode 100755 index 0000000000..e1c6ecf4d2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder +import com.github.tomakehurst.wiremock.http.HttpHeader +import com.github.tomakehurst.wiremock.http.HttpHeaders +import com.github.tomakehurst.wiremock.http.ResponseDefinition +import groovy.transform.PackageScope +import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.spec.internal.Response +import org.springframework.cloud.contract.verifier.util.ContentType + +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent +import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader + +/** + * Converts a {@link Request} into {@link ResponseDefinition} + * + * @since 1.0.0 + */ +@TypeChecked +@PackageScope +class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { + + private final Response response + + WireMockResponseStubStrategy(Contract groovyDsl) { + this.response = groovyDsl.response + } + + @PackageScope + ResponseDefinition buildClientResponseContent() { + if(!response) { + return null + } + ResponseDefinitionBuilder builder = new ResponseDefinitionBuilder() + .withStatus(response.status.clientValue as Integer) + appendHeaders(builder) + appendBody(builder) + appendResponseDelayTime(builder) + return builder.build() + } + + private void appendHeaders(ResponseDefinitionBuilder builder) { + if (response.headers) { + builder.withHeaders(new HttpHeaders(response.headers.entries?.collect { + new HttpHeader(it.name, it.clientValue.toString()) + })) + } + } + + private void appendBody(ResponseDefinitionBuilder builder) { + if (response.body) { + Object body = response.body.clientValue + ContentType contentType = recognizeContentTypeFromHeader(response.headers) + if (contentType == ContentType.UNKNOWN) { + contentType = recognizeContentTypeFromContent(body) + } + builder.withBody(parseBody(body, contentType)) + } + } + + private void appendResponseDelayTime(ResponseDefinitionBuilder builder) { + // TODO: Add a missing test for this + if (response.delay) { + builder.withFixedDelay(response.delay.clientValue as Integer) + } + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy new file mode 100644 index 0000000000..41c1ed1615 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import com.github.tomakehurst.wiremock.http.ResponseDefinition +import com.github.tomakehurst.wiremock.matching.RequestPattern +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Converts a {@link ContractMetadata} into a WireMock stub + * + * @since 1.0.0 + */ +@CompileStatic +class WireMockStubStrategy { + + private static final String STEP_START = "Started" + private static final String STEP_PREFIX = "Step" + private final WireMockRequestStubStrategy wireMockRequestStubStrategy + private final WireMockResponseStubStrategy wireMockResponseStubStrategy + private final Integer priority + private final ContractMetadata contract + private final String rootName + + WireMockStubStrategy(String rootName, ContractMetadata contract, org.springframework.cloud.contract.spec.Contract groovyDsl) { + this.rootName = rootName + this.contract = contract + this.wireMockRequestStubStrategy = new WireMockRequestStubStrategy(groovyDsl) + this.wireMockResponseStubStrategy = new WireMockResponseStubStrategy(groovyDsl) + this.priority = groovyDsl.priority + } + + /** + * Converts {@link ContractMetadata} to String version of {@link StubMapping} + */ + @CompileDynamic + String toWireMockClientStub() { + StubMapping stubMapping = new StubMapping() + + RequestPattern request = wireMockRequestStubStrategy.buildClientRequestContent() + ResponseDefinition response = wireMockResponseStubStrategy.buildClientResponseContent() + if (priority) { + stubMapping.priority = priority + } + + stubMapping.request = request + stubMapping.response = response + + if (!request || !response) { + return '' + } + + if (contract.order != null) { + stubMapping.scenarioName = "Scenario_" + rootName + stubMapping.requiredScenarioState = contract.order == 0 ? STEP_START : STEP_PREFIX + contract.order + if (contract.order < contract.groupSize - 1) { + stubMapping.newScenarioState = STEP_PREFIX + (contract.order + 1) + } + } + + return StubMapping.buildJsonStringFor(stubMapping) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy new file mode 100755 index 0000000000..7b7ad47618 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.file + +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ListMultimap +import groovy.transform.CompileStatic +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.SystemUtils + +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.PathMatcher +import java.util.regex.Pattern + +/** + * Scans the provided file path for the DSLs. There's a possibility to provide + * inclusion and exclusion filters. + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@CompileStatic +class ContractFileScanner { + + private static final String MATCH_PREFIX = "glob:" + private static final Pattern SCENARIO_STEP_FILENAME_PATTERN = Pattern.compile("[0-9]+_.*") + private final File baseDir + private final Set excludeMatchers + private final Set ignoreMatchers + + ContractFileScanner(File baseDir, Set excluded, Set ignored) { + this.baseDir = baseDir + excludeMatchers = processPatterns(excluded ?: [] as Set) + ignoreMatchers = processPatterns(ignored ?: [] as Set) + } + + private Set processPatterns(Set patterns) { + FileSystem fileSystem = FileSystems.getDefault() + return patterns.collect({ + String syntaxAndPattern = MATCH_PREFIX + '**' + File.separator + it + // FIXME: This looks strange, need to be checked on windows + if (SystemUtils.IS_OS_WINDOWS) { + syntaxAndPattern = syntaxAndPattern.replace("\\", "\\\\") + } + fileSystem.getPathMatcher(syntaxAndPattern) + }) as Set + } + + /** + * Returns for a map of paths for which a list of matching contracts has been found + */ + ListMultimap findContracts() { + ListMultimap result = ArrayListMultimap.create() + appendRecursively(baseDir, result) + return result + } + + private void appendRecursively(File baseDir, ListMultimap result) { + File[] files = baseDir.listFiles() + if (!files) { + return; + } + files.sort().eachWithIndex { File file, int index -> + if (!matchesPattern(file, excludeMatchers)) { + if (isContractFile(file)) { + Path path = file.toPath() + Integer order = null + if (hasScenarioFilenamePattern(path)) { + order = index + } + result.put(file.parentFile.toPath(), new ContractMetadata(path, matchesPattern(file, ignoreMatchers), files.size(), order)) + } else { + appendRecursively(file, result) + } + } + } + } + + private boolean hasScenarioFilenamePattern(Path path) { + return SCENARIO_STEP_FILENAME_PATTERN.matcher(path.fileName.toString()).matches() + } + + private boolean matchesPattern(File file, Set excludeMatchers) { + for (PathMatcher matcher : excludeMatchers) { + if (matcher.matches(file.toPath())) { + return true; + } + } + return false; + } + + private boolean isContractFile(File file) { + return file.isFile() && FilenameUtils.getExtension(file.toString()).equalsIgnoreCase("groovy") + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy new file mode 100644 index 0000000000..f201949740 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.file + +import groovy.transform.CompileStatic + +import java.nio.file.Path + +/** + * Contains metadata for a particular file with a DSL + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +@CompileStatic +class ContractMetadata { + final Path path; + final boolean ignored; + final int groupSize + final Integer order; + + ContractMetadata(Path path, boolean ignored, int groupSize, Integer order) { + this.groupSize = groupSize + this.path = path + this.ignored = ignored + this.order = order + } + + @Override + public String toString() { + return "ContractMetadata{" + + "fileName=" + path.fileName + + ", ignored=" + ignored + + ", groupSize=" + groupSize + + ", order=" + order + + '}'; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/BodyExtractor.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/BodyExtractor.groovy new file mode 100644 index 0000000000..9e69876737 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/BodyExtractor.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import groovy.json.JsonOutput +import groovy.json.StringEscapeUtils +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.internal.DslProperty + +import static ContentUtils.extractValue +/** + * Class that constructs a String from a body. The body can be a GString + * or a map. + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +@CompileStatic +class BodyExtractor { + + private BodyExtractor() {} + + /** + * Returns the string representation of the body for the server side. + * That means that all the interpolations etc. will be resolved for the + * server side. + */ + static String extractTestValueFrom(Object body) { + Object bodyValue = extractServerValueFromBody(body) + String json = new JsonOutput().toJson(bodyValue) + json = StringEscapeUtils.unescapeJavaScript(json) + return trimRepeatedQuotes(json) + } + + /** + * Returns the string representation of the body for the client side. + * That means that all the interpolations etc. will be resolved for the + * client side. + */ + static String extractStubValueFrom(Object body) { + Object bodyValue = extractClientValueFromBody(body); + String json = new JsonOutput().toJson(bodyValue) + json = StringEscapeUtils.unescapeJavaScript(json) + return trimRepeatedQuotes(json) + } + + private static String trimRepeatedQuotes(String toTrim) { + return toTrim.startsWith('"') ? toTrim.replaceAll('"', '') : toTrim + } + + static Object extractServerValueFromBody(bodyValue) { + if (bodyValue instanceof GString) { + bodyValue = extractValue(bodyValue, { DslProperty dslProperty -> dslProperty.serverValue }) + } else { + bodyValue = MapConverter.transformValues(bodyValue, { it instanceof DslProperty ? it.serverValue : it }) + } + return bodyValue + } + + static Object extractClientValueFromBody(bodyValue) { + if (bodyValue instanceof GString) { + return extractValue(bodyValue, { DslProperty dslProperty -> dslProperty.clientValue }) + } else if (bodyValue instanceof DslProperty) { + return extractClientValueFromBody(bodyValue.clientValue) + } else { + return MapConverter.transformValues(bodyValue, { it instanceof DslProperty ? it.clientValue : it }) + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentType.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentType.groovy new file mode 100644 index 0000000000..48887c3744 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentType.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +/** + * Represents content type + * + * @since 1.0.0 + */ +enum ContentType { + + JSON("application/json"), + XML("application/xml"), + UNKNOWN("application/octet-stream") + + final String mimeType + + ContentType(String mimeType) { + this.mimeType = mimeType + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy new file mode 100644 index 0000000000..366c03f5d8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -0,0 +1,373 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util +import groovy.json.JsonException +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import groovy.transform.TypeChecked +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.internal.Headers +import org.codehaus.groovy.runtime.GStringImpl +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.OptionalProperty + +import java.util.regex.Matcher +import java.util.regex.Pattern + +import static org.apache.commons.lang3.StringEscapeUtils.escapeJava +import static org.apache.commons.lang3.StringEscapeUtils.escapeJson +import static org.apache.commons.lang3.StringEscapeUtils.escapeXml11 + +@TypeChecked +@Slf4j +class ContentUtils { + + public static final Closure GET_STUB_SIDE = { + it instanceof DslProperty ? it.clientValue : it + } + + public static final Closure GET_TEST_SIDE = { + it instanceof DslProperty ? it.serverValue : it + } + + private static final Pattern TEMPORARY_PATTERN_HOLDER = Pattern.compile('.*REGEXP>>(.*)<<.*') + private static final Pattern TEMPORARY_EXECUTION_PATTERN_HOLDER = Pattern.compile('["]?EXECUTION>>(.*)<<["]?') + private static final Pattern TEMPORARY_OPTIONAL_PATTERN_HOLDER = Pattern.compile('OPTIONAL>>(.*)<<') + private static final String JSON_VALUE_PATTERN_FOR_REGEX = 'REGEXP>>%s<<' + private static final String JSON_VALUE_PATTERN_FOR_OPTIONAL = 'OPTIONAL>>%s<<' + private static final String JSON_VALUE_PATTERN_FOR_EXECUTION = '"EXECUTION>>%s<<"' + + /** + * Due to the fact that we allow users to have a body with GString and different values inside + * we need to be prepared that they pass regexps around both on client and server side. + * + * In order to preserve the original JSON structure we need to convert the passed Regex patterns + * to a temporary string, then convert all to a legitimate JSON structure and then finally + * convert it back from string to a pattern. + * + * @param bodyAsValue - GString with passed values + * @param valueProvider - provider of values either for server or client side + * @return JSON structure with replaced client / server side parts + */ + public static Object extractValue(GString bodyAsValue, ContentType contentType, Closure valueProvider) { + if (bodyAsValue.isEmpty()){ + return bodyAsValue + } + if (contentType == ContentType.JSON) { + return extractValueForJSON(bodyAsValue, valueProvider) + } + if (contentType == ContentType.XML) { + return extractValueForXML(bodyAsValue, valueProvider) + } + // else Brute force :( + try { + log.debug("No content type provided so trying to parse as JSON") + return extractValueForJSON(bodyAsValue, valueProvider) + } catch(JsonException e) { + // Not a JSON format + log.debug("Failed to parse as JSON - trying to parse as XML", e) + try { + return extractValueForXML(bodyAsValue, valueProvider) + } catch (Exception exception) { + log.debug("No content type provided and failed to parse as XML - returning the value back to the user", exception) + return extractValueForGString(bodyAsValue, valueProvider) + } + } + } + + public static ContentType getClientContentType(GString bodyAsValue) { + try { + extractValueForJSON(bodyAsValue, GET_STUB_SIDE) + return ContentType.JSON + } catch(JsonException e) { + try { + new XmlSlurper().parseText(extractValueForXML(bodyAsValue, GET_STUB_SIDE).toString()) + return ContentType.XML + } catch (Exception exception) { + extractValueForGString(bodyAsValue, GET_STUB_SIDE) + return ContentType.UNKNOWN + } + } + } + + public static ContentType getClientContentType(String bodyAsValue) { + try { + new JsonSlurper().parseText(bodyAsValue) + return ContentType.JSON + } catch(JsonException e) { + try { + new XmlSlurper().parseText(bodyAsValue) + return ContentType.XML + } catch (Exception exception) { + return ContentType.UNKNOWN + } + } + } + + public static ContentType getClientContentType(Object bodyAsValue) { + return ContentType.UNKNOWN + } + + public static ContentType getClientContentType(Map bodyAsValue) { + try { + JsonOutput.toJson(bodyAsValue) + return ContentType.JSON + } catch (Exception ignore) { + return ContentType.UNKNOWN + } + } + + public static ContentType getClientContentType(List bodyAsValue) { + try { + JsonOutput.toJson(bodyAsValue) + return ContentType.JSON + } catch (Exception ignore) { + return ContentType.UNKNOWN + } + } + + private static GStringImpl extractValueForGString(GString bodyAsValue, Closure valueProvider) { + return new GStringImpl( + bodyAsValue.values.collect { it instanceof DslProperty ? valueProvider(it) : it } as String[], + bodyAsValue.strings.clone() as String[] + ) + } + + public static Object extractValue(GString bodyAsValue, Closure valueProvider) { + return extractValue(bodyAsValue, ContentType.UNKNOWN, valueProvider) + } + + private static Object extractValueForJSON(GString bodyAsValue, Closure valueProvider) { + GString transformedString = new GStringImpl( + bodyAsValue.values.collect { transformJSONStringValue(it, valueProvider) } as String[], + bodyAsValue.strings.clone() as String[] + ) + def parsedJson = new JsonSlurper().parseText(transformedString.toString().replace('\\', '\\\\')) + return convertAllTemporaryRegexPlaceholdersBackToPatterns(parsedJson) + } + + private static GStringImpl extractValueForXML(GString bodyAsValue, Closure valueProvider) { + return new GStringImpl( + bodyAsValue.values.collect { transformXMLStringValue(it, valueProvider) } as String[], + bodyAsValue.strings.clone() as String[] + ) + } + + protected static Object transformJSONStringValue(Object obj, Closure valueProvider) { + return obj + } + + protected static Object transformJSONStringValue(DslProperty dslProperty, Closure valueProvider) { + return transformJSONStringValue(valueProvider(dslProperty), valueProvider) + } + + protected static Object transformJSONStringValue(Pattern pattern, Closure valueProvider) { + return String.format(JSON_VALUE_PATTERN_FOR_REGEX, pattern.pattern()) + } + + protected static Object transformJSONStringValue(OptionalProperty optional, Closure valueProvider) { + return String.format(JSON_VALUE_PATTERN_FOR_OPTIONAL, optional.value) + } + + protected static Object transformJSONStringValue(ExecutionProperty property, Closure valueProvider) { + return String.format(JSON_VALUE_PATTERN_FOR_EXECUTION, property.executionCommand) + } + + private static String transformXMLStringValue(Object obj, Closure valueProvider) { + return escapeXml11(obj.toString()) + } + + private static String transformXMLStringValue(DslProperty dslProperty, Closure valueProvider) { + return transformXMLStringValue(valueProvider(dslProperty), valueProvider) + } + + protected static Object convertDslPropsToTemporaryRegexPatterns(parsedJson) { + MapConverter.transformValues(parsedJson, { Object value -> + return transformJSONStringValue(value, GET_TEST_SIDE) + }) + } + + private static Object convertAllTemporaryRegexPlaceholdersBackToPatterns(parsedJson) { + MapConverter.transformValues(parsedJson, { Object value -> + if (value instanceof String) { + String string = (String) value + return returnParsedObject(string) + } + return value + }) + } + + /** + *

+ * If you wonder why there is val[1] without null-check then take a look at this: + *

+ *

+ * Example: + *

+ *

+ * Our string equals: {@code EXECUTION>>assertThatRejectionReasonIsNull($it)<<} + * The matcher matches this group with the pattern {@code EXECUTION>>(.*)<<} + *

+ *

+ * So {@code executionMatcher[0]} returns 2 elements: + *

    + *
  • index0: EXECUTION>>assertThatRejectionReasonIsNull($it)<<
  • + *
  • index1: assertThatRejectionReasonIsNull($it)<<
  • + *
+ *

+ *

+ * Thus one can safely write {@code executionMatcher[0][1]} to retrieve the matched group + *

+ * @param string to match the regexps against + * @return object converted from temporary holders + */ + static Object returnParsedObject(Object object) { + if (!(object instanceof String)) { + return object + } + String string = (String) object + Matcher matcher = TEMPORARY_PATTERN_HOLDER.matcher(string.trim()) + if (matcher.matches()) { + return Pattern.compile(patternFromMatchingGroup(matcher)) + } + Matcher executionMatcher = TEMPORARY_EXECUTION_PATTERN_HOLDER.matcher(string.trim()) + if (executionMatcher.matches()) { + return new ExecutionProperty(patternFromMatchingGroup(executionMatcher)) + } + Matcher optionalMatcher = TEMPORARY_OPTIONAL_PATTERN_HOLDER.matcher(string.trim()) + if (optionalMatcher.matches()) { + String patternToMatch = patternFromMatchingGroup(optionalMatcher) + return Pattern.compile(new OptionalProperty(patternToMatch).optionalPattern()) + } + return string + } + + private static String patternFromMatchingGroup(Matcher matcher) { + List val = matcher[0] as List + return val[1] + } + + public static ContentType recognizeContentTypeFromHeader(Headers headers) { + String content = headers?.entries.find { it.name == "Content-Type" } ?.clientValue?.toString() + if (content?.endsWith("json")) { + return ContentType.JSON + } + if (content?.endsWith("xml")) { + return ContentType.XML + } + return ContentType.UNKNOWN + } + + public static MatchingStrategy.Type getEqualsTypeFromContentType(ContentType contentType) { + switch (contentType) { + case ContentType.JSON: + return MatchingStrategy.Type.EQUAL_TO_JSON + case ContentType.XML: + return MatchingStrategy.Type.EQUAL_TO_XML + } + return MatchingStrategy.Type.EQUAL_TO + } + + public static ContentType recognizeContentTypeFromContent(GString gstring) { + if (isJsonType(gstring)) { + return ContentType.JSON + } + if (isXmlType(gstring)) { + return ContentType.XML + } + return ContentType.UNKNOWN + } + + public static ContentType recognizeContentTypeFromContent(Map jsonMap) { + return ContentType.JSON + } + + public static ContentType recognizeContentTypeFromContent(List jsonList) { + return ContentType.JSON + } + + public static ContentType recognizeContentTypeFromContent(String string) { + try { + new JsonSlurper().parseText(string) + return ContentType.JSON + } catch (Exception e){ + return ContentType.UNKNOWN + } + } + + public static ContentType recognizeContentTypeFromContent(Object gstring) { + return ContentType.UNKNOWN + } + + public static boolean isJsonType(GString gstring) { + if (gstring.isEmpty()) { + return false + } + GString stringWithoutValues = new GStringImpl( + gstring.values.collect({ + it instanceof String || it instanceof GString ? it.toString() : escapeJson(it.toString()) + }) as Object[], + gstring.strings.clone() as String[] + ) + try { + new JsonSlurper().parseText(stringWithoutValues.toString()) + return true + } catch (JsonException e) { + // Not JSON + } + return false + } + + public static boolean isXmlType(GString gstring) { + GString stringWithoutValues = new GStringImpl( + gstring.values.collect({ + it instanceof String || it instanceof GString ? it.toString() : escapeXml11(it.toString()) + }) as Object[], + gstring.strings.clone() as String[] + ) + try { + new XmlSlurper().parseText(stringWithoutValues.toString()) + return true + } catch (Exception e) { + // Not XML + } + return false + } + + public static ContentType recognizeContentTypeFromMatchingStrategy(MatchingStrategy.Type type) { + switch (type) { + case MatchingStrategy.Type.EQUAL_TO_XML: + return ContentType.XML + case MatchingStrategy.Type.EQUAL_TO_JSON: + return ContentType.JSON + } + return ContentType.UNKNOWN + } + + static String getGroovyMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return "'$propertyName', '$propertyValue.name.serverValue', '$propertyValue.value.serverValue'.bytes" + } + + static String getJavaMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) { + return """"${escapeJava(propertyName)}", "${escapeJava(propertyValue.name.serverValue as String)}", "${escapeJava(propertyValue.value.serverValue as String)}".getBytes()""" + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy new file mode 100644 index 0000000000..9cd8eb5956 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.CompilerConfiguration +import org.springframework.cloud.contract.spec.Contract + +/** + * Converts a file or String into a {@link Contract} + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +@CompileStatic +class ContractVerifierDslConverter { + + static Contract convert(String dsl) { + return groovyShell().evaluate(dsl) as Contract + } + + static Contract convert(File dsl) { + return groovyShell().evaluate(dsl) as Contract + } + + private static GroovyShell groovyShell() { + return new GroovyShell(ContractVerifierDslConverter.classLoader, new Binding(), new CompilerConfiguration(sourceEncoding: 'UTF-8')) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java new file mode 100644 index 0000000000..5dd82a7fb1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java @@ -0,0 +1,327 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util; + +import java.util.LinkedList; +import java.util.regex.Pattern; + +import com.toomuchcoding.jsonassert.JsonVerifiable; + +import static org.apache.commons.lang3.StringEscapeUtils.escapeJava; + +/** + * Implementation of the {@link MethodBufferingJsonVerifiable} that contains a list + * of String method commands that need to be executed to assert JSONs. + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { + + private static final Pattern FIELD_PATTERN = Pattern.compile("\\.field\\((\")?(.)+(\")?\\)"); + private static final Pattern ARRAY_PATTERN = Pattern.compile("\\.array\\((\")?(.)+(\")?\\)"); + + final JsonVerifiable delegate; + final LinkedList methodsBuffer; + + DelegatingJsonVerifiable(JsonVerifiable delegate, + LinkedList methodsBuffer) { + this.delegate = delegate; + this.methodsBuffer = new LinkedList<>(methodsBuffer); + } + + DelegatingJsonVerifiable(JsonVerifiable delegate) { + this.delegate = delegate; + this.methodsBuffer = new LinkedList<>(); + } + + private static String stringWithEscapedQuotes(Object object) { + String stringValue = object.toString(); + return stringValue.replaceAll("\"", "\\\\\""); + } + + private static String wrapValueWithQuotes(Object value) { + return value instanceof String ? + "\"" + stringWithEscapedQuotes(value) + "\"" : + value.toString(); + } + + private void appendMethodWithValue(String methodName, Object value) { + methodsBuffer.offer("." + methodName + "(" + value + ")"); + } + + private void appendMethodWithQuotedValue(String methodName, Object value) { + appendMethodWithValue(methodName, wrapValueWithQuotes(value)); + } + + @Override + public MethodBufferingJsonVerifiable contains(Object value) { + DelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable(delegate.contains(value), methodsBuffer); + verifiable.appendMethodWithQuotedValue("contains", value); + if (isAssertingAValueInArray()) { + verifiable.methodsBuffer.offer(".value()"); + } + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable field(Object value) { + Object valueToPut = value instanceof ShouldTraverse ? ((ShouldTraverse) value).value : value; + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.field(valueToPut), methodsBuffer); + if (delegate.isIteratingOverArray() && !(value instanceof ShouldTraverse)) { + verifiable.appendMethodWithQuotedValue("contains", valueToPut); + } else { + verifiable.appendMethodWithQuotedValue("field", valueToPut); + } + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable field(String... strings) { + MethodBufferingJsonVerifiable verifiable = null; + for (String string : strings) { + verifiable = verifiable == null ? field(string) : verifiable.field(string); + } + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable array(Object value) { + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.array(value), methodsBuffer); + verifiable.appendMethodWithQuotedValue("array", value); + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable arrayField(Object value) { + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.field(value).arrayField(), methodsBuffer); + verifiable.appendMethodWithQuotedValue("array", value); + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable arrayField() { + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.arrayField(), methodsBuffer); + verifiable.methodsBuffer.offer(".arrayField()"); + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable array() { + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.array(), methodsBuffer); + verifiable.methodsBuffer.offer(".array()"); + return verifiable; + } + + @Override + public JsonVerifiable elementWithIndex(int i) { + DelegatingJsonVerifiable verifiable = new DelegatingJsonVerifiable(delegate.elementWithIndex(i), methodsBuffer); + verifiable.methodsBuffer.offer(".elementWithIndex(" + i + ")"); + return verifiable; + } + + @Override + public MethodBufferingJsonVerifiable iterationPassingArray() { + return new DelegatingJsonVerifiable(delegate, methodsBuffer); + } + + @Override + public MethodBufferingJsonVerifiable isEqualTo(String value) { + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(delegate.isEqualTo(value), methodsBuffer); + if (delegate.isAssertingAValueInArray() && readyToCheck.methodsBuffer.peekLast().equals(".arrayField()")) { + readyToCheck.appendMethodWithQuotedValue("isEqualTo", escapeJava(value)); + readyToCheck.methodsBuffer.offer(".value()"); + } else if (delegate.isAssertingAValueInArray() && !readyToCheck.methodsBuffer.peekLast().contains("array")) { + readyToCheck.methodsBuffer.offer(".value()"); + } else { + readyToCheck.appendMethodWithQuotedValue("isEqualTo", escapeJava(value)); + } + return readyToCheck; + } + + @Override + public MethodBufferingJsonVerifiable isEqualTo(Object value) { + if (value == null) { + return isNull(); + } + return isEqualTo(value.toString()); + } + + @Override + public MethodBufferingJsonVerifiable isEqualTo(Number value) { + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(delegate.isEqualTo(value), methodsBuffer); + // related to #271 - the problem is with asserting arrays of maps vs arrays of primitives + String last = readyToCheck.methodsBuffer.peekLast(); + boolean containsAMatcher = containsAnyMatcher(last); + if (delegate.isAssertingAValueInArray() && containsAMatcher) { + readyToCheck.methodsBuffer.offer(".value()"); + } else { + readyToCheck.appendMethodWithValue("isEqualTo", String.valueOf(value)); + } + return readyToCheck; + } + + private boolean containsAnyMatcher(String string) { + return string.contains("isEqualTo") || string.contains("matches") || string.contains("isNull"); + } + + @Override + public MethodBufferingJsonVerifiable isNull() { + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(delegate.isNull(), methodsBuffer); + readyToCheck.methodsBuffer.offer(".isNull()"); + return readyToCheck; + } + + @Override + public MethodBufferingJsonVerifiable matches(String value) { + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(delegate.matches(value), methodsBuffer); + if (delegate.isAssertingAValueInArray()) { + readyToCheck.appendMethodWithQuotedValue("matches", escapeJava(value)); + readyToCheck.methodsBuffer.offer(".value()"); + } else { + readyToCheck.appendMethodWithQuotedValue("matches", escapeJava(value)); + } + return readyToCheck; + } + + @Override + public MethodBufferingJsonVerifiable isEqualTo(Boolean value) { + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(delegate.isEqualTo(value), methodsBuffer); + if (delegate.isAssertingAValueInArray()) { + readyToCheck.methodsBuffer.offer(".value()"); + } else { + readyToCheck.appendMethodWithValue("isEqualTo", String.valueOf(value)); + } + return readyToCheck; + } + + @Override + public MethodBufferingJsonVerifiable value() { + return new FinishedDelegatingJsonVerifiable(delegate, methodsBuffer); + } + + @Override + public boolean assertsSize() { + for (String s : methodsBuffer) { + if (s.contains(".hasSize(")) { + return true; + } + } + return false; + } + + @Override + public boolean assertsConcreteValue() { + for (String s : methodsBuffer) { + if (FIELD_PATTERN.matcher(s).matches()|| ARRAY_PATTERN.matcher(s).matches()) { + return true; + } + } + return false; + } + + @Override + public JsonVerifiable withoutThrowingException() { + return delegate.withoutThrowingException(); + } + + @Override + public String jsonPath() { + return delegate.jsonPath(); + } + + @Override + public void matchesJsonPath(String s) { + delegate.matchesJsonPath(s); + } + + @Override + public JsonVerifiable hasSize(int size) { + FinishedDelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable(delegate.hasSize(size), methodsBuffer); + verifiable.methodsBuffer.offer(".hasSize(" + size + ")"); + return verifiable; + } + + @Override + public boolean isIteratingOverNamelessArray() { + return delegate.isIteratingOverNamelessArray(); + } + + @Override + public boolean isIteratingOverArray() { + return delegate.isIteratingOverArray(); + } + + @Override + public boolean isAssertingAValueInArray() { + return delegate.isAssertingAValueInArray(); + } + + @Override + public String method() { + return createMethodString(); + } + + private String createMethodString() { + LinkedList queue = new LinkedList<>(methodsBuffer); + StringBuilder stringBuffer = new StringBuilder(); + while (!queue.isEmpty()) { + stringBuffer.append(queue.remove()); + } + return stringBuffer.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + DelegatingJsonVerifiable that = (DelegatingJsonVerifiable) o; + + if (delegate != null ? !delegate.equals(that.delegate) : that.delegate != null) + return false; + if (delegate == null) { + return false; + } + if (delegate.jsonPath() == null && that.delegate.jsonPath() == null) + return true; + return delegate.jsonPath().equals(that.delegate.jsonPath()); + + } + + @Override + public int hashCode() { + int result = delegate != null ? delegate.jsonPath().hashCode() : 0; + return 31 * result; + } + + @Override + public String toString() { + return "DelegatingJsonVerifiable{" + + "delegate=\n" + delegate + + ", methodsBuffer=" + methodsBuffer + + '}'; + } + + @Override + public T read(Class aClass) { + return delegate.read(aClass); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java new file mode 100644 index 0000000000..000d6e75a4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util; + +import java.util.LinkedList; + +import com.toomuchcoding.jsonassert.JsonVerifiable; + +/** + * Helper class that represents a finished assertion of a JSON. + * Contains a list of all necessary method calls to assert the JSON. + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class FinishedDelegatingJsonVerifiable extends DelegatingJsonVerifiable { + + FinishedDelegatingJsonVerifiable(JsonVerifiable delegate, + LinkedList methodsBuffer) { + super(delegate, methodsBuffer); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonPaths.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonPaths.groovy new file mode 100644 index 0000000000..f737a53602 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonPaths.groovy @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import groovy.transform.CompileStatic + +/** + * Represents a set of Strings - set of method calls to assert a JSON + * + * @since 1.0.0 + */ +@CompileStatic +class JsonPaths extends HashSet { +} + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy new file mode 100644 index 0000000000..7825035622 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -0,0 +1,265 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import com.toomuchcoding.jsonassert.JsonAssertion +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.spec.internal.OptionalProperty +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +import java.util.regex.Pattern + +/** + * I would like to apologize to anyone who is reading this class. Since JSON is a hectic structure + * this class is also hectic. The idea is to traverse the JSON structure and build a set of + * JSON Paths together with methods needed to be called to build them. + * + * @author Marcin Grzejszczak + */ +@Slf4j +class JsonToJsonPathsConverter { + + /** + * In case of issues with size assertion just provide this property as system property + * equal to "false" and then size assertion will be disabled + */ + private static final String SIZE_ASSERTION_SYSTEM_PROP = "spring.cloud.contract.verifier.assert.size" + + private static final Boolean SERVER_SIDE = false + private static final Boolean CLIENT_SIDE = true + + private final ContractVerifierConfigProperties configProperties + + JsonToJsonPathsConverter(ContractVerifierConfigProperties configProperties) { + this.configProperties = configProperties + } + + JsonToJsonPathsConverter() { + this.configProperties = new ContractVerifierConfigProperties() + } + + public static JsonPaths transformToJsonPathWithTestsSideValues(def json) { + return new JsonToJsonPathsConverter().transformToJsonPathWithValues(json, SERVER_SIDE) + } + + public static JsonPaths transformToJsonPathWithStubsSideValues(def json) { + return new JsonToJsonPathsConverter().transformToJsonPathWithValues(json, CLIENT_SIDE) + } + + private JsonPaths transformToJsonPathWithValues(def json, boolean clientSide) { + if(!json) { + return new JsonPaths() + } + JsonPaths pathsAndValues = [] as Set + Object convertedJson = MapConverter.getClientOrServerSideValues(json, clientSide) + Object jsonWithPatterns = ContentUtils.convertDslPropsToTemporaryRegexPatterns(convertedJson) + MethodBufferingJsonVerifiable methodBufferingJsonPathVerifiable = + new DelegatingJsonVerifiable(JsonAssertion.assertThat(JsonOutput.toJson(jsonWithPatterns)).withoutThrowingException()) + traverseRecursivelyForKey(jsonWithPatterns, methodBufferingJsonPathVerifiable) + { MethodBufferingJsonVerifiable key, Object value -> + if (value instanceof ExecutionProperty || !(key instanceof FinishedDelegatingJsonVerifiable)) { + return + } + pathsAndValues.add(key) + } + return pathsAndValues + } + + protected def traverseRecursively(Class parentType, MethodBufferingJsonVerifiable key, def value, Closure closure) { + value = ContentUtils.returnParsedObject(value) + if (value instanceof String && value) { + try { + def json = new JsonSlurper().parseText(value) + if (json instanceof Map) { + return convertWithKey(parentType, key, json, closure) + } + } catch (Exception ignore) { + return runClosure(closure, key, value) + } + } else if (isAnEntryWithNonCollectionLikeValue(value)) { + return convertWithKey(List, key, value as Map, closure) + } else if (isAnEntryWithoutNestedStructures(value)) { + return convertWithKey(List, key, value as Map, closure) + } else if (value instanceof Map) { + return convertWithKey(Map, key, value as Map, closure) + // JSON with a list of primitives ["a", "b", "c"] in root issue #266 + } else if (key.isIteratingOverNamelessArray() && value instanceof List && listContainsOnlyPrimitives(value)) { + addSizeVerificationForListWithPrimitives(key, closure, value) + value.each { + traverseRecursively(Object, key.arrayField().contains(ContentUtils.returnParsedObject(it)), + ContentUtils.returnParsedObject(it), closure) + } + // JSON containing list of primitives { "partners":[ { "role":"AGENT", "payment_methods":[ "BANK", "CASH" ] } ] + } else if (value instanceof List && listContainsOnlyPrimitives(value)) { + addSizeVerificationForListWithPrimitives(key, closure, value) + value.each { + traverseRecursively(Object, valueToAsserter(key.arrayField(), ContentUtils.returnParsedObject(it)), + ContentUtils.returnParsedObject(it), closure) + } + } else if (value instanceof List) { + MethodBufferingJsonVerifiable jsonPathVerifiable = createAsserterFromList(key, value) + addSizeVerificationForListWithPrimitives(key, closure, value) + value.each { def element -> + traverseRecursively(List, createAsserterFromListElement(jsonPathVerifiable, ContentUtils.returnParsedObject(element)), + ContentUtils.returnParsedObject(element), closure) + } + return value + } else if (key.isIteratingOverArray()) { + traverseRecursively(Object, key.arrayField().contains(ContentUtils.returnParsedObject(value)), + ContentUtils.returnParsedObject(value), closure) + } + try { + return runClosure(closure, key, value) + } catch (Exception ignore) { + return value + } + } + + // Size verification: https://github.com/Codearte/accurest/issues/279 + private void addSizeVerificationForListWithPrimitives(MethodBufferingJsonVerifiable key, Closure closure, List value) { + Boolean systemPropValue = Boolean.parseBoolean(System.getProperty(SIZE_ASSERTION_SYSTEM_PROP, "false")) + Boolean configPropValue = configProperties.assertJsonSize + boolean configPropSet = configPropValue != null + if (configPropSet && !configPropValue) { + return + } else if (!configPropSet && systemPropValue) { + return + } + log.debug("WARNING: Turning on the incubating feature of JSON array check") + if (isRootElement(key) || key.assertsConcreteValue()) { + if (value.size() > 0) { + closure(key.hasSize(value.size()), value) + } + } + } + + private boolean isRootElement(MethodBufferingJsonVerifiable key) { + return key.jsonPath() == '$' + } + + // If you have a list of not-only primitives it can contain different sets of elements (maps, lists, primitives) + private MethodBufferingJsonVerifiable createAsserterFromList(MethodBufferingJsonVerifiable key, List value) { + if (key.isIteratingOverNamelessArray()) { + return key.array() + } else if (key.isIteratingOverArray() && isAnEntryWithLists(value)) { + if (!value.every { listContainsOnlyPrimitives(it as List)} ) { + return key.array() + } else { + return key.iterationPassingArray() + } + } else if (key.isIteratingOverArray()) { + return key.iterationPassingArray() + } + return key + } + + private MethodBufferingJsonVerifiable createAsserterFromListElement(MethodBufferingJsonVerifiable jsonPathVerifiable, def element) { + if (jsonPathVerifiable.isAssertingAValueInArray()) { + def object = ContentUtils.returnParsedObject(element) + if (object instanceof Pattern) { + return jsonPathVerifiable.matches((object as Pattern).pattern()) + } + return jsonPathVerifiable.contains(object) + } + return jsonPathVerifiable + } + + private def runClosure(Closure closure, MethodBufferingJsonVerifiable key, def value) { + if (key.isAssertingAValueInArray() && !(value instanceof List || value instanceof Map)) { + return closure(valueToAsserter(key, value), value) + } + return closure(key, value) + } + + private boolean isAnEntryWithNonCollectionLikeValue(def value) { + if (!(value instanceof Map)) { + return false + } + Map valueAsMap = ((Map) value) + boolean mapHasOneEntry = valueAsMap.size() == 1 + if (!mapHasOneEntry) { + return false + } + Object valueOfEntry = valueAsMap.entrySet().first().value + return !(valueOfEntry instanceof Map || valueOfEntry instanceof List) + } + + private boolean isAnEntryWithoutNestedStructures(def value) { + if (!(value instanceof Map)) { + return false + } + Map valueAsMap = ((Map) value) + return valueAsMap.entrySet().every { Map.Entry entry -> + [String, Number, Boolean].any { it.isAssignableFrom(entry.value.getClass()) } + } + } + + private boolean listContainsOnlyPrimitives(List list) { + return list.every { def element -> + [String, Number, Boolean].any { + it.isAssignableFrom(element.getClass()) + } + } + } + private boolean isAnEntryWithLists(def value) { + if (!(value instanceof Iterable)) { + return false + } + return value.every { def entry -> + entry instanceof List + } + } + + private Map convertWithKey(Class parentType, MethodBufferingJsonVerifiable parentKey, Map map, Closure closureToExecute) { + return map.collectEntries { + Object entrykey, value -> + def convertedValue = ContentUtils.returnParsedObject(value) + [entrykey, traverseRecursively(parentType, + convertedValue instanceof List ? listContainsOnlyPrimitives(convertedValue) ? + parentKey.arrayField(entrykey) : + parentKey.array(entrykey) : + convertedValue instanceof Map ? parentKey.field(new ShouldTraverse(entrykey)) : + valueToAsserter(parentKey.field(entrykey), convertedValue) + , convertedValue, closureToExecute)] + } + } + + private void traverseRecursivelyForKey(def json, MethodBufferingJsonVerifiable rootKey, Closure closure) { + traverseRecursively(Map, rootKey, json, closure) + } + + protected MethodBufferingJsonVerifiable valueToAsserter(MethodBufferingJsonVerifiable key, Object value) { + def convertedValue = ContentUtils.returnParsedObject(value) + if (key instanceof FinishedDelegatingJsonVerifiable) { + return key + } + if (convertedValue instanceof Pattern) { + return key.matches((convertedValue as Pattern).pattern()) + } else if (convertedValue instanceof OptionalProperty) { + return key.matches((convertedValue as OptionalProperty).optionalPattern()) + } else if (convertedValue instanceof GString) { + return key.matches(RegexpBuilders.buildGStringRegexpForTestSide(convertedValue)) + } else if (convertedValue instanceof ExecutionProperty) { + return key + } + return key.isEqualTo(convertedValue) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy new file mode 100644 index 0000000000..31c5aa1899 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy @@ -0,0 +1,129 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import groovy.json.JsonSlurper +import org.springframework.cloud.contract.spec.internal.DslProperty + +/** + * Converts an object into either client or server side representation. + * Iterates over the structure of an object (depending on whether it's an + * iterable or a primitive type etc.), converts the {@link DslProperty} into their + * client / server representation and returns the result + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class MapConverter { + + public static final boolean STUB_SIDE = true + public static final boolean TEST_SIDE = false + + /** + * Returns the object with client side values of {@link org.springframework.cloud.contract.spec.internal.DslProperty} + */ + static def transformToClientValues(def value) { + return transformValues(value) { + it instanceof DslProperty ? it.clientValue : it + } + } + + /** + * Iterates over the structure of the object and executes the closure + * on each element of that structure. + * + * Returns the transformed structure + */ + static def transformValues(def value, Closure closure) { + if (value instanceof String && value) { + try { + def json = new JsonSlurper().parseText(value) + if (json instanceof Map) { + return convert(json, closure) + } + } catch (Exception ignore) { + } + return extractValue(value, closure); + } else if (value instanceof Map) { + return convert(value as Map, closure) + } else if (value instanceof List) { + return value.collect({ transformValues(it, closure) }) + } + return transformValue(closure, value) + } + + /** + * Transforms a value with the given closure. Needs to be protected, otherwise + * method access exception will occur at runtime. + */ + protected static Object transformValue(Closure closure, Object value) { + return extractValue(value, { Object val-> + Object newValue = closure(val) + if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && value) { + return transformValues(newValue, closure) + } + return newValue; + }) + } + + private static extractValue(Object value, Closure closure) { + try { + return closure(value) + } catch (Exception ignore) { + return value + } + } + + private static Map convert(Map map, Closure closure) { + return map.collectEntries { + key, value -> + [key, transformValues(value, closure)] + } + } + + /** + * If {@code clientSide} is {@code true} returns the client side value for the + * provided object + */ + static Object getClientOrServerSideValues(json, boolean clientSide) { + return transformValues(json) { + if (it instanceof DslProperty) { + DslProperty dslProperty = ((DslProperty) it) + return clientSide ? + getClientOrServerSideValues(dslProperty.clientValue, clientSide) : getClientOrServerSideValues(dslProperty.serverValue, clientSide) + } else if (it instanceof GString) { + return ContentUtils.extractValue(it , null, { + if (it instanceof DslProperty) { + return clientSide ? + getClientOrServerSideValues((it as DslProperty).clientValue, clientSide) : getClientOrServerSideValues((it as DslProperty).serverValue, clientSide) + } + return it + }) + } + return it + } + } + + static Object getStubSideValues(json) { + return getClientOrServerSideValues(json, STUB_SIDE) + } + + static Object getTestSideValues(json) { + return getClientOrServerSideValues(json, TEST_SIDE) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java new file mode 100644 index 0000000000..12774b14bb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util; + +/** + * Contract for classes that buffer method executions + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +public interface MethodBuffering { + + String method(); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java new file mode 100644 index 0000000000..746bf3a9a9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util; + +import com.toomuchcoding.jsonassert.JsonVerifiable; + +/** + * A wrapper over {@link JsonVerifiable} that allows to store the method + * name in order to print it out in the generated test + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +public interface MethodBufferingJsonVerifiable + extends JsonVerifiable, MethodBuffering { + @Override + MethodBufferingJsonVerifiable contains(Object value); + + @Override + MethodBufferingJsonVerifiable field(Object value); + + @Override + MethodBufferingJsonVerifiable array(Object value); + + MethodBufferingJsonVerifiable arrayField(Object value); + + @Override + MethodBufferingJsonVerifiable arrayField(); + + @Override + MethodBufferingJsonVerifiable array(); + + MethodBufferingJsonVerifiable iterationPassingArray(); + + @Override + MethodBufferingJsonVerifiable isEqualTo(String value); + + @Override + MethodBufferingJsonVerifiable isEqualTo(Object value); + + @Override + MethodBufferingJsonVerifiable isEqualTo(Number value); + + @Override + MethodBufferingJsonVerifiable isNull(); + + @Override + MethodBufferingJsonVerifiable matches(String value); + + @Override + MethodBufferingJsonVerifiable isEqualTo(Boolean value); + + @Override + MethodBufferingJsonVerifiable value(); + + boolean assertsSize(); + + boolean assertsConcreteValue(); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/NamesUtil.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/NamesUtil.groovy new file mode 100644 index 0000000000..6957ed3d8d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/NamesUtil.groovy @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +/** + * A utility class that helps to convert names + * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +class NamesUtil { + + /** + * Returns the first element before the last separator presence. + * Returns empty string if separator is not found. + */ + static String beforeLast(String string, String separator) { + if (string?.indexOf(separator) > -1) { + return string.substring(0, string.lastIndexOf(separator)) + } + return '' + } + + /** + * Returns the first element after the last separator presence + * Returns the provided string if separator is not found. + */ + static String afterLast(String string, String separator) { + if (string?.indexOf(separator) > -1) { + return string.substring(string.lastIndexOf(separator) + 1) + } + return string + } + + /** + * Returns the first element after the last dot presence + * Returns the provided string if separator is not found. + */ + static String afterLastDot(String string) { + return afterLast(string, '.') + } + + /** + * Converts a string into a camel case format + */ + static String camelCase(String className) { + if (!className) { + return className + } + String firstChar = className.charAt(0).toLowerCase() as String + return firstChar + className.substring(1) + } + + /** + * Capitalizes the provided string + */ + static String capitalize(String className) { + if (!className) { + return className + } + String firstChar = className.charAt(0).toUpperCase() as String + return firstChar + className.substring(1) + } + + /** + * Returns the whole string to the last present dot. + * Returns input string if there is no dot + */ + static String toLastDot(String string) { + if (string?.indexOf('.') > -1) { + return string.substring(0, string.lastIndexOf('.')) + } + return string + } + + /** + * Converts the Java package notation to a path format + */ + static String packageToDirectory(String packageName) { + return packageName.replace('.' as char, File.separatorChar) + } + + /** + * Converts the path format to a Java package notation + */ + static String directoryToPackage(String directory) { + return directory.replace(File.separator, '.') + } + + /** + * Converts illegal package characters to underscores + */ + static String convertIllegalPackageChars(String packageName) { + return packageName.replace('-', '_') + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy new file mode 100644 index 0000000000..df267cfd87 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import groovy.transform.TypeChecked +import org.codehaus.groovy.runtime.GStringImpl +import org.springframework.cloud.contract.spec.internal.DslProperty + +import java.util.regex.Pattern + +import static ContentUtils.extractValue +import static org.apache.commons.lang3.StringEscapeUtils.escapeJson + +/** + * Useful utility methods to work with regular expresisons + * + * Do not change to {@code @CompileStatic} since it's using double dispatch. + * + * @since 1.0.0 + */ +@TypeChecked +class RegexpBuilders { + + /** + * Converts the {@link GString} passed values into their stub side String representations + */ + static String buildGStringRegexpForStubSide(GString gString) { + new GStringImpl( + gString.values.collect(this.&buildGStringRegexpForStubSide) as Object[], + gString.strings.collect(this.&escapeSpecialRegexChars) as String[] + ) + } + + /** + * Converts the {@link Pattern} passed values into their stub side String representations + */ + static String buildGStringRegexpForStubSide(Pattern pattern) { + return pattern.pattern() + } + + /** + * Converts the {@link org.springframework.cloud.contract.spec.internal.DslProperty} passed values into their stub side String representations + */ + static String buildGStringRegexpForStubSide(DslProperty dslProperty) { + return buildGStringRegexpForStubSide(dslProperty.clientValue) + } + + /** + * Converts the {@link Object} passed values into their stub side String representations + */ + static String buildGStringRegexpForStubSide(Object o) { + return escapeSpecialRegexChars(o.toString()) + } + + /** + * Converts the {@link GString} passed values into their test side String representations + */ + static String buildGStringRegexpForTestSide(GString gString) { + new GStringImpl( + gString.values.collect(this.&buildGStringRegexpForTestSide) as Object[], + gString.strings.collect(this.&escapeSpecialRegexChars) as String[] + ) + } + + /** + * Converts the {@link Pattern} passed values into their test side String representations + */ + static String buildGStringRegexpForTestSide(Pattern pattern) { + return pattern.pattern() + } + + /** + * Converts the {@link DslProperty} passed values into their test side String representations + */ + static String buildGStringRegexpForTestSide(DslProperty dslProperty) { + return buildGStringRegexpForTestSide(dslProperty.clientValue) + } + + /** + * Converts the {@link Object} passed values into their test side String representations + */ + static String buildGStringRegexpForTestSide(Object o) { + return o.toString().replaceAll('\\\\', '\\\\\\\\') + } + + private final static Pattern SPECIAL_REGEX_CHARS = Pattern.compile('[{}()\\[\\].+*?^$\\\\|]') + + private static String escapeSpecialRegexChars(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll('\\\\\\\\$0') + } + + private final static String WS = /\s*/ + + static String buildJSONRegexpMatch(GString gString) { + return buildJSONRegexpMatch(extractValue(gString, ContentType.JSON, { DslProperty dslProperty -> dslProperty.clientValue })) + } + + static String buildJSONRegexpMatch(Map jsonMap) { + return WS + "\\{" + jsonMap.collect(this.&buildJSONRegexpMatch).join(",") + "\\}" + WS + } + + static String buildJSONRegexpMatch(List jsonList) { + return WS + "\\[" + jsonList.collect(this.&buildJSONRegexpMatch).join(",") + "\\]" + WS + } + + /** + * Converts the map into String representation of regular expressions + */ + static String buildJSONRegexpMatch(Map.Entry entry) { + return buildJSONRegexpMatchString(escapeJson(entry.key)) + ":" + buildJSONRegexpMatch(entry.value) + } + + /** + * Converts the object into String representation of regular expressions + */ + static String buildJSONRegexpMatch(Object value) { + return buildJSONRegexpMatchStringOptionalQuotes(escapeJson(value.toString())) + } + + /** + * Converts the pattern into String representation of regular expressions + */ + static String buildJSONRegexpMatch(Pattern pattern) { + return buildJSONRegexpMatchStringOptionalQuotes(pattern.pattern()) + } + + /** + * Converts the String into String representation of regular expressions + */ + static String buildJSONRegexpMatchString(String value) { + return WS + '"' + value + '"' + WS + } + + /** + * Converts the String into an optional String representation of regular expressions + */ + static String buildJSONRegexpMatchStringOptionalQuotes(String value) { + return WS + '"?' + value + '"?' + WS + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ShouldTraverse.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ShouldTraverse.java new file mode 100644 index 0000000000..2fad0fcd6a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/groovy/org/springframework/cloud/contract/verifier/util/ShouldTraverse.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util; + +/** + * Utility class that wraps an object that should be traversed + * when building a list of methods to execute in the generated test. + * + * @author Marcin Grzejszczak + * + * @since 1.0.0 + */ +class ShouldTraverse { + final Object value; + + ShouldTraverse(Object value) { + this.value = value; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/other/sample.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/other/sample.json new file mode 100644 index 0000000000..f4eca9faad --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/other/sample.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "url": "/loanApplication", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.loanapplicationservice.v1+json" + } + }, + "bodyPatterns": [ + { + "matches": "{\"client\":{\"pesel\":\"1234567890\"},\"loanApplicationId\":123123123,\"amount\":123.123}" + } + ] + }, + "response": { + "status": 200, + "body": "{\"loanApplicationStatus\":\"LOAN_APPLIED\",\"loanApplicationId\":\"${123123123:$anyInt($it)}\"}", + "headers": { + "Content-Type": "application/vnd.loanapplicationservice.v1+json" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/sample.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/sample.json new file mode 100644 index 0000000000..92da92f7da --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/main/resources/stubs/wiremocks/sample.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "url": "/loanApplication", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.loanapplicationservice.v1+json" + } + }, + "bodyPatterns": [ + { + "matches": "\\{\"clientPesel\":\"1234567890\",\"loanAmount\":123.123\\}" + } + ] + }, + "response": { + "status": 200, + "body": "{\"loanApplicationStatus\":\"LOAN_APPLIED\",\"loanApplicationId\":\"${123123123:$anyInt($it)}\",\"rejectionReason\":null}", + "headers": { + "Content-Type": "application/vnd.loanapplicationservice.v1+json" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy new file mode 100644 index 0000000000..85f8c84fd4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier + +import org.springframework.cloud.contract.verifier.builder.SingleTestGenerator +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.config.TestFramework +import spock.lang.Specification + +class GeneratorScannerSpec extends Specification { + + private SingleTestGenerator classGenerator = Mock(SingleTestGenerator) + + def "should find all .json files and generate 6 classes for them"() { + given: + File resource = new File(this.getClass().getResource("/directory/with/stubs/stubsRepositoryIndicator").toURI()) + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() + properties.contractsDslDir = resource.parentFile + TestGenerator testGenerator = new TestGenerator(properties, classGenerator, Stub(FileSaver)) + when: + testGenerator.generateTestClasses("org.springframework.cloud.contract.verifier") + then: + 6 * classGenerator.buildClass(_, _, _) >> "qwerty" + } + + def "should create class with full package"() { + given: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(targetFramework: TestFramework.SPOCK) + properties.contractsDslDir = new File(this.getClass().getResource("/directory/with/stubs/package").toURI()) + TestGenerator testGenerator = new TestGenerator(properties, classGenerator, Stub(FileSaver)) + when: + testGenerator.generateTestClasses("org.springframework.cloud.contract.verifier") + then: + 1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier') >> "spec" + 1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v1') >> "spec1" + 1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v2') >> "spec2" + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/MainTest.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/MainTest.groovy new file mode 100644 index 0000000000..261416ca7e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/MainTest.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier + +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.config.TestFramework +import org.springframework.cloud.contract.verifier.config.TestMode + +class MainTest { + public static void main(String[] args) { + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties( + contractsDslDir: new File('/some/path/dsl'), + generatedTestSourcesDir: new File('/tmp/contracts'), + targetFramework: TestFramework.SPOCK, testMode: TestMode.MOCKMVC, basePackageForTests: 'io.test', + staticImports: ['com.package.Test.*'], imports: ['org.package.Test'], excludedFiles: ["**/other"]) + println new TestGenerator(properties).generate() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/BookReturned.groovy new file mode 100644 index 0000000000..c47554348a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy new file mode 100644 index 0000000000..1c23b37367 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy @@ -0,0 +1,317 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import spock.lang.Shared +import spock.lang.Specification + +/** + * Tests used for the documentation + * + * @author Marcin Grzejszczak + */ +class ContractHttpDocsSpec extends Specification { + + @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) + + org.springframework.cloud.contract.spec.Contract httpDsl = + // tag::http_dsl[] + org.springframework.cloud.contract.spec.Contract.make { + // Definition of HTTP request part of the contract + // (this can be a valid request or invalid depending + // on type of contract being specified). + request { + //... + } + + // 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). + response { + //... + } + + // Contract priority, which can be used for overriding + // contracts (1 is highest). Priority is optional. + priority 1 + } + // end::http_dsl[] + + org.springframework.cloud.contract.spec.Contract request = + // tag::request[] + org.springframework.cloud.contract.spec.Contract.make { + request { + // HTTP request method (GET/POST/PUT/DELETE). + method 'GET' + + // Path component of request URL is specified as follows. + urlPath('/users') + } + + response { + //... + } + } + // end::request[] + + org.springframework.cloud.contract.spec.Contract url = + // tag::url[] + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + + // Specifying `url` and `urlPath` in one contract is illegal. + url('http://localhost:8888/users') + } + + response { + //... + } + } + // end::url[] + + org.springframework.cloud.contract.spec.Contract urlPaths = + // tag::urlpath[] + org.springframework.cloud.contract.spec.Contract.make { + request { + //... + + urlPath('/users') { + + // 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. + queryParameters { + + // If a simple literal is used as value + // default matcher function is used (equalTo) + parameter 'limit': 100 + + // `equalTo` function simply compares passed value + // using identity operator (==). + parameter 'filter': equalTo("email") + + // `containing` function matches strings + // that contains passed substring. + parameter 'gender': value(stub(containing("[mf]")), server('mf')) + + // `matching` function tests parameter + // against passed regular expression. + parameter 'offset': value(stub(matching("[0-9]+")), server(123)) + + // `notMatching` functions tests if parameter + // does not match passed regular expression. + parameter 'loginStartsWith': value(stub(notMatching(".{0,2}")), server(3)) + } + } + + //... + } + + response { + //... + } + } + // end::urlpath[] + + org.springframework.cloud.contract.spec.Contract headers = + // tag::headers[] + org.springframework.cloud.contract.spec.Contract.make { + request { + //... + + // Each header is added in form `'Header-Name' : 'Header-Value'`. + headers { + header 'Content-Type': 'application/json' + } + + //... + } + + response { + //... + } + } + // end::headers[] + + org.springframework.cloud.contract.spec.Contract body = + // tag::body[] + org.springframework.cloud.contract.spec.Contract.make { + request { + //... + + // JSON and XML formats of request body are supported. + // Format will be determined from a header or body's content. + body '''{ "login" : "john", "name": "John The Contract" }''' + } + + response { + //... + } + } + // end::body[] + + org.springframework.cloud.contract.spec.Contract bodyAsXml = + // tag::bodyAsXml[] + org.springframework.cloud.contract.spec.Contract.make { + request { + //... + + // In this case body will be formatted as XML. + body equalToXml( + '''johnJohn The Contract''' + ) + } + + response { + //... + } + } + // end::bodyAsXml[] + + org.springframework.cloud.contract.spec.Contract response = + // tag::response[] + org.springframework.cloud.contract.spec.Contract.make { + request { + //... + } + response { + // Status code sent by the server + // in response to request specified above. + status 200 + } + } + // end::response[] + + org.springframework.cloud.contract.spec.Contract regex = + // tag::regex[] + org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(~/\/[0-9]{2}/), server('/12')) + } + response { + status 200 + body( + id: value( + client('123'), + server(regex('[0-9]+')) + ), + surname: $( + client('Kowalsky'), + server('Lewandowski') + ), + name: 'Jan', + created: $(client('2014-02-02 12:23:43'), server(execute('currentDate(it)'))), + correlationId: value(client('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'), + server(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')) + ) + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + // end::regex[] + + org.springframework.cloud.contract.spec.Contract optionals = + // tag::optionals[] + org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(stub(optional(regex(email()))), test('abc@abc.com')), + callback_url: $(stub(regex(hostname())), test('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: value(stub("123123"), test(optional("123123"))) + ) + } + } + // end::optionals[] + + def 'should convert dsl with optionals to proper Spock test'() { + given: + BlockBuilder blockBuilder = new BlockBuilder(" ") + new MockMvcSpockMethodRequestProcessingBodyBuilder(optionals, properties).appendTo(blockBuilder) + expect: + String expectedTest = +// tag::optionals_test[] +""" + given: + def request = given() + .header('Content-Type', 'application/json') + .body('''{"email":"abc@abc.com","callback_url":"http://partners.com"}''') + + when: + def response = given().spec(request) + .post("/users/password") + + then: + response.statusCode == 404 + response.header('Content-Type') == 'application/json' + and: + DocumentContext parsedJson = JsonPath.parse(response.body.asString()) + assertThatJson(parsedJson).field("code").matches("(123123)?") +""" +// end::optionals_test[] + stripped(blockBuilder.toString()) == stripped(expectedTest) + } + + org.springframework.cloud.contract.spec.Contract method = + // tag::method[] + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url $(client(regex('^/api/[0-9]{2}$')), server('/api/12')) + headers { + header 'Content-Type': 'application/json' + } + body '''\ + [{ + "text": "Gonna see you at Warsaw" + }] + ''' + } + response { + body ( + path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))), + correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)'))) + ) + status 200 + } + } + // end::method[] + + private String stripped(String string) { + return string.stripMargin().stripIndent().replace('\t', '').replace('\n', '').replace(' ','') + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy new file mode 100644 index 0000000000..777c28a59c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy @@ -0,0 +1,683 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStubVerifier { + + @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) + + def "should generate assertions for simple response body with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": "a", + "property2": "b" +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").isEqualTo("b")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#187") + def "should generate assertions for null and boolean values with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": "true", + "property2": null, + "property3": false +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property3").isEqualTo(false)""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").isNull()""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("true")""") + and: + stubMappingIsValidWireMockStub(new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), contractDsl).toWireMockClientStub()) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#79") + def "should generate assertions for simple response body constructed from map with a list with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property1: 'a', + property2: [ + [a: 'sth'], + [b: 'sthElse'] + ] + ) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").contains("a").isEqualTo("sth")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").hasSize(2)""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").contains("b").isEqualTo("sthElse")""") + and: + stubMappingIsValidWireMockStub(new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), contractDsl).toWireMockClientStub()) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#82") + def "should generate proper request when body constructed from map with a list with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + body( + items: ['HOP'] + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | """entity('{\"items\":[\"HOP\"]}', 'application/json')""" + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'entity("{\\"items\\":[\\"HOP\\"]}", "application/json")' + } + + @Issue("#88") + def "should generate proper request when body constructed from GString with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + body( + "property1=VAL1" + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | """entity('property1=VAL1', 'application/octet-stream')""" + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'entity("\\"property1=VAL1\\"", "application/octet-stream")' + } + + def "should generate assertions for array in response body with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """[ +{ + "property1": "a" +}, +{ + "property2": "b" +}]""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).array().contains("property1").isEqualTo("a")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array().contains("property2").isEqualTo("b")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate assertions for array inside response body element with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": [ + { "property2": "test1"}, + { "property3": "test2"} + ] +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property1").contains("property2").isEqualTo("test1")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property1").contains("property3").isEqualTo("test2")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate assertions for nested objects in response body with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body '''\ +{ + "property1": "a", + "property2": {"property3": "b"} +} +''' + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").field("property3").isEqualTo("b")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate regex assertions for map objects in response body with #methodBodyName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property1: "a", + property2: value( + client('123'), + server(regex('[0-9]{3}')) + ) + ) + headers { + header('Content-Type': 'application/json') + + } + + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").matches("[0-9]{3}")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate regex assertions for string objects in response body with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body("""{"property1":"a","property2":"${value(client('123'), server(regex('[0-9]{3}')))}"}""") + headers { + header('Content-Type': 'application/json') + + } + + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").matches("[0-9]{3}")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should ignore 'Accept' header and use 'request' method with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + headers { + header("Accept", "text/plain") + } + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains(requestString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | requestString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | "request('text/plain')" + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'request("text/plain")' + } + + def "should ignore 'Content-Type' header and use 'entity' method with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "GET" + url "test" + headers { + header("Content-Type", "text/plain") + header("Timer", "123") + } + body '' + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + for (String requestString : requestStrings) { + blockBuilder.toString().contains(requestString) + } + !blockBuilder.toString().contains("""Content Type""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | requestStrings + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ["""entity('', 'text/plain')""", """header('Timer', '123')"""] + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | ['entity("\\"\\"", "text/plain")', 'header("Timer", "123")'] + } + + def "should generate a call with an url path and query parameters with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath('/users') { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server(equalTo("10"))) + parameter 'offset': $(client(containing("20")), server(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55")) + parameter 'age': $(client(notMatching("^\\w*\$")), server("99")) + parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov")) + parameter 'email': "bob@email.com" + parameter 'hello': $(client(matching("Denis.*")), server(absent())) + parameter 'hello': absent() + } + } + } + response { + status 200 + body """ + { + "property1": "a", + "property2": "b" + } + """ + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(modifyStringIfRequired.call("queryParam('limit', '10'")) + test.contains(modifyStringIfRequired.call("queryParam('offset', '20'")) + test.contains(modifyStringIfRequired.call("queryParam('filter', 'email'")) + test.contains(modifyStringIfRequired.call("queryParam('sort', 'name'")) + test.contains(modifyStringIfRequired.call("queryParam('search', '55'")) + test.contains(modifyStringIfRequired.call("queryParam('age', '99'")) + test.contains(modifyStringIfRequired.call("queryParam('name', 'Denis.Stepanov'")) + test.contains(modifyStringIfRequired.call("queryParam('email', 'bob@email.com'")) + test.contains(modifyStringIfRequired.call("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""")) + test.contains(modifyStringIfRequired.call("""assertThatJson(parsedJson).field("property2").isEqualTo("b")""")) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | modifyStringIfRequired + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String paramString -> paramString } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | { String paramString -> paramString.replace("'", "\"") } + } + + @Issue('#169') + def "should generate a call with an url path and query parameters with url containing a pattern with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url($(stub(regex('/foo/[0-9]+')), test('/foo/123456'))) { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server(equalTo("10"))) + parameter 'offset': $(client(containing("20")), server(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55")) + parameter 'age': $(client(notMatching("^\\w*\$")), server("99")) + parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov")) + parameter 'email': "bob@email.com" + parameter 'hello': $(client(matching("Denis.*")), server(absent())) + parameter 'hello': absent() + } + } + } + response { + status 200 + body """ + { + "property1": "a", + "property2": "b" + } + """ + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(modifyStringIfRequired.call("queryParam('limit', '10'")) + test.contains(modifyStringIfRequired.call("queryParam('offset', '20'")) + test.contains(modifyStringIfRequired.call("queryParam('filter', 'email'")) + test.contains(modifyStringIfRequired.call("queryParam('sort', 'name'")) + test.contains(modifyStringIfRequired.call("queryParam('search', '55'")) + test.contains(modifyStringIfRequired.call("queryParam('age', '99'")) + test.contains(modifyStringIfRequired.call("queryParam('name', 'Denis.Stepanov'")) + test.contains(modifyStringIfRequired.call("queryParam('email', 'bob@email.com'")) + test.contains(modifyStringIfRequired.call("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""")) + test.contains(modifyStringIfRequired.call("""assertThatJson(parsedJson).field("property2").isEqualTo("b")""")) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | modifyStringIfRequired + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String paramString -> paramString } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | { String paramString -> paramString.replace("'", "\"") } + } + + def "should generate test for empty body with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('POST') + url("/ws/payments") + body("") + } + response { + status 406 + } + } + + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | "entity('', 'application/octet-stream')" + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'entity("\\"\\"", "application/octet-stream"' + } + + def "should generate test for String in response body with #methodBodyName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "POST" + url "test" + } + response { + status 200 + body "test" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyDefinitionString) + test.contains(bodyEvaluationString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyDefinitionString | bodyEvaluationString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | "String responseAsString = response.readEntity(String)" | 'responseBody == "test"' + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'Object responseBody = (responseAsString);' | 'assertThat(responseBody).isEqualTo("test");' + } + + @Issue('#171') + def "should generate test with uppercase method name with #methodBuilderName"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "get" + url "/v1/some_cool_requests/e86df6f693de4b35ae648464c5b0dc08" + } + response { + status 200 + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + body """ +{"id":"789fgh","other_data":1268} +""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(methodString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | methodString + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ".method('GET')" + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | 'method("GET")' + } + + def "should generate a call with an url path and query parameters with JUnit - we'll put it into docs"() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath('/users') { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server(equalTo("10"))) + parameter 'offset': $(client(containing("20")), server(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55")) + parameter 'age': $(client(notMatching("^\\w*\$")), server("99")) + parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov")) + parameter 'email': "bob@email.com" + parameter 'hello': $(client(matching("Denis.*")), server(absent())) + parameter 'hello': absent() + } + } + } + response { + status 200 + body """ + { + "property1": "a" + } + """ + } + } + MethodBodyBuilder builder = new JaxRsClientJUnitMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedResponse = +// tag::jaxrs[] +''' + // when: + Response response = webTarget + .path("/users") + .queryParam("limit", "10") + .queryParam("offset", "20") + .queryParam("filter", "email") + .queryParam("sort", "name") + .queryParam("search", "55") + .queryParam("age", "99") + .queryParam("name", "Denis.Stepanov") + .queryParam("email", "bob@email.com") + .request() + .method("GET"); + + String responseAsString = response.readEntity(String.class); + + // then: + assertThat(response.getStatus()).isEqualTo(200); + // and: + DocumentContext parsedJson = JsonPath.parse(responseAsString); + assertThatJson(parsedJson).field("property1").isEqualTo("a"); +''' +// end::jaxrs[] + stripped(test) == stripped(expectedResponse) + and: + stubMappingIsValidWireMockStub(contractDsl) + } + + private String stripped(String string) { + return string.stripMargin().stripIndent().replace('\t', '').replace('\n', '').replace(' ','') + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy new file mode 100644 index 0000000000..2257459583 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy @@ -0,0 +1,447 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import spock.lang.Shared +import spock.lang.Specification +/** + * @author Marcin Grzejszczak + */ +class MessagingMethodBodyBuilderSpec extends Specification { + + @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) + + def "should work for triggered based messaging with Spock"() { + given: +// tag::trigger_method_dsl[] +def contractDsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('activemq:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } +} +// end::trigger_method_dsl[] + MethodBodyBuilder builder = new SpockMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMessage = +// tag::trigger_method_test[] +''' + when: + bookReturnedTriggered() + + then: + def response = contractVerifierMessaging.receiveMessage('activemq:output') + assert response != null + response.getHeader('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + assertThatJson(parsedJson).field("bookName").isEqualTo("foo") + +''' +// end::trigger_method_test[] + stripped(test) == stripped(expectedMessage) + } + + def "should work for triggered based messaging with JUnit"() { + given: + def contractDsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('activemq:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + MethodBodyBuilder builder = new JUnitMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMessage = +// tag::trigger_method_junit_test[] +''' + // when: + bookReturnedTriggered(); + + // then: + ContractVerifierMessage response = contractVerifierMessaging.receiveMessage("activemq:output"); + assertThat(response).isNotNull(); + assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo"); + // and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' +// end::trigger_method_junit_test[] + stripped(test) == stripped(expectedMessage) + } + + def "should generate tests triggered by a message for Spock"() { + given: + // tag::trigger_message_dsl[] +def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} + // end::trigger_message_dsl[] + MethodBodyBuilder builder = new SpockMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMessage = +// tag::trigger_message_spock[] +"""\ +given: + def inputMessage = contractVerifierMessaging.create( + '''{"bookName":"foo"}''', + ['sample': 'header'] + ) + +when: + contractVerifierMessaging.send(inputMessage, 'jms:input') + +then: + def response = contractVerifierMessaging.receiveMessage('jms:output') + assert response !- null + response.getHeader('BOOK-NAME') == 'foo' +and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + assertThatJson(parsedJson).field("bookName").isEqualTo("foo") +""" +// end::trigger_message_spock[] + stripped(test) == stripped(expectedMessage) + } + + def "should generate tests triggered by a message for JUnit"() { + given: + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + MethodBodyBuilder builder = new JUnitMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMessage = +// tag::trigger_message_junit[] +''' +// given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" +, headers() + .header("sample", "header")); + +// when: + contractVerifierMessaging.send(inputMessage, "jms:input"); + +// then: + ContractVerifierMessage response = contractVerifierMessaging.receiveMessage("jms:output"); + assertThat(response).isNotNull(); + assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo"); +// and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' +// end::trigger_message_junit[] + stripped(test) == stripped(expectedMessage) + } + + def "should generate tests without destination, triggered by a message"() { + given: + // tag::trigger_no_output_dsl[] +def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } +} + // end::trigger_no_output_dsl[] + MethodBodyBuilder builder = new SpockMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMsg = +// tag::trigger_no_output_spock[] +''' +given: + def inputMessage = contractVerifierMessaging.create( + \'\'\'{"bookName":"foo"}\'\'\', + ['sample': 'header'] + ) + +when: + contractVerifierMessaging.send(inputMessage, 'jms:delete') + +then: + noExceptionThrown() + bookWasDeleted() +''' +// end::trigger_no_output_spock[] + stripped(test) == stripped(expectedMsg) + } + + def "should generate tests without destination, triggered by a message for JUnit"() { + given: + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + MethodBodyBuilder builder = new JUnitMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMsg = +// tag::trigger_no_output_junit[] +''' +// given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" +, headers() + .header("sample", "header")); + +// when: + contractVerifierMessaging.send(inputMessage, "jms:delete"); + +// then: + bookWasDeleted(); +''' +// end::trigger_no_output_junit[] + stripped(test) == stripped(expectedMsg) + } + + def "should generate tests without headers for JUnit"() { + given: + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + } + } + MethodBodyBuilder builder = new JUnitMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMsg = +''' + // given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" + , headers() + .header("sample", "header")); + + // when: + contractVerifierMessaging.send(inputMessage, "jms:input"); + + // then: + ContractVerifierMessage response = contractVerifierMessaging.receiveMessage("jms:output"); + assertThat(response).isNotNull(); + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' + stripped(test) == stripped(expectedMsg) + } + + def "should generate tests without headers for Spock"() { + given: + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + } + } + MethodBodyBuilder builder = new SpockMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMsg = +""" + given: + def inputMessage = contractVerifierMessaging.create('''{"bookName":"foo"}''' + ,[ + 'sample': 'header' + ]) + + when: + contractVerifierMessaging.send(inputMessage, 'jms:input') + + then: + def response = contractVerifierMessaging.receiveMessage('jms:output') + assert response != null + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + assertThatJson(parsedJson).field("bookName").isEqualTo("foo") +""" + stripped(test) == stripped(expectedMsg) + } + + private String stripped(String text) { + return text.stripIndent().stripMargin().replace(' ', '').replace('\n', '').replace('\t', '').replaceAll("\\W", "") + } + + def "should generate tests without headers for JUnit with consumer / producer notation"() { + given: + def contractDsl = + // tag::consumer_producer[] +Contract.make { + label 'some_label' + input { + messageFrom value(consumer('jms:output'), producer('jms:input')) + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo $(consumer('jms:input'), producer('jms:output')) + body([ + bookName: 'foo' + ]) + } +} + // end::consumer_producer[] + MethodBodyBuilder builder = new JUnitMessagingMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + String expectedMsg = + ''' + // given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" + , headers() + .header("sample", "header")); + + // when: + contractVerifierMessaging.send(inputMessage, "jms:input"); + + // then: + ContractVerifierMessage response = contractVerifierMessaging.receiveMessage("jms:output"); + assertThat(response).isNotNull(); + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' + stripped(test) == stripped(expectedMsg) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy new file mode 100644 index 0000000000..7f8dec46f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -0,0 +1,1447 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import java.util.regex.Pattern +/** + * @author Jakub Kubrynski, codearte.io + */ +class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStubVerifier { + + @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) + + @Shared + Contract dslWithOptionalsInString = Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(stub(optional(regex(email()))), test('abc@abc.com')), + callback_url: $(stub(regex(hostname())), test('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: value(stub("123123"), test(optional("123123"))), + message: "User not found by email = [${value(test(regex(email())), stub('not.existing@user.com'))}]" + ) + } + } + + @Shared + Contract dslWithOptionals = Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + """ { + "email" : "${ + value(stub(optional(regex(email()))), test('abc@abc.com')) + }", + "callback_url" : "${ + value(client(regex(hostname())), server('http://partners.com')) + }" + } + """ + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + """ { + "code" : "${value(stub(123123), test(optional(123123)))}", + "message" : "User not found by email = [${ + value(server(regex(email())), client('not.existing@user.com')) + }]" + } + """ + ) + } + } + + def "should generate assertions for simple response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": "a", + "property2": "b" +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").isEqualTo("b")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#187") + def "should generate assertions for null and boolean values with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": "true", + "property2": null, + "property3": false +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("true")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").isNull()""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property3").isEqualTo(false)""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#79") + def "should generate assertions for simple response body constructed from map with a list with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property1: 'a', + property2: [ + [a: 'sth'], + [b: 'sthElse'] + ] + ) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").contains("a").isEqualTo("sth")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").hasSize(2)""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property2").contains("b").isEqualTo("sthElse")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue("#82") + def "should generate proper request when body constructed from map with a list #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + body( + items: ['HOP'] + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | """.body('''{\"items\":[\"HOP\"]}''')""" + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '.body("{\\"items\\":[\\"HOP\\"]}")' + } + + @Issue("#88") + def "should generate proper request when body constructed from GString with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + body( + "property1=VAL1" + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | """.body('''property1=VAL1''')""" + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '.body("\\"property1=VAL1\\"")' + } + + @Issue("185") + def "should generate assertions for a response body containing map with integers as keys with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property: [ + 14: 0.0, + 7 : 0.0 + ] + ) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property").field(7).isEqualTo(0.0)""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property").field(14).isEqualTo(0.0)""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate assertions for array in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """[ +{ + "property1": "a" +}, +{ + "property2": "b" +}]""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).array().contains("property2").isEqualTo("b")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array().contains("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate assertions for array inside response body element with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body """{ + "property1": [ + { "property2": "test1"}, + { "property3": "test2"} + ] +}""" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property1").contains("property2").isEqualTo("test1")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).array("property1").contains("property3").isEqualTo("test2")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate assertions for nested objects in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body '''\ +{ + "property1": "a", + "property2": {"property3": "b"} +} +''' + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").field("property3").isEqualTo("b")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate regex assertions for map objects in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property1: "a", + property2: value( + client('123'), + server(regex('[0-9]{3}')) + ) + ) + headers { + header('Content-Type': 'application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").matches("[0-9]{3}")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate regex assertions for string objects in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body("""{"property1":"a","property2":"${ + value(client('123'), server(regex('[0-9]{3}'))) + }"}""") + headers { + header('Content-Type': 'application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").matches("[0-9]{3}")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue(["#126", "#143"]) + def "should generate escaped regex assertions for string objects in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body("""{"property":" ${ + value(client('123'), server(regex('\\d+'))) + }"}""") + headers { + header('Content-Type': 'application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property").matches("\\\\d+")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate a call with an url path and query parameters with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath('/users') { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server(equalTo("10"))) + parameter 'offset': $(client(containing("20")), server(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55")) + parameter 'age': $(client(notMatching("^\\w*\$")), server("99")) + parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov")) + parameter 'email': "bob@email.com" + parameter 'hello': $(client(matching("Denis.*")), server(absent())) + parameter 'hello': absent() + } + } + } + response { + status 200 + body """ + { + "property1": "a", + "property2": "b" + } + """ + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('get("/users?limit=10&offset=20&filter=email&sort=name&search=55&age=99&name=Denis.Stepanov&email=bob@email.com")') + test.contains('assertThatJson(parsedJson).field("property1").isEqualTo("a")') + test.contains('assertThatJson(parsedJson).field("property2").isEqualTo("b")') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('#169') + def "should generate a call with an url path and query parameters with url containing a pattern with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url($(stub(regex('/foo/[0-9]+')), test('/foo/123456'))) { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server(equalTo("10"))) + parameter 'offset': $(client(containing("20")), server(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55")) + parameter 'age': $(client(notMatching("^\\w*\$")), server("99")) + parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov")) + parameter 'email': "bob@email.com" + parameter 'hello': $(client(matching("Denis.*")), server(absent())) + parameter 'hello': absent() + } + } + } + response { + status 200 + body """ + { + "property1": "a", + "property2": "b" + } + """ + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('get("/foo/123456?limit=10&offset=20&filter=email&sort=name&search=55&age=99&name=Denis.Stepanov&email=bob@email.com")') + test.contains('assertThatJson(parsedJson).field("property1").isEqualTo("a")') + test.contains('assertThatJson(parsedJson).field("property2").isEqualTo("b")') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should generate test for empty body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method('POST') + url("/ws/payments") + body("") + } + response { + status 406 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ".body('''''')" + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | ".body(\"\\\"\\\"\")" + } + + def "should generate test for String in response body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "POST" + url "test" + } + response { + status 200 + body "test" + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyDefinitionString) + test.contains(bodyEvaluationString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyDefinitionString | bodyEvaluationString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | 'def responseBody = (response.body.asString())' | 'responseBody == "test"' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | 'Object responseBody = (response.getBody().asString());' | 'assertThat(responseBody).isEqualTo("test");' + } + + @Issue('113') + def "should generate regex test for String in response header with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'POST' + url $(client(regex('/partners/[0-9]+/users')), server('/partners/1000/users')) + headers { header 'Content-Type': 'application/json' } + body( + first_name: 'John', + last_name: 'Smith', + personal_id: '12345678901', + phone_number: '500500500', + invitation_token: '00fec7141bb94793bfe7ae1d0f39bda0', + password: 'john' + ) + } + response { + status 201 + headers { + header 'Location': $(client('http://localhost/partners/1000/users/1001'), server(regex('http://localhost/partners/[0-9]+/users/[0-9]+'))) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(headerEvaluationString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | headerEvaluationString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '''response.header('Location') ==~ java.util.regex.Pattern.compile('http://localhost/partners/[0-9]+/users/[0-9]+')''' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | 'assertThat(response.header("Location")).matches("http://localhost/partners/[0-9]+/users/[0-9]+");' + } + + @Issue('115') + def "should generate regex with helper method with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'POST' + url $(client(regex('/partners/[0-9]+/users')), server('/partners/1000/users')) + headers { header 'Content-Type': 'application/json' } + body( + first_name: 'John', + last_name: 'Smith', + personal_id: '12345678901', + phone_number: '500500500', + invitation_token: '00fec7141bb94793bfe7ae1d0f39bda0', + password: 'john' + ) + } + response { + status 201 + headers { + header 'Location': $(client('http://localhost/partners/1000/users/1001'), server(regex("^${hostname()}/partners/[0-9]+/users/[0-9]+"))) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(headerEvaluationString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | headerEvaluationString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '''response.header('Location') ==~ java.util.regex.Pattern.compile('^((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?/partners/[0-9]+/users/[0-9]+')''' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | 'assertThat(response.header("Location")).matches("^((http[s]?|ftp):/)/?([^:/s]+)(:[0-9]{1,5})?/partners/[0-9]+/users/[0-9]+");' + } + + def "should work with more complex stuff and jsonpaths with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + priority 10 + request { + method 'POST' + url '/validation/client' + headers { + header 'Content-Type': 'application/json' + } + body( + bank_account_number: '0014282912345698765432161182', + email: 'foo@bar.com', + phone_number: '100299300', + personal_id: 'ABC123456' + ) + } + + response { + status 200 + body(errors: [ + [property: "bank_account_number", message: "incorrect_format"] + ]) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains("""assertThatJson(parsedJson).array("errors").contains("property").isEqualTo("bank_account_number")""") + test.contains("""assertThatJson(parsedJson).array("errors").contains("message").isEqualTo("incorrect_format")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should work properly with GString url with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + + request { + method 'PUT' + url "/partners/${value(client(regex('^[0-9]*$')), server('11'))}/agents/11/customers/09665703Z" + headers { + header 'Content-Type': 'application/json' + } + body( + first_name: 'Josef', + ) + } + response { + status 422 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''/partners/11/agents/11/customers/09665703Z''') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + def "should resolve properties in GString with regular expression with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(client(regex(email())), server('not.existing@user.com')), + callback_url: $(client(regex(hostname())), server('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: 4, + message: "User not found by email = [${value(server(regex(email())), client('not.existing@user.com'))}]" + ) + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains("""assertThatJson(parsedJson).field("message").matches("User not found by email = \\\\\\\\[[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\\\\\.[a-zA-Z]{2,4}\\\\\\\\]")""") + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('42') + def "should not omit the optional field in the test creation with MockMvcSpockMethodBodyBuilder"() { + given: + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''"email":"abc@abc.com"''') + test.contains("""assertThatJson(parsedJson).field("code").matches("(123123)?")""") + !test.contains('''REGEXP''') + !test.contains('''OPTIONAL''') + !test.contains('''OptionalProperty''') + where: + contractDsl << [dslWithOptionals, dslWithOptionalsInString] + } + + @Issue('42') + def "should not omit the optional field in the test creation with MockMvcJUnitMethodBodyBuilder"() { + given: + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('\\"email\\":\\"abc@abc.com\\"') + test.contains('assertThatJson(parsedJson).field("code").matches("(123123)?");') + !test.contains('''REGEXP''') + !test.contains('''OPTIONAL''') + !test.contains('''OptionalProperty''') + where: + contractDsl << [dslWithOptionals, dslWithOptionalsInString] + } + + @Issue('72') + def "should make the execute method work with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${ + value(client(regex('[0-9]{10}')), server('1234567890')) + }", + "loanAmount":123.123 + } + """ + ) + headers { + header("""Content-Type""", """application/vnd.fraud.v1+json""") + } + + } + response { + status 200 + body("""{ + "fraudCheckStatus": "OK", + "rejectionReason": ${ + value(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + } +}""") + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + header 'Location': value( + stub(null), + test(execute('assertThatLocationIsNull($it)')) + ) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + String test = blockBuilder.toString() + then: + assertionStrings.each { String assertionString -> + assert test.contains(assertionString) + } + where: + methodBuilderName | methodBuilder | assertionStrings + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ['''assertThatRejectionReasonIsNull(parsedJson.read('$.rejectionReason'))''', '''assertThatLocationIsNull(response.header('Location'))'''] + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | ['''assertThatRejectionReasonIsNull(parsedJson.read("$.rejectionReason"))''', '''assertThatLocationIsNull(response.header("Location"))'''] + } + + def "should support inner map and list definitions with #methodBuilderName"() { + given: + + Pattern PHONE_NUMBER = Pattern.compile(/[+\w]*/) + Pattern ANYSTRING = Pattern.compile(/.*/) + Pattern NUMBERS = Pattern.compile(/[\d\.]*/) + Pattern DATETIME = ANYSTRING + + Contract contractDsl = Contract.make { + request { + method "PUT" + url "/v1/payments/e86df6f693de4b35ae648464c5b0dc09/client_data" + headers { + header('Content-Type': 'application/json') + } + body( + client: [ + first_name : $(stub(regex(onlyAlphaUnicode())), test('Denis')), + last_name : $(stub(regex(onlyAlphaUnicode())), test('FakeName')), + email : $(stub(regex(email())), test('fakemail@fakegmail.com')), + fax : $(stub(PHONE_NUMBER), test('+xx001213214')), + phone : $(stub(PHONE_NUMBER), test('2223311')), + data_of_birth: $(stub(DATETIME), test('2002-10-22T00:00:00Z')) + ], + client_id_card: [ + id : $(stub(ANYSTRING), test('ABC12345')), + date_of_issue: $(stub(ANYSTRING), test('2002-10-02T00:00:00Z')), + address : [ + street : $(stub(ANYSTRING), test('Light Street')), + city : $(stub(ANYSTRING), test('Fire')), + region : $(stub(ANYSTRING), test('Skys')), + country: $(stub(ANYSTRING), test('HG')), + zip : $(stub(NUMBERS), test('658965')) + ] + ], + incomes_and_expenses: [ + monthly_income : $(stub(NUMBERS), test('0.0')), + monthly_loan_repayments: $(stub(NUMBERS), test('100')), + monthly_living_expenses: $(stub(NUMBERS), test('22')) + ], + additional_info: [ + allow_to_contact: $(stub(optional(regex(anyBoolean()))), test('true')) + ] + ) + } + response { + status 200 + headers { + header('Content-Type': 'application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains bodyString + !test.contains("clientValue") + !test.contains("cursor") + where: + methodBuilderName | methodBuilder | bodyString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '"street":"Light Street"' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '\\"street\\":\\"Light Street\\"' + + } + + def "shouldn't generate unicode escape characters with #methodBuilderName"() { + given: + Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/) + + Contract contractDsl = Contract.make { + request { + method "PUT" + url "/v1/payments/e86df6f693de4b35ae648464c5b0dc09/енев" + headers { + header('Content-Type': 'application/json') + } + body( + client: [ + first_name: $(stub(ONLY_ALPHA_UNICODE), test('Пенева')), + last_name : $(stub(ONLY_ALPHA_UNICODE), test('Пенева')) + ] + ) + } + response { + status 200 + headers { + header('Content-Type': 'application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains("\\u041f") + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('177') + def "should generate proper test code when having multiline body with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "PUT" + url "/multiline" + body('''hello, +World.''') + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.given(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyString) + where: + methodBuilderName | methodBuilder | bodyString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | """'''hello, +World.'''""" + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '\\"hello,\\nWorld.\\"' + } + + @Issue('180') + def "should generate proper test code when having multipart parameters with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "PUT" + url "/multipart" + headers { + header('content-type', 'multipart/form-data;boundary=AaB03x') + } + multipart( + formParameter: value(client(regex('.+')), server('"formParameterValue"')), + someBooleanParameter: value(client(regex('(true|false)')), server('true')), + file: named(value(client(regex('.+')), server('filename.csv')), value(client(regex('.+')), server('file content'))) + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + for (String requestString : requestStrings) { + test.contains(requestString) + } + where: + methodBuilderName | methodBuilder | requestStrings + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ["""'content-type', 'multipart/form-data;boundary=AaB03x'""", + """.param('formParameter', '"formParameterValue"'""", + """.param('someBooleanParameter', 'true')""", + """.multiPart('file', 'filename.csv', 'file content'.bytes)"""] + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | ['"content-type", "multipart/form-data;boundary=AaB03x"', + '.param("formParameter", "\\"formParameterValue\\"")', + '.param("someBooleanParameter", "true")', + '.multiPart("file", "filename.csv", "file content".getBytes());'] + } + + @Issue('180') + def "should generate proper test code when having multipart parameters with named as map with #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method "PUT" + url "/multipart" + multipart( + formParameter: value(client(regex('".+"')), server('"formParameterValue"')), + someBooleanParameter: value(client(regex('(true|false)')), server('true')), + file: named( + name: value(client(regex('.+')), server('filename.csv')), + content: value(client(regex('.+')), server('file content'))) + ) + } + response { + status 200 + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.given(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('.multiPart') + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('#216') + def "should parse JSON with arrays using Spock"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + urlPath('/auth/oauth/check_token') { + queryParameters { + parameter 'token': + value( + client(regex('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')), + server('6973b31d-7140-402a-bca6-1cdb954e03a7') + ) + } + } + } + response { + status 200 + body( + authorities: [ + value(stub('ROLE_ADMIN'), test(regex('^[a-zA-Z0-9_\\- ]+$'))) + ] + ) + } + } + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''assertThatJson(parsedJson).array("authorities").arrayField().matches("^[a-zA-Z0-9_\\\\- ]+\\$").value()''') + } + + @Issue('#216') + def "should parse JSON with arrays using JUnit"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + urlPath('/auth/oauth/check_token') { + queryParameters { + parameter 'token': + value( + client(regex('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')), + server('6973b31d-7140-402a-bca6-1cdb954e03a7') + ) + } + } + } + response { + status 200 + body( + authorities: [ + value(stub('ROLE_ADMIN'), test(regex('^[a-zA-Z0-9_\\- ]+$'))) + ] + ) + } + } + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''assertThatJson(parsedJson).array("authorities").arrayField().matches("^[a-zA-Z0-9_\\\\- ]+$").value()''') + } + + def "should work with execution property"() { + given: + Contract contractDsl = Contract.make { + request { + method 'PUT' + url '/fraudcheck' + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + } + + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains('''assertThatJson(parsedJson).field("rejectionReason").isEqualTo("assertThatRejectionReasonIsNull("''') + test.contains('''assertThatRejectionReasonIsNull(''') + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('262') + def "should generate proper test code with map inside list"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/foos' + } + response { + status 200 + body([[id: value( + client('123'), + server(regex('[0-9]+')) + )], [id: value( + client('567'), + server(regex('[0-9]+')) + )]]) + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + } + } + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThatJson(parsedJson).array().contains("id").matches("[0-9]+")') + } + + @Issue('266') + def "should generate proper test code with top level array using #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/api/tags' + } + response { + status 200 + body(["Java", "Java8", "Spring", "SpringBoot", "Stream"]) + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThatJson(parsedJson).hasSize(5)') + test.contains('assertThatJson(parsedJson).arrayField().contains("Java8").value()') + test.contains('assertThatJson(parsedJson).arrayField().contains("Spring").value()') + test.contains('assertThatJson(parsedJson).arrayField().contains("Java").value()') + test.contains('assertThatJson(parsedJson).arrayField().contains("Stream").value()') + test.contains('assertThatJson(parsedJson).arrayField().contains("SpringBoot").value()') + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('266') + def "should generate proper test code with top level array or arrays using #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/api/categories' + } + response { + status 200 + body([["Programming", "Java"], ["Programming", "Java", "Spring", "Boot"]]) + headers { + header('Content-Type': 'application/json;charset=UTF-8') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThatJson(parsedJson).hasSize(2)') + test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Programming").value()') + test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Java").value()') + test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Spring").value()') + test.contains('assertThatJson(parsedJson).array().arrayField().isEqualTo("Boot").value()') + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('47') + def "should generate async body when async flag set in response"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url '/test' + } + response { + status 200 + async() + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains(bodyDefinitionString) + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder | bodyDefinitionString + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '.when().async()' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '.when().async()' + } + + def "should generate proper test code with array of primitives using #methodBuilderName"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/api/tags' + } + response { + status 200 + body('''{ + "partners":[ + { + "payment_methods":[ "BANK", "CASH" ] + } + ] + } + ''') + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThatJson(parsedJson).array("partners").array("payment_methods").arrayField().isEqualTo("BANK").value()') + test.contains('assertThatJson(parsedJson).array("partners").array("payment_methods").arrayField().isEqualTo("CASH").value()') + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('#273') + def "should not escape dollar in Spock regex tests"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + } + response { + status 200 + body( code: 9, message: $(client('Wrong credentials'), server(regex('^(?!\\s*$).+'))) ) + } + } + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThatJson(parsedJson).field("message").matches("^(?!\\\\s*\\$).+")') + } + + + Contract dslForDocs = + // tag::dsl_example[] + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/api/12' + headers { + header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json' + } + body '''\ + [{ + "created_at": "Sat Jul 26 09:38:57 +0000 2014", + "id": 492967299297845248, + "id_str": "492967299297845248", + "text": "Gonna see you at Warsaw", + "place": + { + "attributes":{}, + "bounding_box": + { + "coordinates": + [[ + [-77.119759,38.791645], + [-76.909393,38.791645], + [-76.909393,38.995548], + [-77.119759,38.995548] + ]], + "type":"Polygon" + }, + "country":"United States", + "country_code":"US", + "full_name":"Washington, DC", + "id":"01fbe706f872cb32", + "name":"Washington", + "place_type":"city", + "url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json" + } + }] + ''' + } + response { + status 200 + } + } + // end::dsl_example[] +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy new file mode 100644 index 0000000000..8bb9f1591f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.config.TestMode +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import spock.lang.Specification + +import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT +import static org.springframework.cloud.contract.verifier.config.TestFramework.SPOCK + +class SingleTestGeneratorSpec extends Specification { + + @Rule + TemporaryFolder tmpFolder = new TemporaryFolder() + File file + + static List jUnitClassStrings = ['package test;', 'import com.jayway.jsonpath.DocumentContext;', 'import com.jayway.jsonpath.JsonPath;', + 'import org.junit.FixMethodOrder;', 'import org.junit.Ignore;', 'import org.junit.Test;', 'import org.junit.runners.MethodSorters;', + 'import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;', 'import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;', + '@FixMethodOrder(MethodSorters.NAME_ASCENDING)', '@Test', '@Ignore', 'mport com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;', + 'import com.jayway.restassured.response.ResponseOptions;', 'import static org.assertj.core.api.Assertions.assertThat;'] + + static List spockClassStrings = ['package test', 'import com.jayway.jsonpath.DocumentContext', 'import com.jayway.jsonpath.JsonPath', + 'import spock.lang.Ignore', 'import spock.lang.Specification', 'import spock.lang.Stepwise', + 'import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson', 'import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*', + '@Stepwise', '@Ignore'] + + def setup() { + file = tmpFolder.newFile() + file.write(""" + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url 'url' + } + response { + status 200 + } + } +""") + } + + def "should build MockMvc test class for #testFramework"() { + given: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(); + properties.targetFramework = testFramework + ContractMetadata contract = new ContractMetadata(file.toPath(), true, 1, 2) + contract.ignored >> true + contract.order >> 2 + SingleTestGenerator testGenerator = new SingleTestGenerator(properties) + + when: + String clazz = testGenerator.buildClass([contract], "test", "test") + + then: + classStrings.each { clazz.contains(it) } + + where: + testFramework | classStrings + JUNIT | jUnitClassStrings + SPOCK | spockClassStrings + } + + def "should build JaxRs test class for #testFramework"() { + given: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(); + properties.testMode = TestMode.JAXRSCLIENT + properties.targetFramework = testFramework + ContractMetadata contract = new ContractMetadata(file.toPath(), true, 1, 2) + contract.ignored >> true + contract.order >> 2 + SingleTestGenerator testGenerator = new SingleTestGenerator(properties) + + when: + String clazz = testGenerator.buildClass([contract], "test", "test") + + then: + classStrings.each { clazz.contains(it) } + + where: + testFramework | classStrings + JUNIT | ['import static javax.ws.rs.client.Entity.*;', 'import javax.ws.rs.core.Response;'] + SPOCK | ['import static javax.ws.rs.client.Entity.*;'] + } + + def "should work if there is messaging and rest in one folder #testFramework"() { + given: + File secondFile = tmpFolder.newFile() + secondFile.write(""" + org.springframework.cloud.contract.spec.Contract.make { + label 'some_label' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + """) + and: + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(); + properties.targetFramework = testFramework + ContractMetadata contract = new ContractMetadata(file.toPath(), true, 1, 2) + contract.ignored >> true + contract.order >> 2 + and: + ContractMetadata contract2 = new ContractMetadata(secondFile.toPath(), true, 1, 2) + contract2.ignored >> true + contract2.order >> 2 + and: + SingleTestGenerator testGenerator = new SingleTestGenerator(properties) + + when: + String clazz = testGenerator.buildClass([contract, contract2], "test", "test") + + then: + classStrings.each { clazz.contains(it) } + clazz.contains('@Inject ContractVerifierMessaging') + + where: + testFramework | classStrings + JUNIT | jUnitClassStrings + SPOCK | spockClassStrings + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy new file mode 100755 index 0000000000..92c0f26a49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy @@ -0,0 +1,1577 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl + +import groovy.json.JsonBuilder +import groovy.json.JsonSlurper +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import org.springframework.cloud.contract.verifier.util.AssertionUtil +import spock.lang.Issue +import spock.lang.Specification + +class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifier { + + def 'should convert groovy dsl stub to wireMock stub for the client side'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(~/\/[0-9]{2}/), server('/12')) + } + response { + status 200 + body( + id: value( + client('123'), + server(regex('[0-9]+')) + ), + surname: $( + client('Kowalsky'), + server('Lewandowski') + ), + name: 'Jan', + created: $(client('2014-02-02 12:23:43'), server({ currentDate(it) })) + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual(''' + { + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "{\\"id\\":\\"123\\",\\"surname\\":\\"Kowalsky\\",\\"name\\":\\"Jan\\",\\"created\\":\\"2014-02-02 12:23:43\\"}", + "headers" : { + "Content-Type" : "text/plain" + } + } + } + ''', wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + @Issue("#79") + def 'should convert groovy dsl stub to wireMock stub for the client side with a body containing a map'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url '/ingredients' + headers { + header 'Content-Type': 'application/vnd.pl.devoxx.aggregatr.v1+json' + } + } + response { + status 200 + body( + ingredients: [ + [type: 'MALT', quantity: 100], + [type: 'WATER', quantity: 200], + [type: 'HOP', quantity: 300], + [type: 'YIEST', quantity: 400] + ] + ) + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual(''' +{ + "request" : { + "url" : "/ingredients", + "method" : "GET", + "headers" : { + "Content-Type" : { + "equalTo" : "application/vnd.pl.devoxx.aggregatr.v1+json" + } + } + }, + "response" : { + "status" : 200, + "body" : "{\\"ingredients\\":[{\\"type\\":\\"MALT\\",\\"quantity\\":100},{\\"type\\":\\"WATER\\",\\"quantity\\":200},{\\"type\\":\\"HOP\\",\\"quantity\\":300},{\\"type\\":\\"YIEST\\",\\"quantity\\":400}]}" + } +} +''', wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + @Issue("#86") + def 'should convert groovy dsl stub with GString and regexp'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('POST') + url('/ws/payments') + headers { + header("Content-Type": 'application/x-www-form-urlencoded') + } + body("""paymentType=INCOMING&transferType=BANK&amount=${ + value(client(regex('[0-9]{3}\\.[0-9]{2}')), server(500.00)) + }&bookingDate=${ + value(client(regex('[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])')), server('2015-05-18')) + }""") + } + response { + status 204 + body( + paymentId: value(client('4'), server(regex('[1-9][0-9]*'))), + foundExistingPayment: false + ) + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual(''' +{ + "request": { + "method": "POST", + "headers": { + "Content-Type": { + "equalTo": "application/x-www-form-urlencoded" + } + }, + "url": "/ws/payments", + "bodyPatterns": [ + { + "matches": "paymentType=INCOMING&transferType=BANK&amount=[0-9]{3}\\\\.[0-9]{2}&bookingDate=[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])" + } + ] + }, + "response": { + "status": 204, + "body": "{\\"paymentId\\":\\"4\\",\\"foundExistingPayment\\":false}" + } +} +''', wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should convert groovy dsl stub with Body as String to wireMock stub for the client side'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(~/\/[0-9]{2}/), server('/12')) + } + response { + status 200 + body("""\ + { + "id": "${value(client('123'), server('321'))}", + "surname": "${value(client('Kowalsky'), server('Lewandowski'))}", + "name": "Jan", + "created" : "${$(client('2014-02-02 12:23:43'), server('2999-09-09 01:23:45'))}" + } + """ + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "{\\"created\\":\\"2014-02-02 12:23:43\\",\\"id\\":\\"123\\",\\"name\\":\\"Jan\\",\\"surname\\":\\"Kowalsky\\"}", + "headers" : { + "Content-Type" : "text/plain" + } + } +} +'''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should convert groovy dsl stub with simple Body as String to wireMock stub for the client side'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(regex('/[0-9]{2}')), server('/12')) + body """ + { + "name": "Jan" + } + """ + } + response { + status 200 + body("""\ + { + "name": "Jan" + } + """ + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual(''' +{ + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.name == 'Jan')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"name\\":\\"Jan\\"}", + "headers" : { + "Content-Type" : "text/plain" + } + } +} +''', wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should use equalToJson when body match is defined as map'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(~/\/[0-9]{2}/), server('/12')) + body( + id: value( + client('123'), + server({ regex('[0-9]+') }) + ), + surname: $( + client('Kowalsky'), + server('Lewandowski') + ), + name: 'Jan', + created: $(client('2014-02-02 12:23:43'), server({ currentDate(it) })) + ) + } + response { + status 200 + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.created == '2014-02-02 12:23:43')]" + }, { + "matchesJsonPath" : "$[?(@.surname == 'Kowalsky')]" + }, { + "matchesJsonPath" : "$[?(@.name == 'Jan')]" + }, { + "matchesJsonPath" : "$[?(@.id == '123')]" + } ] + }, + "response" : { + "status" : 200 + } +} + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should use equalToJson when content type ends with json'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + headers { + header "Content-Type", "customtype/json" + } + body """ + { + "name": "Jan" + } + """ + } + response { + status 200 + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "url" : "/users", + "method" : "GET", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.name == 'Jan')]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "customtype/json" + } + } + }, + "response" : { + "status" : 200 + } +} + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should use equalToXml when content type ends with xml'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + headers { + header "Content-Type", "customtype/xml" + } + body """${value(client('Jozo'), server('Denis'))}${ + value(client(""), server('1234567890')) + }""" + } + response { + status 200 + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "url": "/users", + "headers": { + "Content-Type": { + "equalTo": "customtype/xml" + } + }, + "bodyPatterns": [ + { + "equalToXml":"Jozo<test>" + } + ] + }, + "response": { + "status": 200 + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should use equalToXml when content type is parsable xml'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + body """${value(client('Jozo'), server('Denis'))}${ + value(client(""), server('1234567890')) + }""" + } + response { + status 200 + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "url": "/users", + "bodyPatterns": [ + { + "equalToXml":"Jozo<test>" + } + ] + }, + "response": { + "status": 200 + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should support xml as a response body'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + } + response { + status 200 + body """${value(client('Jozo'), server('Denis'))}${ + value(client(""), server('1234567890')) + }""" + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "url": "/users" + }, + "response": { + "status": 200, + "body":"Jozo<test>" + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should use equalToJson'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + body equalToJson('''{"name":"Jan"}''') + } + response { + status 200 + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "url": "/users", + "bodyPatterns": [ + { + "equalToJson":"{\\"name\\":\\"Jan\\"}" + } + ] + }, + "response": { + "status": 200 + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should use equalToXml'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url "/users" + body equalToXml("""${value(client('Jozo'), server('Denis'))}${ + value(client(""), server('1234567890')) + }""") + } + response { + status 200 + } + } + when: + String json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "url": "/users", + "bodyPatterns": [ + { + "equalToXml":"Jozo<test>" + } + ] + }, + "response": { + "status": 200 + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should convert groovy dsl stub with regexp Body as String to wireMock stub for the client side'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(regex('/[0-9]{2}')), server('/12')) + body """ + { + "personalId": "${value(client(regex('^[0-9]{11}$')), server('57593728525'))}" + } + """ + } + response { + status 200 + body("""\ + { + "name": "Jan" + } + """ + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.personalId =~ /^[0-9]{11}$/)]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"name\\":\\"Jan\\"}", + "headers" : { + "Content-Type" : "text/plain" + } + } +} +'''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should convert groovy dsl stub with a regexp and an integer in request body'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "url" : "/fraudcheck", + "method" : "PUT", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.loanAmount == 123.123)]" + }, { + "matchesJsonPath" : "$[?(@.clientPesel =~ /[0-9]{10}/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/vnd.fraud.v1+json" + } + } + }, + "response" : { + "status" : 200, + "body" : "{\\"fraudCheckStatus\\":\\"OK\\",\\"rejectionReason\\":null}", + "headers" : { + "Content-Type" : "application/vnd.fraud.v1+json" + } + } +} +'''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def "should generate request with urlPath and queryParameters for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath($(client("users"), server("items"))) { + queryParameters { + parameter 'limit': $(client(equalTo("20")), server("10")) + parameter 'offset': $(client(containing("10")), server("10")) + parameter 'filter': "email" + parameter 'sort': $(client(~/^[0-9]{10}$/), server("1234567890")) + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("10")) + parameter 'age': $(client(notMatching("^\\w*\$")), server(10)) + parameter 'name': $(client(matching("Denis.*")), server("Denis")) + parameter 'credit': absent() + } + } + } + response { + status 200 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "urlPath": "users", + "queryParameters": { + "offset": { + "contains": "10" + }, + "limit": { + "equalTo": "20" + }, + "filter": { + "equalTo": "email" + }, + "sort": { + "matches": "^[0-9]{10}$" + }, + "search": { + "doesNotMatch": "^/[0-9]{2}$" + }, + "age": { + "doesNotMatch": "^\\\\w*$" + }, + "name": { + "matches": "Denis.*" + }, + "credit": { + "absent": true + } + } + }, + "response": { + "status": 200, + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + @Issue('#230') + def "should generate request with urlPathPattern and queryParameters for client side\ + when both contains regular expressions"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath($(client(regex("/users/[0-9]+")), server("/users/1"))) { + queryParameters { + parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("10")) + } + } + } + response { + status 200 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "urlPathPattern": "/users/[0-9]+", + "queryParameters": { + "search": { + "doesNotMatch": "^/[0-9]{2}$" + } + } + }, + "response": { + "status": 200, + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + + def "should generate request with urlPath for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath $(client("boxes"), server("items")) + } + response { + status 200 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "urlPath": "boxes" + }, + "response": { + "status": 200, + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def "should generate simple request with urlPath for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath "boxes" + } + response { + status 200 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "urlPath": "boxes" + }, + "response": { + "status": 200, + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def "should not allow regexp in url for server value"() { + when: + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url(regex(/users\/[0-9]*/)) { + queryParameters { + parameter 'age': notMatching("^\\w*\$") + parameter 'name': matching("Denis.*") + } + } + } + response { + status 200 + } + } + then: + def e = thrown(IllegalStateException) + e.message.contains "Url can't be a pattern for the server side" + } + + def "should not allow regexp in query parameter for server value"() { + when: + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url("abc") { + queryParameters { + parameter 'age': $(client(notMatching("^\\w*\$")), server(regex(".*"))) + } + } + } + response { + status 200 + } + } + then: + def e = thrown(IllegalStateException) + e.message.contains "Query parameter 'age' can't be a pattern for the server side" + } + + def "should not allow query parameter unresolvable for a server value"() { + when: + org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath("users") { + queryParameters { + parameter 'age': notMatching("^\\w*\$") + parameter 'name': matching("Denis.*") + } + } + } + response { + status 200 + } + } + then: + def e = thrown(IllegalStateException) + e.message.contains "Query parameter 'age' can't be of a matching type: NOT_MATCHING for the server side" + } + + def "should not allow query parameter with a different absent variation for server/client"() { + when: + org.springframework.cloud.contract.spec.Contract.make dsl + then: + def e = thrown(IllegalStateException) + e.message.contains "Absent cannot only be used only on one side" + where: + dsl << [ + { + request { + method 'GET' + urlPath("users") { + queryParameters { + parameter 'name': $(client(absent()), server("")) + } + } + } + response { + status 200 + } + }, + { + request { + method 'GET' + urlPath("users") { + queryParameters { + parameter 'name': $(client(""), server(absent())) + } + } + } + response { + status 200 + } + }, + { + request { + method 'GET' + urlPath("users") { + queryParameters { + parameter 'name': $(client(absent()), server(matching("abc"))) + } + } + } + response { + status 200 + } + } + ] + } + + def "should generate request with url and queryParameters for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url($(client(regex(/users\/[0-9]*/)), server("users/123"))) { + queryParameters { + parameter 'age': $(client(notMatching("^\\w*\$")), server(10)) + parameter 'name': $(client(matching("Denis.*")), server("Denis")) + } + } + } + response { + status 200 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "GET", + "urlPattern": "users/[0-9]*", + "queryParameters": { + "age": { + "doesNotMatch": "^\\\\w*$" + }, + "name": { + "matches": "Denis.*" + } + } + }, + "response": { + "status": 200, + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + def 'should convert groovy dsl stub with rich tree Body as String to wireMock stub for the client side'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(client(~/\/[0-9]{2}/), server('/12')) + body """\ + { + "personalId": "${value(client(regex('[0-9]{11}')), server('57593728525'))}", + "firstName": "${value(client(regex('.*')), server('Bruce'))}", + "lastName": "${value(client(regex('.*')), server('Lee'))}", + "birthDate": "${value(client(regex('[0-9]{4}-[0-9]{2}-[0-9]{2}')), server('1985-12-12'))}", + "errors": [ + { + "propertyName": "${value(client(regex('[0-9]{2}')), server('04'))}", + "providerValue": "Test" + }, + { + "propertyName": "${value(client(regex('[0-9]{2}')), server('08'))}", + "providerValue": "Test" + } + ] + } + """ + } + response { + status 200 + body("""\ + { + "name": "Jan" + } + """ + ) + headers { + header 'Content-Type': 'text/plain' + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "urlPattern" : "/[0-9]{2}", + "method" : "GET", + "bodyPatterns" : [ { + "matchesJsonPath" : "$.errors[*][?(@.propertyName =~ /[0-9]{2}/)]" + }, { + "matchesJsonPath" : "$.errors[*][?(@.providerValue == 'Test')]" + }, { + "matchesJsonPath" : "$[?(@.lastName =~ /.*/)]" + }, { + "matchesJsonPath" : "$[?(@.firstName =~ /.*/)]" + }, { + "matchesJsonPath" : "$[?(@.birthDate =~ /[0-9]{4}-[0-9]{2}-[0-9]{2}/)]" + }, { + "matchesJsonPath" : "$[?(@.personalId =~ /[0-9]{11}/)]" + }] + }, + "response" : { + "status" : 200, + "body" : "{\\"name\\":\\"Jan\\"}", + "headers" : { + "Content-Type" : "text/plain" + } + } +} + '''), wireMockStub) + } + + def 'should use regexp matches when request body match is defined using a map with a pattern'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url '/reissue-payment-order' + body( + loanNumber: "999997001", + amount: value(client(regex('[0-9.]+')), server('100.00')), + currency: "DKK", + applicationName: value(client(regex('.*')), server("Auto-Repayments")), + username: value(client(regex('.*')), server("scheduler")), + cardId: 1 + ) + } + response { + status 200 + body ''' + { + "status": "OK" + } + ''' + headers { + header 'Content-Type': 'application/json' + } + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "url" : "/reissue-payment-order", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.loanNumber == '999997001')]" + }, { + "matchesJsonPath" : "$[?(@.username =~ /.*/)]" + }, { + "matchesJsonPath" : "$[?(@.amount =~ /[0-9.]+/)]" + }, { + "matchesJsonPath" : "$[?(@.cardId == 1)]" + }, { + "matchesJsonPath" : "$[?(@.currency == 'DKK')]" + }, { + "matchesJsonPath" : "$[?(@.applicationName =~ /.*/)]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"status\\":\\"OK\\"}", + "headers" : { + "Content-Type" : "application/json" + } + } +} + '''), json) + } + + def "should generate stub for empty body"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('POST') + url("test") + body("") + } + response { + status 406 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "POST", + "url": "test", + "bodyPatterns": [ + { + "equalTo": "" + } + ] + }, + "response": { + "status": 406 + } + } +'''), json) + } + + def "should generate stub with priority"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + priority 9 + request { + method('POST') + url("test") + } + response { + status 406 + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "priority": 9, + "request": { + "method": "POST", + "url": "test" + }, + "response": { + "status": 406 + } + } + '''), json) + } + + @Issue("#127") + def 'should use "test" as an alias for "server"'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('POST') + body( + property: value(stub("value"), test("value")) + ) + } + response { + status 200 + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' +{ + "request" : { + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.property == 'value')]" + } ] + }, + "response" : { + "status" : 200 + } +} + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + @Issue("#121") + def 'should generate stub with empty list as a value of a field'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method('POST') + body( + values: [] + ) + } + response { + status 200 + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request": { + "method": "POST", + "bodyPatterns": [ + { + "equalToJson": "{\\"values\\":[]}" + } + ] + }, + "response": { + "status": 200 + } + } + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should generate stub properly resolving GString with regular expression'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(client(regex(email())), server('not.existing@user.com')), + callback_url: $(client(regex(hostname())), server('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: 4, + message: "User not found by email = [${value(server(regex(email())), client('not.existing@user.com'))}]" + ) + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "url" : "/users/password", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]" + }, { + "matchesJsonPath" : "$[?(@.email =~ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4}/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 404, + "body" : "{\\"code\\":4,\\"message\\":\\"User not found by email = [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } + }, + "priority" : 1 + } + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + def 'should generate stub properly resolving GString with regular expression in url'() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + + request { + method 'PUT' + url "/partners/${value(client(regex('^[0-9]*$')), server('11'))}/agents/11/customers/09665703Z" + headers { + header 'Content-Type': 'application/json' + } + body( + first_name: 'Josef', + ) + } + response { + status 422 + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "urlPattern" : "/partners/^[0-9]*$/agents/11/customers/09665703Z", + "method" : "PUT", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.first_name == 'Josef')]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 422 + } + } + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + @Issue('42') + def 'should generate stub without optional parameters'() { + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), contractDsl).toWireMockClientStub() + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "url" : "/users/password", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]" + }, { + "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 404, + "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email = [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } + }, + "priority" : 1 + } + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + where: + contractDsl << [ + org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + email: $(stub(optional(regex(email()))), test('abc@abc.com')), + callback_url: $(stub(regex(hostname())), test('http://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: $(stub("123123"), test(optional("123123"))), + message: "User not found by email = [${value(test(regex(email())), stub('not.existing@user.com'))}]" + ) + } + }, + org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + header 'Content-Type': 'application/json' + } + body( + """ { + "email" : "${value(stub(optional(regex(email()))), test('abc@abc.com'))}", + "callback_url" : "${value(client(regex(hostname())), server('http://partners.com'))}" + } + """ + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + """ { + "code" : "${value(stub(123123), test(optional(123123)))}", + "message" : "User not found by email = [${value(server(regex(email())), client('not.existing@user.com'))}]" + } + """ + ) + } + } + ] + } + + String toJsonString(value) { + new JsonBuilder(value).toPrettyString() + } + + Object parseJson(json) { + new JsonSlurper().parseText(json) + } + + String toWireMockClientJsonStub(groovyDsl) { + new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), groovyDsl).toWireMockClientStub() + } + + @Issue('180') + def 'should generate stub with multipart parameters'() { + given: + org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method "PUT" + url "/multipart" + multipart( + formParameter: value(client(regex('".+"')), server('"formParameterValue"')), + someBooleanParameter: value(client(regex('(true|false)')), server('true')), + file: named( + name: value(client(regex('.+')), server('filename.csv')), + content: value(client(regex('.+')), server('file content'))) + ) + } + response { + status 200 + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), contractDsl).toWireMockClientStub() + then: + println wireMockStub + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "url" : "/multipart", + "method" : "PUT", + "bodyPatterns" : [ { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*" + }, { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*" + }, { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\".+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n.+\\r\\n--\\\\1.*" + } ] + }, + "response" : { + "status" : 200 + } + } + '''), wireMockStub) + and: + stubMappingIsValidWireMockStub(wireMockStub) + } + + @Issue('#219') + def "should generate request with an optional queryParameter for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath ('/some/api') { + queryParameters { + parameter 'size': value( + client(regex('[0-9]+')), + server(1) + ) + parameter 'page': value( + client(regex('[0-9]+')), + server(0) + ) + parameter sort: value( + client(optional(regex('^[a-z]+$'))), + server('id') + ) + } + } + } + response { + status 200 + body( + content: [[ + id : '00000000-0000-0000-0000-000000000000', + type : 'Extraordinary', + state : 'ACTIVE', + ]], + totalPages: 1, + totalElements: 1, + last: true, + sort: [[ + direction: 'ASC', + property: 'id', + ignoreCase: false, + nullHandling: 'NATIVE', + ascending: true + ]], + first: true, + numberOfElements: 1, + size: 1, + number: 0 + ) + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "urlPath" : "/some/api", + "method" : "GET", + "queryParameters" : { + "size" : { + "matches" : "[0-9]+" + }, + "page" : { + "matches" : "[0-9]+" + }, + "sort" : { + "matches" : "(^[a-z]+$)?" + } + } + }, + "response" : { + "status" : 200, + "body" : "{\\"content\\":[{\\"id\\":\\"00000000-0000-0000-0000-000000000000\\",\\"type\\":\\"Extraordinary\\",\\"state\\":\\"ACTIVE\\"}],\\"totalPages\\":1,\\"totalElements\\":1,\\"last\\":true,\\"sort\\":[{\\"direction\\":\\"ASC\\",\\"property\\":\\"id\\",\\"ignoreCase\\":false,\\"nullHandling\\":\\"NATIVE\\",\\"ascending\\":true}],\\"first\\":true,\\"numberOfElements\\":1,\\"size\\":1,\\"number\\":0}" + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy new file mode 100644 index 0000000000..825b913136 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl + +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +import java.util.regex.Pattern + +trait WireMockStubVerifier { + + void stubMappingIsValidWireMockStub(String mappingDefinition) { + StubMapping stubMapping = StubMapping.buildFrom(mappingDefinition) + stubMapping.request.bodyPatterns.findAll { it.matches }.every { + Pattern.compile(it.matches) + } + assert !mappingDefinition.contains('org.springframework.cloud.contract.spec.internal') + } + + void stubMappingIsValidWireMockStub(org.springframework.cloud.contract.spec.Contract contractDsl) { + stubMappingIsValidWireMockStub(new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null), contractDsl).toWireMockClientStub()) + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/ExecutionPropertySpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/ExecutionPropertySpec.groovy new file mode 100644 index 0000000000..65b3997d99 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/ExecutionPropertySpec.groovy @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import spock.lang.Specification + +class ExecutionPropertySpec extends Specification { + + def 'should insert passed value in place of $it placeholder'() { + given: + String commandToExecute = 'commandToExecute($it)' + ExecutionProperty executionProperty = new ExecutionProperty(commandToExecute) + and: + String valueToInsert = 'someObject.itsValue' + when: + String commandWithInsertedValue = executionProperty.insertValue(valueToInsert) + then: + 'commandToExecute(someObject.itsValue)' == commandWithInsertedValue + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/RegexPatternsSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/RegexPatternsSpec.groovy new file mode 100644 index 0000000000..80b9333bce --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/internal/RegexPatternsSpec.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import spock.lang.Specification + +import java.util.regex.Pattern + +class RegexPatternsSpec extends Specification { + + RegexPatterns regexPatterns = new RegexPatterns() + + def "should generate a regex for ip address [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.ipAddress()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + '123.123.123.123' || true + 'a.b.' || false + } + + def "should generate a regex for hostname [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.hostname()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'https://asd.com' || true + 'https://asd.com:8080' || true + 'https://localhost' || true + 'https://localhost:8080' || true + 'https://asd.com/asd' || false + 'asd.com' || false + } + + def "should generate a regex for email [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.email()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'asd@asd.com' || true + 'a.b.' || false + } + + def "should generate a regex for url [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.url()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + 'ftp://asd.com:9090/asd/a?a=b' || true + 'a.b.' || false + } + + def "should generate a regex for a number [#textToMatch] that is a match [#shouldMatch]"() { + expect: + shouldMatch == Pattern.compile(regexPatterns.number()).matcher(textToMatch).matches() + where: + textToMatch || shouldMatch + '1' || true + '1.0' || true + '0.1' || true + '.1' || true + '1.' || false + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy new file mode 100644 index 0000000000..73e7fc85c0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.file + +import com.google.common.collect.ListMultimap +import spock.lang.Specification + +import java.nio.file.Path + +/** + * @author Jakub Kubrynski, codearte.io + */ +class ContractFileScannerSpec extends Specification { + + def "should find contract files"() { + given: + File baseDir = new File(this.getClass().getResource("/directory/with/stubs").toURI()) + Set excluded = ["package/**"] as Set + Set ignored = ["other/different/**"] as Set + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored) + when: + ListMultimap result = scanner.findContracts() + then: + result.keySet().size() == 3 + result.get(baseDir.toPath().resolve("different")).size() == 1 + result.get(baseDir.toPath().resolve("other")).size() == 2 + and: + Collection ignoredSet = result.get(baseDir.toPath().resolve("other").resolve("different")) + ignoredSet.size() == 1 + ignoredSet.ignored == [true] + } + + def "should find contract files in strange directories"() { + given: + File baseDir = new File(this.getClass().getResource("/strange_[3.3.3]_directory").toURI()) + Set excluded = ["foo/**"] as Set + Set ignored = ["bar/**"] as Set + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored) + when: + ListMultimap result = scanner.findContracts() + then: + result.entries().size() == 2 + and: + Collection ignoredSet = result.get(baseDir.toPath().resolve("bar")) + ignoredSet.size() == 1 + ignoredSet.ignored == [true] + } + + def "should find contracts group in scenario"() { + given: + File baseDir = new File(this.getClass().getResource("/directory/with/scenario").toURI()) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set) + when: + ListMultimap contracts = scanner.findContracts() + then: + contracts.values().size() == 3 + contracts.values().find { it.path.fileName.toString().startsWith('01') }.groupSize == 3 + contracts.values().find { it.path.fileName.toString().startsWith('01') }.order == 0 + contracts.values().find { it.path.fileName.toString().startsWith('02') }.order == 1 + contracts.values().find { it.path.fileName.toString().startsWith('03') }.order == 2 + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy new file mode 100644 index 0000000000..7bbf672dca --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy @@ -0,0 +1,611 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import com.jayway.jsonpath.Configuration +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.jayway.jsonpath.Option +import com.toomuchcoding.jsonassert.JsonAssertion +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import net.minidev.json.JSONArray +import spock.lang.Specification + +import java.util.regex.Pattern + +class JsonToJsonPathsConverterSpec extends Specification { + + def 'should convert a json with list as root to a map of path to value'() { + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.array().field("some").field("nested").field("json").isEqualTo("with value")""" && + it.jsonPath() == '''$[*].some.nested[?(@.json == 'with value')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").field("anothervalue").isEqualTo(4)""" && + it.jsonPath() == '''$[*].some.nested[?(@.anothervalue == 4)]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").contains("name").isEqualTo("name1")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*][?(@.name == 'name1')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").contains("name").isEqualTo("name2")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*][?(@.name == 'name2')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").field("anothernested").field("name").isEqualTo("name3")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*].anothernested[?(@.name == 'name3')]''' + } + and: + assertThatJsonPathsInMapAreValid(json, pathAndValues) + where: + json << [ + ''' + [ { + "some" : { + "nested" : { + "json" : "with value", + "anothervalue": 4, + "withlist" : [ + { "name" :"name1"} , {"name": "name2"}, {"anothernested": { "name": "name3"} } + ] + } + } + }, + { + "someother" : { + "nested" : { + "json" : "with value", + "anothervalue": 4, + "withlist" : [ + { "name" :"name1"} , {"name": "name2"} + ] + } + } + } + ] + ''', + ''' + [{ + "someother" : { + "nested" : { + "json" : "with value", + "anothervalue": 4, + "withlist" : [ + { "name" :"name1"} , {"name": "name2"} + ] + } + } + }, + { + "some" : { + "nested" : { + "json" : "with value", + "anothervalue": 4, + "withlist" : [ + {"name": "name2"}, {"anothernested": { "name": "name3"} }, { "name" :"name1"} + ] + } + } + } + ]'''] + } + + def 'should convert a json with a map as root to a map of path to value'() { + given: + String json = ''' + { + "some" : { + "nested" : { + "json" : "with value", + "anothervalue": 4, + "withlist" : [ + { "name" :"name1"} , {"name": "name2"} + ] + } + } + } +''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.field("some").field("nested").field("json").isEqualTo("with value")""" && + it.jsonPath() == '''$.some.nested[?(@.json == 'with value')]''' + } + pathAndValues.find { + it.method() == """.field("some").field("nested").field("anothervalue").isEqualTo(4)""" && + it.jsonPath() == '''$.some.nested[?(@.anothervalue == 4)]''' + } + pathAndValues.find { + it.method() == """.field("some").field("nested").array("withlist").contains("name").isEqualTo("name1")""" && + it.jsonPath() == '''$.some.nested.withlist[*][?(@.name == 'name1')]''' + } + pathAndValues.find { + it.method() == """.field("some").field("nested").array("withlist").contains("name").isEqualTo("name2")""" && + it.jsonPath() == '''$.some.nested.withlist[*][?(@.name == 'name2')]''' + } + and: + assertThatJsonPathsInMapAreValid(json, pathAndValues) + } + + def 'should convert a json with a list'() { + given: + String json = ''' + { + "items" : ["HOP"] + } +''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.array("items").arrayField().isEqualTo("HOP").value()""" && + it.jsonPath() == '''$.items[?(@ == 'HOP')]''' + } + and: + assertThatJsonPathsInMapAreValid(json, pathAndValues) + } + + def 'should convert a json with null and boolean values'() { + given: + String json = ''' + { + "property1" : null, + "property2" : true + } +''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.field("property1").isNull()""" && + it.jsonPath() == '''$[?(@.property1 == null)]''' + } + pathAndValues.find { + it.method() == """.field("property2").isEqualTo(true)""" && + it.jsonPath() == '''$[?(@.property2 == true)]''' + } + } + + def "should convert numbers map"() { + given: + String json = ''' { + "extensions": {"7":28.00,"14":41.00,"30":60.00} + } + ''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.field("extensions").field("7").isEqualTo(28)""" && + it.jsonPath() == '''$.extensions[?(@.7 == 28)]''' + } + pathAndValues.find { + it.method() == """.field("extensions").field("14").isEqualTo(41)""" && + it.jsonPath() == '''$.extensions[?(@.14 == 41)]''' + } + pathAndValues.find { + it.method() == """.field("extensions").field("30").isEqualTo(60)""" && + it.jsonPath() == '''$.extensions[?(@.30 == 60)]''' + } + and: + assertThatJsonPathsInMapAreValid(json, pathAndValues) + } + + def 'should convert a json with a list of errors'() { + given: + String json = ''' + { + "errors" : [ + { "property" : "email", "message" : "inconsistent value" }, + { "property" : "email", "message" : "inconsistent value2" } + ] + } +''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method() == """.array("errors").contains("property").isEqualTo("email")""" && + it.jsonPath() == '''$.errors[*][?(@.property == 'email')]''' + } + pathAndValues.find { + it.method() == """.array("errors").contains("message").isEqualTo("inconsistent value")""" && + it.jsonPath() == '''$.errors[*][?(@.message == 'inconsistent value')]''' + } + pathAndValues.find { + it.method() == """.array("errors").contains("message").isEqualTo("inconsistent value2")""" && + it.jsonPath() == '''$.errors[*][?(@.message == 'inconsistent value2')]''' + } + and: + assertThatJsonPathsInMapAreValid(json, pathAndValues) + } + + def 'should convert a map json with a regex pattern'() { + given: + List json = [ + [some: + [nested: [ + json: "with value", + anothervalue: 4, + withlist: + [ + [name: "name2"], + [name: "name1"], + [anothernested: + [name: Pattern.compile("[a-zA-Z]+")] + ], + [age: "123456789"] + ] + ] + ] + ], + [someother: + [nested: [ + json: "with value", + anothervalue: 4, + withlist: + [ + [name: "name2"], + [name: "name1"] + ] + ] + ] + ] + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method() == """.array().field("some").field("nested").field("json").isEqualTo("with value")""" && + it.jsonPath() == '''$[*].some.nested[?(@.json == 'with value')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").field("anothervalue").isEqualTo(4)""" && + it.jsonPath() == '''$[*].some.nested[?(@.anothervalue == 4)]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").contains("name").isEqualTo("name1")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*][?(@.name == 'name1')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").contains("name").isEqualTo("name2")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*][?(@.name == 'name2')]''' + } + pathAndValues.find { + it.method() == """.array().field("some").field("nested").array("withlist").field("anothernested").field("name").matches("[a-zA-Z]+")""" && + it.jsonPath() == '''$[*].some.nested.withlist[*].anothernested[?(@.name =~ /[a-zA-Z]+/)]''' + } + when: + json.some.nested.withlist[0][2].anothernested.name = "Kowalski" + then: + assertThatJsonPathsInMapAreValid(JsonOutput.prettyPrint(JsonOutput.toJson(json)), pathAndValues) + } + + + def "should generate assertions for simple response body"() { + given: + String json = """{ + "property1": "a", + "property2": "b" + }""" + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.field("property1").isEqualTo("a")""" && + it.jsonPath() == """\$[?(@.property1 == 'a')]""" + } + pathAndValues.find { + it.method()== """.field("property2").isEqualTo("b")""" && + it.jsonPath() == """\$[?(@.property2 == 'b')]""" + } + and: + pathAndValues.size() == 2 + } + + def "should generate assertions for null and boolean values"() { + given: + String json = """{ + "property1": "true", + "property2": null, + "property3": false + }""" + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.field("property1").isEqualTo("true")""" && + it.jsonPath() == """\$[?(@.property1 == 'true')]""" + } + pathAndValues.find { + it.method()== """.field("property2").isNull()""" && + it.jsonPath() == """\$[?(@.property2 == null)]""" + } + pathAndValues.find { + it.method()== """.field("property3").isEqualTo(false)""" && + it.jsonPath() == """\$[?(@.property3 == false)]""" + } + and: + pathAndValues.size() == 3 + } + + def "should generate assertions for simple response body constructed from map with a list"() { + given: + Map json = [ + property1: 'a', + property2: [ + [a: 'sth'], + [b: 'sthElse'] + ] + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method()== """.field("property1").isEqualTo("a")""" && + it.jsonPath() == """\$[?(@.property1 == 'a')]""" + } + pathAndValues.find { + it.method()== """.array("property2").contains("a").isEqualTo("sth")""" && + it.jsonPath() == """\$.property2[*][?(@.a == 'sth')]""" + } + pathAndValues.find { + it.method()== """.array("property2").hasSize(2)""" && + it.jsonPath() == """\$.property2[*]""" + } + pathAndValues.find { + it.method()== """.array("property2").contains("b").isEqualTo("sthElse")""" && + it.jsonPath() == """\$.property2[*][?(@.b == 'sthElse')]""" + } + and: + pathAndValues.size() == 4 + } + + def "should generate assertions for a response body containing map with integers as keys"() { + given: + Map json = [ + property: [ + 14: 0.0, + 7 : 0.0 + ] + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method()== """.field("property").field(7).isEqualTo(0.0)""" && + it.jsonPath() == """\$.property[?(@.7 == 0.0)]""" + } + pathAndValues.find { + it.method()== """.field("property").field(14).isEqualTo(0.0)""" && + it.jsonPath() == """\$.property[?(@.14 == 0.0)]""" + } + and: + pathAndValues.size() == 2 + } + + def "should generate assertions for array in response body"() { + given: + String json = """[ + { + "property1": "a" + }, + { + "property2": "b" + }]""" + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.array().contains("property1").isEqualTo("a")""" && + it.jsonPath() == """\$[*][?(@.property1 == 'a')]""" + } + pathAndValues.find { + it.method()== """.array().contains("property2").isEqualTo("b")""" && + it.jsonPath() == """\$[*][?(@.property2 == 'b')]""" + } + pathAndValues.find { + it.method()== """.hasSize(2)""" && + it.jsonPath() == """\$""" + } + and: + pathAndValues.size() == 3 + } + + def "should generate assertions for array inside response body element"() { + given: + String json = """{ + "property1": [ + { "property2": "test1"}, + { "property3": "test2"} + ] +}""" + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.array("property1").contains("property2").isEqualTo("test1")""" && + it.jsonPath() == """\$.property1[*][?(@.property2 == 'test1')]""" + } + pathAndValues.find { + it.method()== """.array("property1").contains("property3").isEqualTo("test2")""" && + it.jsonPath() == """\$.property1[*][?(@.property3 == 'test2')]""" + } + pathAndValues.find { + it.method()== """.array("property1").hasSize(2)""" && + it.jsonPath() == """\$.property1[*]""" + } + and: + pathAndValues.size() == 3 + } + + def "should generate assertions for nested objects in response body"() { + given: + String json = """{ + "property1": "a", + "property2": {"property3": "b"} + }""" + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.field("property2").field("property3").isEqualTo("b")""" && + it.jsonPath() == """\$.property2[?(@.property3 == 'b')]""" + } + pathAndValues.find { + it.method()== """.field("property1").isEqualTo("a")""" && + it.jsonPath() == """\$[?(@.property1 == 'a')]""" + } + and: + pathAndValues.size() == 2 + } + + def "should generate regex assertions for map objects in response body"() { + given: + Map json = [ + property1: "a", + property2: Pattern.compile('[0-9]{3}') + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method()== """.field("property2").matches("[0-9]{3}")""" && + it.jsonPath() == """\$[?(@.property2 =~ /[0-9]{3}/)]""" + } + pathAndValues.find { + it.method()== """.field("property1").isEqualTo("a")""" && + it.jsonPath() == """\$[?(@.property1 == 'a')]""" + } + and: + pathAndValues.size() == 2 + } + + def "should generate escaped regex assertions for string objects in response body"() { + given: + Map json = [ + property2: Pattern.compile('\\d+') + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method()== """.field("property2").matches("\\\\d+")""" && + it.jsonPath() == """\$[?(@.property2 =~ /\\d+/)]""" + } + and: + pathAndValues.size() == 1 + } + + + def "should work with more complex stuff and jsonpaths"() { + given: + Map json = [ + errors: [ + [property: "bank_account_number", + message: "incorrect_format"] + ] + ] + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(json) + then: + pathAndValues.find { + it.method()== """.array("errors").contains("property").isEqualTo("bank_account_number")""" && + it.jsonPath() == """\$.errors[*][?(@.property == 'bank_account_number')]""" + } + pathAndValues.find { + it.method()== """.array("errors").contains("message").isEqualTo("incorrect_format")""" && + it.jsonPath() == """\$.errors[*][?(@.message == 'incorrect_format')]""" + } + pathAndValues.find { + it.method()== """.array("errors").hasSize(1)""" && + it.jsonPath() == """\$.errors[*]""" + } + and: + pathAndValues.size() == 3 + } + + def "should manage to parse a double array"() { + given: + String json = ''' + [{ + "place": + { + "bounding_box": + { + "coordinates": + [[ + [-77.119759,38.995548], + [-76.909393,38.791645] + ]] + } + } + }] + ''' + when: + JsonPaths pathAndValues = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(new JsonSlurper().parseText(json)) + then: + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").array().arrayField().isEqualTo(38.995548)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*][*][?(@ == 38.995548)]""" + } + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").array().arrayField().isEqualTo(-77.119759)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*][*][?(@ == -77.119759)]""" + } + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").array().arrayField().isEqualTo(-76.909393)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*][*][?(@ == -76.909393)]""" + } + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").array().arrayField().isEqualTo(38.791645)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*][*][?(@ == 38.791645)]""" + } + pathAndValues.find { + it.method()== """.hasSize(1)""" && + it.jsonPath() == """\$""" + } + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").array().hasSize(2)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*][*]""" + } + pathAndValues.find { + it.method()== """.array().field("place").field("bounding_box").array("coordinates").hasSize(1)""" && + it.jsonPath() == """\$[*].place.bounding_box.coordinates[*]""" + } + and: + pathAndValues.size() == 7 + and: + pathAndValues.each { + JsonAssertion.assertThat(json).matchesJsonPath(it.jsonPath()) + } + } + + private void assertThatJsonPathsInMapAreValid(String json, JsonPaths pathAndValues) { + DocumentContext parsedJson = JsonPath.using(Configuration.builder().options(Option.ALWAYS_RETURN_LIST).build()).parse(json); + pathAndValues.each { + assert !parsedJson.read(it.jsonPath(), JSONArray).empty + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/01_login.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/01_login.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/01_login.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/02_showCart.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/02_showCart.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/02_showCart.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/03_logout.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/03_logout.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/scenario/03_logout.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/different/diff.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/different/diff.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/different/diff.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/different/diff.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/different/diff.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/different/diff.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/other.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/other.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/other.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/sample.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/sample.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/other/sample.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/exceptions/test.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/exceptions/test.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/exceptions/test.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v1/exceptions/testv1.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v1/exceptions/testv1.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v1/exceptions/testv1.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v2/exceptions/testv2.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v2/exceptions/testv2.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/package/v2/exceptions/testv2.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/stubsRepositoryIndicator b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/directory/with/stubs/stubsRepositoryIndicator new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/dsl/basic/sampleDsl.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/dsl/basic/sampleDsl.groovy new file mode 100644 index 0000000000..a24fc9fb0a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/dsl/basic/sampleDsl.groovy @@ -0,0 +1,48 @@ +import org.springframework.cloud.contract.spec.Contract + +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Contract.make { + request { + method('PUT') + headers { + header 'Content-Type': 'application/json' + } + body("""\ + { + "name": "Jan", + "id": "${value(client('abc'), server('def'))}", + } + """ + ) + url $(client('/[0-9]{2}'), server('/12')) + } + response { + status 200 + body("""\ + { + "name": "Jan", + "id": "${value(client('123'), server('321'))}", + "surname": "${value(client('Kowalsky'), server('$checkIfSurnameValid($value)'))}" + } + """ + ) + headers { + header 'Content-Type': 'text/plain' + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/main.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/main.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/02_login.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/02_login.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/02_login.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/bar/03_login.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/bar/03_login.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/bar/03_login.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/foo/01_login.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/foo/01_login.groovy new file mode 100644 index 0000000000..958ccd0673 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-core/src/test/resources/strange_[3.3.3]_directory/foo/01_login.groovy @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy new file mode 100644 index 0000000000..ce5301b3fd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import org.gradle.api.GradleException +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.springframework.cloud.contract.spec.ContractVerifierException +import org.springframework.cloud.contract.verifier.TestGenerator +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties + +/** + * Task used to generate server side tests + * + * @since 1.0.0 + */ +class GenerateServerTestsTask extends ConventionTask { + + @InputDirectory + File contractsDslDir + @OutputDirectory + File generatedTestSourcesDir + + //TODO: How to deal with @Input*, @Output* and that domain object? + ContractVerifierConfigProperties configProperties + + @TaskAction + void generate() { + project.logger.info("Spring Cloud Contract Verifier Plugin: Invoking test sources generation") + + project.sourceSets.test.groovy { + project.logger.info("Registering ${getConfigProperties().generatedTestSourcesDir} as test source directory") + srcDir getConfigProperties().generatedTestSourcesDir + } + + try { + //TODO: What with that? How to pass? + TestGenerator generator = new TestGenerator(getConfigProperties()) + int generatedClasses = generator.generate() + project.logger.info("Generated {} test classes", generatedClasses) + } catch (ContractVerifierException e) { + throw new GradleException("Spring Cloud Contract Verifier Plugin exception: ${e.message}", e) + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateWireMockClientStubsFromDslTask.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateWireMockClientStubsFromDslTask.groovy new file mode 100644 index 0000000000..e87a0422cd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateWireMockClientStubsFromDslTask.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter +import org.springframework.cloud.contract.verifier.wiremock.RecursiveFilesConverter +//TODO: Implement as an incremental task: https://gradle.org/docs/current/userguide/custom_tasks.html#incremental_tasks ? +/** + * Generates WireMock stubs from the contracts + * + * @since 1.0.0 + */ +class GenerateWireMockClientStubsFromDslTask extends ConventionTask { + + private static final String DEFAULT_MAPPINGS_FOLDER = 'mappings' + + @InputDirectory + File contractsDslDir + @OutputDirectory + File stubsOutputDir + + ContractVerifierConfigProperties configProperties + + @TaskAction + void generate() { + logger.info("Spring Cloud Contract Verifier Plugin: Invoking DSL to WireMock client stubs conversion") + logger.debug("From '${getContractsDslDir()}' to '${getStubsOutputDir()}'") + ContractVerifierConfigProperties props = getConfigProperties() + File outMappingsDir = props.stubsOutputDir != null ? new File(props.stubsOutputDir, DEFAULT_MAPPINGS_FOLDER) + : new File(project.buildDir, "stubs/$DEFAULT_MAPPINGS_FOLDER") + RecursiveFilesConverter converter = new RecursiveFilesConverter(new DslToWireMockClientConverter(), + getConfigProperties(), outMappingsDir) + converter.processFiles() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy new file mode 100644 index 0000000000..2ee94f97da --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy @@ -0,0 +1,206 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin +import org.gradle.api.tasks.Copy +import org.gradle.jvm.tasks.Jar +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +/** + * Gradle plugin for Spring Cloud Contract Verifier that from the DSL contract can + *
    + *
  • generate tests
  • + *
  • generate stubs
  • + *
+ * + * Also adds the necessary {@code testCompile} dependencies + * + *
    + *
  • WireMock
  • + *
  • JSON Assert
  • + *
  • AssertJ
  • + *
+ * + * @author Jakub Kubrynski, codearte.io + * + * @since 1.0.0 + */ +class SpringCloudContractVerifierGradlePlugin implements Plugin { + + private static final String GENERATE_SERVER_TESTS_TASK_NAME = 'generateContractTests' + private static final String DSL_TO_WIREMOCK_CLIENT_TASK_NAME = 'generateWireMockClientStubs' + private static final String COPY_CONTRACTS_TASK_NAME = 'copyContracts' + private static final String VERIFIER_STUBS_JAR_TASK_NAME = 'verifierStubsJar' + + private static final Class IDEA_PLUGIN_CLASS = org.gradle.plugins.ide.idea.IdeaPlugin + private static final String GROUP_NAME = "Verification" + private static final String EXTENSION_NAME = 'contractVerifier' + + private Project project + + @Override + void apply(Project project) { + this.project = project + ContractVerifierConfigProperties extension = project.extensions.create(EXTENSION_NAME, ContractVerifierConfigProperties) + + project.check.dependsOn(GENERATE_SERVER_TESTS_TASK_NAME) + + setConfigurationDefaults(extension) + createGenerateTestsTask(extension) + createAndConfigureGenerateWireMockClientStubsFromDslTask(extension) + Task stubsJar = createAndConfigureStubsJarTasks(extension) + createAndConfigureCopyContractsTask(stubsJar, extension) + createAndConfigureMavenPublishPlugin(stubsJar, extension) + project.dependencies.add("testCompile", "com.github.tomakehurst:wiremock:2.0.10-beta") + project.dependencies.add("testCompile", "com.toomuchcoding.jsonassert:jsonassert:0.4.7") + project.dependencies.add("testCompile", "org.assertj:assertj-core:2.3.0") + + project.afterEvaluate { + def hasIdea = project.plugins.findPlugin(IDEA_PLUGIN_CLASS) + if (hasIdea) { + project.idea { + module { + testSourceDirs += extension.generatedTestSourcesDir + testSourceDirs += extension.contractsDslDir + } + } + } + } + } + + private void setConfigurationDefaults(ContractVerifierConfigProperties extension) { + extension.with { + generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts") + contractsDslDir = defaultContractsDir() //TODO: Use sourceset + basePackageForTests = 'org.springframework.cloud.contract.verifier.tests' + stubsOutputDir = stubsOutputDir ?: project.file("${project.buildDir}/stubs") + } + } + + private File defaultContractsDir() { + project.file("${project.rootDir}/src/test/resources/contracts") + } + + private void createGenerateTestsTask(ContractVerifierConfigProperties extension) { + Task task = project.tasks.create(GENERATE_SERVER_TESTS_TASK_NAME, GenerateServerTestsTask) + task.description = "Generate server tests from the contracts" + task.group = GROUP_NAME + task.conventionMapping.with { + contractsDslDir = { extension.contractsDslDir } + generatedTestSourcesDir = { extension.generatedTestSourcesDir } + configProperties = { extension } + } + } + + private void createAndConfigureGenerateWireMockClientStubsFromDslTask(ContractVerifierConfigProperties extension) { + Task task = project.tasks.create(DSL_TO_WIREMOCK_CLIENT_TASK_NAME, GenerateWireMockClientStubsFromDslTask) + task.description = "Generate WireMock client stubs from the contracts" + task.group = GROUP_NAME + task.conventionMapping.with { + contractsDslDir = { extension.contractsDslDir } + stubsOutputDir = { extension.stubsOutputDir } + configProperties = { extension } + } + } + + private Task createAndConfigureStubsJarTasks(ContractVerifierConfigProperties extension) { + Task task = stubsTask() + if (task) { + project.logger.info("Spring Cloud Contract Verifier Plugin: Stubs jar task was present - won't create one. Remember about adding it to artifacts as an archive!") + return task + } else { + task = project.tasks.create(type: Jar, name: VERIFIER_STUBS_JAR_TASK_NAME, + dependsOn: DSL_TO_WIREMOCK_CLIENT_TASK_NAME) { + baseName = project.name + classifier = extension.stubsSuffix + from { extension.stubsOutputDir ?: project.file("${project.buildDir}/stubs") } + } + task.description = "Creates the stubs JAR task" + task.group = GROUP_NAME + + project.artifacts { + archives task + } + return task + } + } + + private Task stubsTask() { + try { + return project.tasks.getByName(VERIFIER_STUBS_JAR_TASK_NAME) + } catch (Exception e) { + return null + } + } + + private Task createAndConfigureCopyContractsTask(Task stubs, ContractVerifierConfigProperties extension) { + Task task = project.tasks.create(type: Copy, name: COPY_CONTRACTS_TASK_NAME) { + from { extension.contractsDslDir } + into { extension.stubsOutputDir != null ? + project.file("${extension.stubsOutputDir}/contracts") : project.file("${project.buildDir}/stubs/contracts") } + } + task.description = "Copies contracts to the output folder" + task.group = GROUP_NAME + stubs.dependsOn task + return task + } + + private void createAndConfigureMavenPublishPlugin(Task stubsTask, ContractVerifierConfigProperties extension) { + if (!classIsOnClasspath("org.gradle.api.publish.maven.plugins.MavenPublishPlugin")) { + project.logger.debug("Maven Publish Plugin is not present - won't add default publication") + return + } + project.logger.debug("Spring Cloud Contract Verifier Plugin: Generating default publication") + project.afterEvaluate { + project.plugins.withType(MavenPublishPlugin) { def publishingPlugin -> + def publishingExtension = project.extensions.findByName('publishing') + if (!hasPublication(publishingExtension)) { + project.logger.debug("Spring Cloud Contract Verifier Plugin: Stubs publication is not present - will create one") + publishingExtension.publications { + stubs(MavenPublication) { + artifactId "${project.name}" + artifact stubsTask + } + } + } else { + project.logger.info("Spring Cloud Contract Verifier Plugin: Stubs publication was present - won't create a new one. Remember about passing stubs as artifact") + } + } + } + } + + private boolean hasPublication(def publishingExtension) { + try { + return publishingExtension.publications.getByName('stubs') + } catch (Exception e) { + return false + } + } + + private boolean classIsOnClasspath(String className) { + try { + Class.forName(className) + return true + } catch (Exception e) { + project.logger.debug("Maven Publish Plugin is not available") + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/resources/META-INF/gradle-plugins/contract-verifier.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/resources/META-INF/gradle-plugins/contract-verifier.properties new file mode 100644 index 0000000000..d5479da71a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/main/resources/META-INF/gradle-plugins/contract-verifier.properties @@ -0,0 +1,17 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.springframework.cloud.contract.verifier.plugin.SpringCloudContractVerifierGradlePlugin \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/BasicFunctionalSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/BasicFunctionalSpec.groovy new file mode 100755 index 0000000000..8634877b49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/BasicFunctionalSpec.groovy @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin +import org.springframework.cloud.contract.verifier.util.AssertionUtil +import org.gradle.testkit.runner.BuildResult +import spock.lang.Stepwise + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS +import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE + +@Stepwise +class BasicFunctionalSpec extends ContractVerifierIntegrationSpec { + + private static final String GENERATED_TEST = "build//generated-test-sources//contracts//contracts//spring//cloud//twitter_places_analyzer//PairIdSpec.groovy" + private static final String GENERATED_CLIENT_JSON_STUB = "build//production//bootSimple-stubs//repository//mappings//spring//cloud//twitter-places-analyzer//pairId//collerate_PlacesFrom_Tweet.json" + private static final String GROOVY_DSL_CONTRACT = "repository//mappings//spring//cloud//twitter-places-analyzer//pairId//collerate_PlacesFrom_Tweet.groovy" + private static final String TEST_EXECUTION_XML_REPORT = "build/test-results/TEST-contracts.spring.cloud.twitter_places_analyzer.PairIdSpec.xml" + + def setup() { + setupForProject("functionalTest/bootSimple") + runTasksSuccessfully('clean') //delete accidental output when previously importing SimpleBoot into Idea to tweak it + } + + def "should pass basic flow"() { + when: + BuildResult result = run("check", "publishToMavenLocal") + then: + result.task(":generateWireMockClientStubs").outcome == SUCCESS + result.task(":generateContractTests").outcome == SUCCESS + + and: "tests generated" + fileExists(GENERATED_TEST) + + and: "client stubs generated" + fileExists(GENERATED_CLIENT_JSON_STUB) + + and: "generated tests executed" + fileExists(TEST_EXECUTION_XML_REPORT) + } + + def "should generate valid client json stubs for simple input"() { + when: + run('generateWireMockClientStubs') + then: + def generatedClientJsonStub = file(GENERATED_CLIENT_JSON_STUB).text + AssertionUtil.assertThatJsonsAreEqual(""" + { + "request" : { + "url" : "/api/12", + "method" : "PUT", + "bodyPatterns" : [ { + "matchesJsonPath" : "\$[*][?(@.text == 'Gonna see you at Warsaw')]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 200 + }, + "priority" : 2 + } + """, generatedClientJsonStub) + } + + def "tasks should be up-to-date when appropriate"() { + given: + assert !fileExists(GENERATED_CLIENT_JSON_STUB) + assert !fileExists(TEST_EXECUTION_XML_REPORT) + when: + runTasksSuccessfully('generateWireMockClientStubs', 'generateContractTests') + then: + fileExists(GENERATED_CLIENT_JSON_STUB) + fileExists(GENERATED_TEST) + + when: "running generation without change inputs" + def secondExecutionResult = run('generateWireMockClientStubs', 'generateContractTests') + + then: "tasks should be up-to-date" + validateTasksOutcome(secondExecutionResult, UP_TO_DATE, 'generateWireMockClientStubs', 'generateContractTests') + + when: "inputs changed" + def groovyDslFile = file(GROOVY_DSL_CONTRACT) + groovyDslFile.text = groovyDslFile.text.replace("200", "599") + + and: "tasks run" + def thirdExecutionResult = run('generateWireMockClientStubs', 'generateContractTests') + + then: "tasks should be reexecuted" + validateTasksOutcome(thirdExecutionResult, SUCCESS, 'generateWireMockClientStubs', 'generateContractTests') + + and: "changes visible in generate files" + file(GENERATED_CLIENT_JSON_STUB).text.contains("599") + file(GENERATED_TEST).text.contains("599") + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy new file mode 100644 index 0000000000..6e3ced3368 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy @@ -0,0 +1,154 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import org.apache.commons.io.FileUtils +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.Path +import java.util.zip.ZipException +import java.util.zip.ZipFile + +import static java.nio.charset.StandardCharsets.UTF_8 + +abstract class ContractVerifierIntegrationSpec extends Specification { + + File testProjectDir + + def setup() { + def dateString = new Date().format("yyyy-MM-dd_HH-mm-ss") + def testFolder = new File("build/generated-tests/${getClass().simpleName}/${dateString}") + testFolder.mkdirs() + testProjectDir = testFolder + } + + public static final String SPOCK = "targetFramework = 'Spock'" + public static final String JUNIT = "targetFramework = 'JUnit'" + public static final String MVC_SPEC = "baseClassForTests = 'org.springframework.cloud.MvcSpec'" + public static final String MVC_TEST = "baseClassForTests = 'org.springframework.cloud.MvcTest'" + + protected void setupForProject(String projectRoot) { + copyResourcesToRoot(projectRoot) + String gradlePluginSysProp = System.getProperty("contract-verifier-gradle-plugin-libs-dir") + String gradlePluginLibsDir = (gradlePluginSysProp ?: new File("build/").toString()).replace('\\', '\\\\') + String messagingLibDirProp = System.getProperty("messaging-libs-dir") + String messagingLibDir = (messagingLibDirProp ?: new File("build/").toString()).replace('\\', '\\\\') + + buildFile.write """ + ext.messagingLibsDir = '$messagingLibDir' + + buildscript { + dependencies { + classpath fileTree(dir: '$gradlePluginLibsDir', include: '*.jar') + } + } + + """ + buildFile.text + // Extending buildscript is required when 'apply' is used. + // 'GradleRunner#withPluginClasspath' can be used when plugin is added using 'plugins { id...' + } + + protected void switchToJunitTestFramework() { + switchToJunitTestFramework(MVC_SPEC, MVC_TEST) + } + + protected void switchToJunitTestFramework(String from, String to) { + Path path = buildFile.toPath() + String content = new StringBuilder(new String(Files.readAllBytes(path), UTF_8)).replaceAll(SPOCK, JUNIT) + .replaceAll(from, to) + Files.write(path, content.getBytes(UTF_8)) + } + + protected void runTasksSuccessfully(String... tasks) { + BuildResult result = run(tasks) + result.tasks.each { + assert it.outcome == TaskOutcome.SUCCESS || it.outcome == TaskOutcome.UP_TO_DATE + } + } + + protected BuildResult validateTasksOutcome(BuildResult result, TaskOutcome expectedOutcome, String... tasks) { + tasks.each { + BuildTask task = result.task(":" + it) + assert task + assert task.outcome == expectedOutcome + } + return result + } + + protected BuildResult run(String... tasks) { + return GradleRunner.create() + .withProjectDir(testProjectDir) + .withArguments(tasks) + .build() + } + + protected void copyResourcesToRoot(String srcDir) { + copyResources(srcDir, testProjectDir) + } + + protected void copyResources(String srcDir, File destinationFile) { + ClassLoader classLoader = getClass().getClassLoader() + URL resource = classLoader.getResource(srcDir) + if (resource == null) { + throw new RuntimeException("Could not find classpath resource: $srcDir") + } + File resourceFile = new File(resource.toURI()) + if (resourceFile.file) { + FileUtils.copyFile(resourceFile, destinationFile) + } else { + FileUtils.copyDirectory(resourceFile, destinationFile) + } + } + + protected File file(String path) { + return new File(testProjectDir, path) + } + + protected boolean fileExists(String path) { + return file(path).exists() + } + + protected File getBuildFile() { + return new File('build.gradle', testProjectDir) + } + + protected boolean jarContainsContractVerifierContracts(String path) { + assert fileExists(path) + File rootFile = file(path) + boolean containsGroovyFiles = false + rootFile.eachFileRecurse { File file -> + try { + if (file.isFile() && file.name.endsWith('jar')) { + new ZipFile(file).entries().each { + if (it.name.endsWith('.groovy')) { + containsGroovyFiles = true + } + } + } + }catch (ZipException zipEx) { + println "Unable to open file ${file.name}" + } + } + return containsGroovyFiles + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/MessagingProjectSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/MessagingProjectSpec.groovy new file mode 100755 index 0000000000..9f63a1b29c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/MessagingProjectSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import spock.lang.Stepwise + +@Stepwise +class MessagingProjectSpec extends ContractVerifierIntegrationSpec { + + def setup() { + setupForProject("functionalTest/messagingProject") + runTasksSuccessfully('clean') //delete accidental output when previously importing SimpleBoot into Idea to tweak it + } + + def "should pass basic flow for Spock"() { + given: + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('build/libs') + } + + def "should pass basic flow for JUnit"() { + given: + runTasksSuccessfully('clean') + assert fileExists('build.gradle') + expect: + switchToJunitTestFramework('org.springframework.cloud.samples.book.MessagingBaseSpec', 'org.springframework.cloud.samples.book.MessagingBaseTest') + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('build/libs') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleJerseyProjectSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleJerseyProjectSpec.groovy new file mode 100755 index 0000000000..25b6754d60 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleJerseyProjectSpec.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import spock.lang.Stepwise + +@Stepwise +class SampleJerseyProjectSpec extends ContractVerifierIntegrationSpec { + + def setup() { + setupForProject("functionalTest/sampleJerseyProject") + runTasksSuccessfully('clean') //delete accidental output when previously importing SimpleBoot into Idea to tweak it + } + + def "should pass basic flow for Spock"() { + given: + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + + def "should pass basic flow for JUnit"() { + given: + switchToJunitTestFramework() + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy new file mode 100755 index 0000000000..5b31c3d4dd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import spock.lang.Stepwise + +@Stepwise +class SampleProjectSpec extends ContractVerifierIntegrationSpec { + + def setup() { + setupForProject("functionalTest/sampleProject") + runTasksSuccessfully('clean') //delete accidental output when previously importing SimpleBoot into Idea to tweak it + } + + def "should pass basic flow for Spock"() { + given: + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + + def "should pass basic flow for JUnit"() { + given: + switchToJunitTestFramework() + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ScenarioProjectSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ScenarioProjectSpec.groovy new file mode 100755 index 0000000000..f822982bef --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ScenarioProjectSpec.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.plugin + +import spock.lang.Stepwise + +@Stepwise +class ScenarioProjectSpec extends ContractVerifierIntegrationSpec { + + def setup() { + setupForProject("functionalTest/scenarioProject") + runTasksSuccessfully('clean') //delete accidental output when previously importing SimpleBoot into Idea to tweak it + } + + def "should pass basic flow for Spock"() { + given: + assert fileExists('build.gradle') + expect: + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + + def "should pass basic flow for JUnit"() { + given: + assert fileExists('build.gradle') + expect: + switchToJunitTestFramework() + runTasksSuccessfully('check', "publishToMavenLocal") + jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/build.gradle new file mode 100644 index 0000000000..1f0e725251 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/build.gradle @@ -0,0 +1,69 @@ +buildscript { + repositories { + mavenCentral() + } +} + +apply plugin: 'groovy' +apply plugin: 'contract-verifier' +apply plugin: 'maven-publish' + +group = 'org.springframework.cloud.testprojects' + +ext { + contractsDir = file("repository/mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/repository/") +} + +configurations { + all { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + // To prevent an accidental usage of groovy-all.jar and groovy.jar in different versions + // all modularized Groovy jars are replaced with groovy-all.jar by default. + if (details.requested.group == 'org.codehaus.groovy' && details.requested.name != "groovy-all") { + details.useTarget("org.codehaus.groovy:groovy-all:${details.requested.version}") + } + } + } + } +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + compile "org.springframework:spring-web:$springVersion" + compile "org.springframework:spring-context-support:$springVersion" + compile "org.codehaus.groovy:groovy-all:2.4.5" + compile 'com.jayway.jsonpath:json-path-assert:2.2.0' + + testCompile "com.github.tomakehurst:wiremock:2.0.10-beta" + testCompile "org.spockframework:spock-spring:1.0-groovy-2.4" + testCompile "com.jayway.restassured:rest-assured:$restAssuredVersion" + testCompile "com.jayway.restassured:spring-mock-mvc:$restAssuredVersion" + testCompile "ch.qos.logback:logback-classic:1.1.2" +} + +contractVerifier { + baseClassForTests = 'org.springframework.cloud.contract.verifier.twitter.places.BaseMockMvcSpec' + basePackageForTests = 'contracts' + contractsDslDir = contractsDir +// generatedTestSourcesDir = file("${project.rootDir}/src/test/groovy/") + stubsOutputDir = stubsOutputDirRoot + targetFramework = 'Spock' +} + +generateContractTests.dependsOn generateWireMockClientStubs + +wrapper { + gradleVersion '2.2.1' +} + +test { + testLogging { + exceptionFormat = 'full' + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties new file mode 100644 index 0000000000..7c325172fd --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +groupId=org.springframework.cloud.contract.verifier +jacksonMapper=1.9.13 +restAssuredVersion=2.4.0 +springVersion=4.1.7.RELEASE + +springBootVersion=1.3.3.RELEASE \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000000..f510690c32 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Sat Feb 21 20:13:29 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew.bat new file mode 100755 index 0000000000..8a0b282aa6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/collerate_PlacesFrom_Tweet.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/collerate_PlacesFrom_Tweet.groovy new file mode 100644 index 0000000000..ab1d853b3e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/collerate_PlacesFrom_Tweet.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + priority 2 + request { + method 'PUT' + url '/api/12' + headers { + header 'Content-Type': 'application/json' + } + body '''\ + [{ + "text": "Gonna see you at Warsaw" + }] +''' + } + response { + status 200 + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/moreComplexVersion.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/moreComplexVersion.groovy new file mode 100644 index 0000000000..887e54edf7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/repository/mappings/spring/cloud/twitter-places-analyzer/pairId/moreComplexVersion.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method 'PUT' + url $(client(regex('^/api/[0-9]{2}$')), server('/api/12')) + headers { + header 'Content-Type': 'application/json' + } + body '''\ + [{ + "text": "Gonna see you at Warsaw" + }] +''' + } + response { + headers { + header 'Content-Type': $(client('application/json'), server(regex('application/json.*'))) + header 'Location': $(client('https://localhost:8080'), server(execute('isEmpty($it)'))) + } + body ( + path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))), + correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)'))) + ) + status 200 + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/settings.gradle new file mode 100644 index 0000000000..f452413fb1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/settings.gradle @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +rootProject.name='bootSimple' diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/PairIdController.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/PairIdController.groovy new file mode 100644 index 0000000000..38b7dfc178 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/PairIdController.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.twitter.place + +import groovy.transform.TypeChecked +import groovy.util.logging.Slf4j +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import static org.springframework.web.bind.annotation.RequestMethod.PUT + +@Slf4j +@RestController +@RequestMapping('/api') +@TypeChecked +class PairIdController { + + @RequestMapping( + value = '{pairId}', + method = PUT, + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + String getPlacesFromTweets(@PathVariable long pairId, @RequestBody List tweets) { + log.info("Inside PairIdController, doing very important logic") + if (tweets?.text != ["Gonna see you at Warsaw"]) { + throw new IllegalArgumentException("Wrong text in tweet: ${tweets?.text}") + } + return """ + { + "path" : "/api/$pairId", + "correlationId" : 123456 + } + """ + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/Tweet.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/Tweet.java new file mode 100644 index 0000000000..9417289cec --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/main/groovy/org/springframework/cloud/twitter/place/Tweet.java @@ -0,0 +1,13 @@ +package org.springframework.cloud.contract.verifier.twitter.place; + +public class Tweet { + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/AcceptanceSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/AcceptanceSpec.groovy new file mode 100644 index 0000000000..9f482e0b4c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/AcceptanceSpec.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.twitter.places + +import org.springframework.cloud.contract.verifier.twitter.place.PairIdController +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Specification + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class AcceptanceSpec extends Specification { + + def "should have controller up and running"() { + given: + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new PairIdController()).build() + expect: + mockMvc.perform(put("/api/${1}"). + contentType(MediaType.APPLICATION_JSON). + content("""[{"text":"Gonna see you at Warsaw"}]""")). + andExpect(status().isOk()) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy new file mode 100644 index 0000000000..6057d7081a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.twitter.places + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import org.springframework.cloud.contract.verifier.twitter.place.PairIdController +import spock.lang.Specification + +// tag::base_class[] +abstract class BaseMockMvcSpec extends Specification { + + def setup() { + RestAssuredMockMvc.standaloneSetup(new PairIdController()) + } + + void isProperCorrelationId(Integer correlationId) { + assert correlationId == 123456 + } + + void isEmpty(String value) { + assert value == null + } + +} +// end::base_class[] diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/resources/logback-test.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/resources/logback-test.groovy new file mode 100644 index 0000000000..0b49bdfb4a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/resources/logback-test.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.core.ConsoleAppender + +String console = "CONSOLE" +String logPattern = "%d{yyyy-MM-dd HH:mm:ss.SSSZ, Europe/Warsaw} | %-5level | %X{correlationId} | %thread | %logger{1} | %m%n" + +appender(console, ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = logPattern + } +} + +root(INFO, [console]) +logger("org.springframework.cloud.contract.verifier", DEBUG) diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/build.gradle new file mode 100644 index 0000000000..222f9b241c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/build.gradle @@ -0,0 +1,80 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + } +} + +apply plugin: 'groovy' +apply plugin: 'contract-verifier' +apply plugin: 'maven-publish' + +group = 'org.springframework.cloud.testprojects' + +ext { + contractsDir = file("repository/mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") + targetFramework = 'Spock' +} + +configurations { + all { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + // To prevent an accidental usage of groovy-all.jar and groovy.jar in different versions + // all modularized Groovy jars are replaced with groovy-all.jar by default. + if (details.requested.group == 'org.codehaus.groovy' && details.requested.name != "groovy-all") { + details.useTarget("org.codehaus.groovy:groovy-all:${details.requested.version}") + } + } + } + } +} + +repositories { + mavenCentral() + mavenLocal() + maven { + url "http://repo.spring.io/snapshot" + } + maven { + url "http://repo.spring.io/milestone" + } +} + +dependencies { + compile "org.codehaus.groovy:groovy-all:2.4.5" + compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + compile "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" + compile "org.springframework.boot:spring-boot-starter-integration:${springBootVersion}" + + compile fileTree(dir: messagingLibsDir, include: '*.jar') + + testCompile "org.spockframework:spock-spring:1.0-groovy-2.4" + testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testCompile "ch.qos.logback:logback-classic:1.1.2" + testCompile 'com.jayway.restassured:spring-mock-mvc:2.9.0' // needed if you're going to use Spring MockMvc +} + +contractVerifier { + //baseClassForTests = 'org.springframework.cloud.samples.book.MessagingBaseTest' + baseClassForTests = 'org.springframework.cloud.samples.book.MessagingBaseSpec' + basePackageForTests = 'contracts' + //targetFramework = 'JUnit' + targetFramework = 'Spock' + contractsDslDir = contractsDir +// generatedTestSourcesDir = file("${project.rootDir}/src/test/groovy/") + stubsOutputDir = stubsOutputDirRoot +} + +generateContractTests.dependsOn generateWireMockClientStubs + +wrapper { + gradleVersion '2.12' +} + +test { + testLogging { + exceptionFormat = 'full' + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle.properties new file mode 100644 index 0000000000..7293ee6bc8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +groupId=org.springframework.cloud +jacksonMapper=1.9.13 +restAssuredVersion=2.9.0 +springVersion=4.2.3.RELEASE + +springBootVersion=1.3.3.RELEASE \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000000..7f6620ddcf --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Sun Apr 17 20:31:11 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew new file mode 100755 index 0000000000..9d82f78915 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew.bat new file mode 100755 index 0000000000..5f192121eb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/a_foo.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/a_foo.groovy new file mode 100644 index 0000000000..476aa94eb3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/a_foo.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url '/foo' + method 'GET' + } + response { + status 200 + body 'bar' + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookDeleted.groovy new file mode 100644 index 0000000000..4f7ca765f5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookDeleted.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + label 'some_label' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned1.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned1.groovy new file mode 100644 index 0000000000..302e1d11f2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned1.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned2.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned2.groovy new file mode 100644 index 0000000000..c5a9919e99 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/repository/mappings/bookReturned2.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + label 'some_label' + input { + messageFrom('input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/settings.gradle new file mode 100644 index 0000000000..f452413fb1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/settings.gradle @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +rootProject.name='bootSimple' diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookDeleted.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookDeleted.groovy new file mode 100644 index 0000000000..3d9e527fb5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookDeleted.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookDeleted { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookDeleted(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookListener.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookListener.groovy new file mode 100644 index 0000000000..6399b53a79 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookListener.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder + +import java.util.concurrent.atomic.AtomicBoolean + +@CompileStatic +@Slf4j +class BookListener { + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + Message returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + return MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ])) + } + + /** + Scenario for "should generate tests triggered by a message": + client side: if sends a message to input.messageFrom then message will be sent to output.messageFrom + server side: will send a message to input, verify the message contents and await upon receiving message on the output messageFrom + */ + void bookDeleted(BookDeleted bookDeleted) { + log.info("Deleting book [$bookDeleted]") + bookSuccessfulyDeleted.set(true) + } + + AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false) +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookReturned.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookReturned.groovy new file mode 100644 index 0000000000..22353e827f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookReturned.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic + +@CompileStatic +class BookReturned { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookService.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookService.groovy new file mode 100644 index 0000000000..b44a36a834 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/BookService.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Service + +@Service +@CompileStatic +@Slf4j +class BookService { + + @Autowired @Qualifier("outputChannel") MessageChannel outputChannel + + /** + Scenario for "should generate tests triggered by a method": + client side: must have a possibility to "trigger" sending of a message to the given messageFrom + server side: will run the method and await upon receiving message on the output messageFrom + + Method triggers sending a message to a source + */ + void returnBook(BookReturned bookReturned) { + log.info("Returning book [$bookReturned]") + outputChannel.send(MessageBuilder.createMessage(bookReturned, new MessageHeaders([ + 'BOOK-NAME': bookReturned.bookName as Object + ]))) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/IntegrationMessagingApplication.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/IntegrationMessagingApplication.groovy new file mode 100644 index 0000000000..e7345c64c8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/groovy/org/springframework/cloud/samples/book/IntegrationMessagingApplication.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ImportResource +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication +@RestController +@ImportResource("classpath*:integration-context.xml") +class IntegrationMessagingApplication { + + @RequestMapping("/foo") + String foo() { + return "bar" + } + + static void main(String[] args) { + SpringApplication.run(IntegrationMessagingApplication.class, args) + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/resources/integration-context.xml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/resources/integration-context.xml new file mode 100644 index 0000000000..f29045cdb2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/main/resources/integration-context.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/groovy/org/springframework/cloud/samples/book/MessagingBaseSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/groovy/org/springframework/cloud/samples/book/MessagingBaseSpec.groovy new file mode 100644 index 0000000000..b0950a030c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/groovy/org/springframework/cloud/samples/book/MessagingBaseSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc + +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = [IntegrationMessagingApplication], loader = SpringApplicationContextLoader) +abstract class MessagingBaseSpec extends Specification { + + // BASE CLASS WOULD HAVE THIS: + + @Autowired BookService bookService + @Autowired BookListener bookListener + + def setup() { + RestAssuredMockMvc.standaloneSetup(new IntegrationMessagingApplication()) + } + + void bookReturnedTriggered() { + bookService.returnBook(new BookReturned("foo")) + } + + void bookWasDeleted() { + assert bookListener.bookSuccessfulyDeleted.get() + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/java/org/springframework/cloud/samples/book/MessagingBaseTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/java/org/springframework/cloud/samples/book/MessagingBaseTest.java new file mode 100644 index 0000000000..2cc4aa8abb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/src/test/java/org/springframework/cloud/samples/book/MessagingBaseTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.samples.book; + +import org.assertj.core.api.Assertions; +import org.junit.runner.RunWith; +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationContextLoader; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {IntegrationMessagingApplication.class}, loader = SpringApplicationContextLoader.class) +public abstract class MessagingBaseTest { + + // BASE CLASS WOULD HAVE THIS: + + @Autowired BookService bookService; + @Autowired BookListener bookListener; + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new IntegrationMessagingApplication()); + } + + public void bookReturnedTriggered() { + bookService.returnBook(new BookReturned("foo")); + } + + public void bookWasDeleted() { + Assertions.assertThat(bookListener.getBookSuccessfulyDeleted().get()).isTrue(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle new file mode 100644 index 0000000000..9375d437c5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE" + } +} + +group = 'org.springframework.cloud.testprojects' + +ext { + restAssuredVersion = '2.5.0' + spockVersion = '1.0-groovy-2.4' + wiremockVersion = '2.0.10-beta' + + contractVerifierStubsBaseDirectory = 'src/test/resources/stubs' +} + +subprojects { + apply plugin: 'groovy' + + repositories { + mavenCentral() + mavenLocal() + } + + dependencies { + testCompile 'org.codehaus.groovy:groovy-all:2.4.5' + testCompile "org.spockframework:spock-core:$spockVersion" + testCompile 'junit:junit:4.12' + testCompile "com.github.tomakehurst:wiremock:$wiremockVersion" + } +} + +configure([project(':fraudDetectionService'), project(':loanApplicationService')]) { + apply plugin: 'spring-boot' + apply plugin: 'contract-verifier' + apply plugin: 'maven-publish' + + ext { + contractsDir = file("mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") + } + + contractVerifier { + targetFramework = 'Spock' + testMode = 'JaxRsClient' + baseClassForTests = 'org.springframework.cloud.MvcSpec' + contractsDslDir = file("${project.projectDir.absolutePath}/mappings/") + generatedTestSourcesDir = file("${project.buildDir}/generated-test-sources/") + stubsOutputDir = stubsOutputDirRoot + } + + jar { + version = '0.0.1' + } + + configurations { + compile.exclude module: "spring-boot-starter-tomcat" + } + + dependencies { + compile 'org.glassfish.jersey.containers:jersey-container-jetty-http:2.15' + compile 'org.springframework.boot:spring-boot-starter-jersey' + compile 'org.springframework.boot:spring-boot-starter-jetty' + + testRuntime "org.spockframework:spock-spring:$spockVersion" + + compile 'org.glassfish.jersey.connectors:jersey-apache-connector:2.15' + testCompile 'org.springframework:spring-test' + } + + task cleanup(type: Delete) { + delete 'src/test/resources/mappings', 'src/test/resources/stubs' + } + + clean.dependsOn('cleanup') + + test { + testLogging { + exceptionFormat = 'full' + } + } + +} + +configure(project(':fraudDetectionService')) { + test.dependsOn('generateWireMockClientStubs') +} + +configure(project(':loanApplicationService')) { + + task copyCollaboratorStubs(type: Copy) { + File fraudBuildDir = project(':fraudDetectionService').buildDir + from(new File(fraudBuildDir, "/production/${project(':fraudDetectionService').name}-stubs/")) + into "src/test/resources/" + } + + generateContractTests.dependsOn('copyCollaboratorStubs') +} + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..1fc39340b5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":99999} + """ + ) + headers { + header("""Content-Type""", """application/vnd.fraud.v1+json""") + } + + } + response { + status 200 + body( """{ + "fraudCheckStatus": "${value(client('FRAUD'), server(regex('[A-Z]{5}')))}", + "rejectionReason": "Amount too high" +}""") + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..94026415f3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java new file mode 100644 index 0000000000..e671a56892 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.frauddetection; + +import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + ResourceConfig resourceConfig() { + return ResourceConfig.forApplication(new FraudRestApplication()); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java new file mode 100644 index 0000000000..3eaaea3a42 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java @@ -0,0 +1,38 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheck; +import org.springframework.cloud.frauddetection.model.FraudCheckResult; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.ws.rs.*; +import java.math.BigDecimal; + +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.FRAUD; +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.OK; + +@Controller +@Path("/") +public class FraudDetectionController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + private static final String NO_REASON = null; + private static final String AMOUNT_TOO_HIGH = "Amount too high"; + private static final BigDecimal MAX_AMOUNT = new BigDecimal("5000"); + + @PUT + @Path("/fraudcheck") + @Produces(FRAUD_SERVICE_JSON_VERSION_1) + @Consumes(FRAUD_SERVICE_JSON_VERSION_1) + public FraudCheckResult fraudCheck(@RequestBody(required = false) FraudCheck fraudCheck) { + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FRAUD, AMOUNT_TOO_HIGH); + } + return new FraudCheckResult(OK, NO_REASON); + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudRestApplication.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudRestApplication.java new file mode 100644 index 0000000000..064e59196b --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudRestApplication.java @@ -0,0 +1,13 @@ +package org.springframework.cloud.frauddetection; + +import java.util.Collections; +import java.util.Set; + +public class FraudRestApplication extends javax.ws.rs.core.Application { + + @Override + public Set> getClasses() { + return Collections.>singleton(FraudDetectionController.class); + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java new file mode 100644 index 0000000000..72551915bc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java @@ -0,0 +1,29 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudCheck { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java new file mode 100644 index 0000000000..95fd5e04c4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java @@ -0,0 +1,32 @@ +package org.springframework.cloud.frauddetection.model; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudCheckResult() { + } + + public FraudCheckResult(FraudCheckStatus fraudCheckStatus, String rejectionReason) { + this.fraudCheckStatus = fraudCheckStatus; + this.rejectionReason = rejectionReason; + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..b4fd951df2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/resources/application.yml new file mode 100644 index 0000000000..a30a91f034 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8085 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy new file mode 100644 index 0000000000..1fab371acc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud +import org.springframework.cloud.frauddetection.Application +import org.springframework.cloud.frauddetection.FraudRestApplication +import org.eclipse.jetty.server.Server +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider +import org.glassfish.jersey.client.ClientConfig +import org.glassfish.jersey.jetty.JettyHttpContainerFactory +import org.glassfish.jersey.server.ResourceConfig +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Shared +import spock.lang.Specification + +import javax.ws.rs.client.Client +import javax.ws.rs.client.ClientBuilder +import javax.ws.rs.client.WebTarget +import javax.ws.rs.core.UriBuilder + +import static org.springframework.util.SocketUtils.findAvailableTcpPort + +abstract class MvcSpec extends Specification { + + @Shared + WebTarget webTarget + + @Shared + private Server server + + @Shared + private Client client + + def setupSpec() { + + URI baseUri = UriBuilder.fromUri("http://localhost").port(findAvailableTcpPort(8000)).build() + + + ResourceConfig resourceConfig = ResourceConfig.forApplication(new FraudRestApplication()) + resourceConfig.property("contextConfig", new AnnotationConfigApplicationContext(Application)) + server = JettyHttpContainerFactory.createServer(baseUri, resourceConfig, true) + + ClientConfig clientConfig = new ClientConfig() + clientConfig.connectorProvider(new ApacheConnectorProvider()) + client = ClientBuilder.newClient(clientConfig) + + webTarget = client.target(baseUri) + + server.start() + } + + def cleanupSpec() { + client?.close() + server?.stop() + } + + void assertThatRejectionReasonIsNull(def rejectionReason) { + assert !rejectionReason + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java new file mode 100644 index 0000000000..93c9209621 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java @@ -0,0 +1,70 @@ +package org.springframework.cloud; + +import org.springframework.cloud.frauddetection.Application; +import org.springframework.cloud.frauddetection.FraudRestApplication; +import org.eclipse.jetty.server.Server; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jetty.JettyHttpContainerFactory; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; + +import static org.springframework.util.SocketUtils.findAvailableTcpPort; + +public abstract class MvcTest { + + public static WebTarget webTarget; + + private static Server server; + + private static Client client; + + @BeforeClass + public static void setupTest() { + + URI baseUri = UriBuilder.fromUri("http://localhost").port(findAvailableTcpPort(8000)).build(); + + + ResourceConfig resourceConfig = ResourceConfig.forApplication(new FraudRestApplication()); + resourceConfig.property("contextConfig", new AnnotationConfigApplicationContext(Application.class)); + server = JettyHttpContainerFactory.createServer(baseUri, resourceConfig, true); + + ClientConfig clientConfig = new ClientConfig(); + clientConfig.connectorProvider(new ApacheConnectorProvider()); + client = ClientBuilder.newClient(clientConfig); + + webTarget = client.target(baseUri); + + try { + server.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @AfterClass + public static void cleanupTest() { + if(client != null) { + client.close(); + } + if (server != null) { + try { + server.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1aa6b34ba4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Wed Jan 28 00:32:44 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/mappings/.gitkeep b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/mappings/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java new file mode 100644 index 0000000000..e70eca242e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java new file mode 100644 index 0000000000..64402473fa --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheckStatus; +import org.springframework.cloud.frauddetection.model.FraudServiceRequest; +import org.springframework.cloud.frauddetection.model.FraudServiceResponse; +import org.springframework.cloud.frauddetection.model.LoanApplication; +import org.springframework.cloud.frauddetection.model.LoanApplicationResult; +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class LoanApplicationService { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = + "application/vnd.fraud.v1+json"; + + private final RestTemplate restTemplate; + + private int port = 8080; + + public LoanApplicationService() { + this.restTemplate = new RestTemplate(); + } + + public LoanApplicationResult loanApplication(LoanApplication loanApplication) { + FraudServiceRequest request = + new FraudServiceRequest(loanApplication); + + FraudServiceResponse response = + sendRequestToFraudDetectionService(request); + + return buildResponseFromFraudResult(response); + } + + private FraudServiceResponse sendRequestToFraudDetectionService( + FraudServiceRequest request) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), + FraudServiceResponse.class); + + return response.getBody(); + } + + private LoanApplicationResult buildResponseFromFraudResult(FraudServiceResponse response) { + LoanApplicationStatus applicationStatus = null; + if (FraudCheckStatus.OK == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLIED; + } else if (FraudCheckStatus.FRAUD == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLICATION_REJECTED; + } + + return new LoanApplicationResult(applicationStatus, response.getRejectionReason()); + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java new file mode 100644 index 0000000000..403da83818 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +public class Client { + + private String pesel; + + public String getPesel() { + return pesel; + } + + public void setPesel(String pesel) { + this.pesel = pesel; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..4fb43b0cf8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java new file mode 100644 index 0000000000..fc2aa4580c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudServiceRequest { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudServiceRequest() { + } + + public FraudServiceRequest(LoanApplication loanApplication) { + this.clientPesel = loanApplication.getClient().getPesel(); + this.loanAmount = loanApplication.getAmount(); + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java new file mode 100644 index 0000000000..79eb5b2c0d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +public class FraudServiceResponse { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudServiceResponse() { + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java new file mode 100644 index 0000000000..ad2c6c80a1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class LoanApplication { + + private Client client; + + private BigDecimal amount; + + private String loanApplicationId; + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getLoanApplicationId() { + return loanApplicationId; + } + + public void setLoanApplicationId(String loanApplicationId) { + this.loanApplicationId = loanApplicationId; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java new file mode 100644 index 0000000000..a9b0617c11 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +public class LoanApplicationResult { + + private LoanApplicationStatus loanApplicationStatus; + + private String rejectionReason; + + public LoanApplicationResult() { + } + + public LoanApplicationResult(LoanApplicationStatus loanApplicationStatus, String rejectionReason) { + this.loanApplicationStatus = loanApplicationStatus; + this.rejectionReason = rejectionReason; + } + + public LoanApplicationStatus getLoanApplicationStatus() { + return loanApplicationStatus; + } + + public void setLoanApplicationStatus(LoanApplicationStatus loanApplicationStatus) { + this.loanApplicationStatus = loanApplicationStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java new file mode 100644 index 0000000000..e939d49b8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.frauddetection.model; + +public enum LoanApplicationStatus { + LOAN_APPLIED, LOAN_APPLICATION_REJECTED +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/resources/application.yml new file mode 100644 index 0000000000..e86bbd0e0f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8090 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy new file mode 100644 index 0000000000..d5104eb499 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud + +import org.springframework.cloud.frauddetection.Application +import org.springframework.cloud.frauddetection.LoanApplicationService +import org.springframework.cloud.frauddetection.model.Client +import org.springframework.cloud.frauddetection.model.LoanApplication +import org.springframework.cloud.frauddetection.model.LoanApplicationResult +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus +import com.github.tomakehurst.wiremock.junit.WireMockClassRule +import org.junit.ClassRule +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.test.context.ContextConfiguration +import spock.lang.Shared +import spock.lang.Specification + +@ContextConfiguration(loader = SpringApplicationContextLoader, classes = Application) +class LoanApplicationServiceSpec extends Specification { + + public static int port = org.springframework.util.SocketUtils.findAvailableTcpPort() + + @ClassRule + @Shared + WireMockClassRule wireMockRule = new WireMockClassRule(port) + + @Autowired + LoanApplicationService sut + + def setup() { + sut.port = port + } + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 123.123) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason == null + } + + def 'should be rejected due to abnormal loan amount'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 99_999) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLICATION_REJECTED + loanApplication.rejectionReason == 'Amount too high' + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..7229872299 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "\\s*\\{\\s*\"clientPesel\"\\s*:\\s*\"?[0-9]{10}\"?\\s*,\\s*\"loanAmount\"\\s*:\\s*\"?99999\"?\\s*\\}\\s*" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..5a251171c7 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "\\s*\\{\\s*\"clientPesel\"\\s*:\\s*\"?[0-9]{10}\"?\\s*,\\s*\"loanAmount\"\\s*:\\s*\"?123.123\"?\\s*\\}\\s*" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"OK\",\"rejectionReason\":null}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/settings.gradle new file mode 100644 index 0000000000..48db488159 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/settings.gradle @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +include ':fraudDetectionService' +include ':loanApplicationService' diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle new file mode 100644 index 0000000000..eb108f14e3 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + mavenCentral() + mavenLocal() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE") + } +} + +ext { + restAssuredVersion = '2.5.0' + spockVersion = '1.0-groovy-2.4' + wiremockVersion = '2.0.10-beta' + + contractVerifierStubsBaseDirectory = 'src/test/resources/stubs' +} + +group = 'org.springframework.cloud.testprojects' + +subprojects { + apply plugin: 'groovy' + + repositories { + mavenCentral() + mavenLocal() + } + + dependencies { + testCompile "org.codehaus.groovy:groovy-all:2.4.5" + testCompile "org.spockframework:spock-core:$spockVersion" + testCompile("junit:junit:4.12") + testCompile "com.github.tomakehurst:wiremock:$wiremockVersion" + } +} + +configure([project(':fraudDetectionService'), project(':loanApplicationService')]) { + apply plugin: 'spring-boot' + apply plugin: 'contract-verifier' + apply plugin: 'maven-publish' + + ext { + contractsDir = file("mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") + } + + contractVerifier { + targetFramework = 'Spock' + testMode = 'MockMvc' + baseClassForTests = 'org.springframework.cloud.MvcSpec' + contractsDslDir = file("${project.projectDir.absolutePath}/mappings/") + generatedTestSourcesDir = file("${project.buildDir}/generated-test-sources/") + stubsOutputDir = stubsOutputDirRoot + } + + jar { + version = '0.0.1' + } + + dependencies { + compile("org.springframework.boot:spring-boot-starter-web") { + exclude module: "spring-boot-starter-tomcat" + } + compile("org.springframework.boot:spring-boot-starter-jetty") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testRuntime "org.spockframework:spock-spring:$spockVersion" + testCompile "org.springframework:spring-test" + testCompile "com.jayway.restassured:rest-assured:$restAssuredVersion" + testCompile "com.jayway.restassured:spring-mock-mvc:$restAssuredVersion" + } + + task cleanup(type: Delete) { + delete 'src/test/resources/mappings', 'src/test/resources/stubs' + } + + clean.dependsOn('cleanup') + + test { + testLogging { + exceptionFormat = 'full' + } + } +} + +configure(project(':fraudDetectionService')) { + test.dependsOn('generateWireMockClientStubs') +} + +configure(project(':loanApplicationService')) { + + task copyCollaboratorStubs(type: Copy) { + File fraudBuildDir = project(':fraudDetectionService').buildDir + from(new File(fraudBuildDir, "/production/${project(':fraudDetectionService').name}-stubs/")) + into "src/test/resources/" + } + + generateContractTests.dependsOn('copyCollaboratorStubs') +} + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..a2e6875fd9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsFraud.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":99999} + """ + ) + headers { + header("""Content-Type""", """application/vnd.fraud.v1+json""") + } + + } + response { + status 200 + body( """{ + "fraudCheckStatus": "${value(client('FRAUD'), server(regex('[A-Z]{5}')))}", + "rejectionReason": "Amount too high" +}""") + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..76df090cd6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/Application.java new file mode 100644 index 0000000000..bc8131fe49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/Application.java @@ -0,0 +1,18 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run( + org.springframework.cloud.frauddetection.Application.class, args); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/FraudDetectionController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/FraudDetectionController.java new file mode 100644 index 0000000000..213a037f87 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/FraudDetectionController.java @@ -0,0 +1,39 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheck; +import org.springframework.cloud.frauddetection.model.FraudCheckResult; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; + +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.FRAUD; +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.OK; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +@RestController +public class FraudDetectionController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + private static final String NO_REASON = null; + private static final String AMOUNT_TOO_HIGH = "Amount too high"; + private static final BigDecimal MAX_AMOUNT = new BigDecimal("5000"); + + @RequestMapping( + value = "/fraudcheck", + method = PUT, + consumes = FRAUD_SERVICE_JSON_VERSION_1, + produces = FRAUD_SERVICE_JSON_VERSION_1) + public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FRAUD, AMOUNT_TOO_HIGH); + } + return new FraudCheckResult(OK, NO_REASON); + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheck.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheck.java new file mode 100644 index 0000000000..72551915bc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheck.java @@ -0,0 +1,29 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudCheck { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckResult.java new file mode 100644 index 0000000000..95fd5e04c4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckResult.java @@ -0,0 +1,32 @@ +package org.springframework.cloud.frauddetection.model; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudCheckResult() { + } + + public FraudCheckResult(FraudCheckStatus fraudCheckStatus, String rejectionReason) { + this.fraudCheckStatus = fraudCheckStatus; + this.rejectionReason = rejectionReason; + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..b4fd951df2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/java/org/springframework/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/resources/application.yml new file mode 100644 index 0000000000..a30a91f034 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8085 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy new file mode 100644 index 0000000000..dfff02db5e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud + +import org.springframework.cloud.frauddetection.FraudDetectionController +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import spock.lang.Specification + +class MvcSpec extends Specification { + def setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController()) + } + + void assertThatRejectionReasonIsNull(def rejectionReason) { + assert !rejectionReason + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java new file mode 100644 index 0000000000..8fa2aad6ff --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java @@ -0,0 +1,16 @@ +package org.springframework.cloud; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; + +public class MvcTest { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new org.springframework.cloud.frauddetection.FraudDetectionController()); + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1aa6b34ba4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Wed Jan 28 00:32:44 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/mappings/.gitkeep b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/mappings/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java new file mode 100644 index 0000000000..bc8131fe49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java @@ -0,0 +1,18 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run( + org.springframework.cloud.frauddetection.Application.class, args); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java new file mode 100644 index 0000000000..65ec787113 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java @@ -0,0 +1,68 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheckStatus; +import org.springframework.cloud.frauddetection.model.FraudServiceRequest; +import org.springframework.cloud.frauddetection.model.FraudServiceResponse; +import org.springframework.cloud.frauddetection.model.LoanApplication; +import org.springframework.cloud.frauddetection.model.LoanApplicationResult; +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class LoanApplicationService { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = + "application/vnd.fraud.v1+json"; + + private final RestTemplate restTemplate; + + private int port = 8080; + + public LoanApplicationService() { + this.restTemplate = new RestTemplate(); + } + + public LoanApplicationResult loanApplication(LoanApplication loanApplication) { + FraudServiceRequest request = + new FraudServiceRequest(loanApplication); + + FraudServiceResponse response = + sendRequestToFraudDetectionService(request); + + return buildResponseFromFraudResult(response); + } + + private FraudServiceResponse sendRequestToFraudDetectionService( + FraudServiceRequest request) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), + FraudServiceResponse.class); + + return response.getBody(); + } + + private LoanApplicationResult buildResponseFromFraudResult(FraudServiceResponse response) { + LoanApplicationStatus applicationStatus = null; + if (FraudCheckStatus.OK == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLIED; + } else if (FraudCheckStatus.FRAUD == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLICATION_REJECTED; + } + + return new LoanApplicationResult(applicationStatus, response.getRejectionReason()); + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java new file mode 100644 index 0000000000..1a85740dde --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java @@ -0,0 +1,14 @@ +package org.springframework.cloud.frauddetection.model; + +public class Client { + + private String pesel; + + public String getPesel() { + return pesel; + } + + public void setPesel(String pesel) { + this.pesel = pesel; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..b4fd951df2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java new file mode 100644 index 0000000000..f79ff3b740 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java @@ -0,0 +1,34 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudServiceRequest { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudServiceRequest() { + } + + public FraudServiceRequest(LoanApplication loanApplication) { + this.clientPesel = loanApplication.getClient().getPesel(); + this.loanAmount = loanApplication.getAmount(); + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java new file mode 100644 index 0000000000..dbf8384a53 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.frauddetection.model; + +public class FraudServiceResponse { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudServiceResponse() { + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java new file mode 100644 index 0000000000..58757331d0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java @@ -0,0 +1,36 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class LoanApplication { + + private Client client; + + private BigDecimal amount; + + private String loanApplicationId; + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getLoanApplicationId() { + return loanApplicationId; + } + + public void setLoanApplicationId(String loanApplicationId) { + this.loanApplicationId = loanApplicationId; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java new file mode 100644 index 0000000000..11c3ee1d2c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java @@ -0,0 +1,32 @@ +package org.springframework.cloud.frauddetection.model; + +public class LoanApplicationResult { + + private LoanApplicationStatus loanApplicationStatus; + + private String rejectionReason; + + public LoanApplicationResult() { + } + + public LoanApplicationResult(LoanApplicationStatus loanApplicationStatus, String rejectionReason) { + this.loanApplicationStatus = loanApplicationStatus; + this.rejectionReason = rejectionReason; + } + + public LoanApplicationStatus getLoanApplicationStatus() { + return loanApplicationStatus; + } + + public void setLoanApplicationStatus(LoanApplicationStatus loanApplicationStatus) { + this.loanApplicationStatus = loanApplicationStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java new file mode 100644 index 0000000000..bdb886d0fc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum LoanApplicationStatus { + LOAN_APPLIED, LOAN_APPLICATION_REJECTED +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/resources/application.yml new file mode 100644 index 0000000000..e86bbd0e0f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8090 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy new file mode 100644 index 0000000000..d5104eb499 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud + +import org.springframework.cloud.frauddetection.Application +import org.springframework.cloud.frauddetection.LoanApplicationService +import org.springframework.cloud.frauddetection.model.Client +import org.springframework.cloud.frauddetection.model.LoanApplication +import org.springframework.cloud.frauddetection.model.LoanApplicationResult +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus +import com.github.tomakehurst.wiremock.junit.WireMockClassRule +import org.junit.ClassRule +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.test.context.ContextConfiguration +import spock.lang.Shared +import spock.lang.Specification + +@ContextConfiguration(loader = SpringApplicationContextLoader, classes = Application) +class LoanApplicationServiceSpec extends Specification { + + public static int port = org.springframework.util.SocketUtils.findAvailableTcpPort() + + @ClassRule + @Shared + WireMockClassRule wireMockRule = new WireMockClassRule(port) + + @Autowired + LoanApplicationService sut + + def setup() { + sut.port = port + } + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 123.123) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason == null + } + + def 'should be rejected due to abnormal loan amount'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 99_999) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLICATION_REJECTED + loanApplication.rejectionReason == 'Amount too high' + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..157726ca2e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "{\"clientPesel\":\"[0-9]{10}\",\"loanAmount\":\"99999\"}" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..afa27159d9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "{\"clientPesel\":\"[0-9]{10}\",\"loanAmount\":\"123.123\"}" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"OK\",\"rejectionReason\":null}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/settings.gradle new file mode 100644 index 0000000000..48db488159 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/settings.gradle @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +include ':fraudDetectionService' +include ':loanApplicationService' diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle new file mode 100644 index 0000000000..67fa4ba4b8 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle @@ -0,0 +1,134 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + mavenCentral() + mavenLocal() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE") + } +} + +ext { + restAssuredVersion = '2.5.0' + spockVersion = '1.0-groovy-2.4' + wiremockVersion = '2.0.10-beta' + + contractVerifierStubsBaseDirectory = 'src/test/resources/stubs' +} + +group = 'org.springframework.cloud.testprojects' + +subprojects { + apply plugin: 'groovy' + apply plugin: 'maven-publish' + + repositories { + mavenCentral() + mavenLocal() + } + + dependencies { + testCompile "org.codehaus.groovy:groovy-all:2.4.5" + testCompile "org.spockframework:spock-core:$spockVersion" + testCompile("junit:junit:4.12") + testCompile "com.github.tomakehurst:wiremock:$wiremockVersion" + } +} + +configure([project(':fraudDetectionService'), project(':loanApplicationService')]) { + apply plugin: 'spring-boot' + apply plugin: 'contract-verifier' + + // tag::jar_setup[] + ext { + contractsDir = file("mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") + } + + // Automatically added by plugin: + // copyContracts - copies contracts to the output folder from which JAR will be created + // verifierStubsJar - JAR with a provided stub suffix + // the presented publication is also added by the plugin but you can modify it as you wish + + publishing { + publications { + stubs(MavenPublication) { + artifactId "${project.name}-stubs" + artifact verifierStubsJar + } + } + } + // end::jar_setup[] + + contractVerifier { + // tag::target_framework[] + targetFramework = 'Spock' + // end::target_framework[] + testMode = 'MockMvc' + baseClassForTests = 'org.springframework.cloud.MvcSpec' + contractsDslDir = file("${project.projectDir.absolutePath}/mappings/") + generatedTestSourcesDir = file("${project.buildDir}/generated-test-sources/") + stubsOutputDir = stubsOutputDirRoot + } + + jar { + version = '0.0.1' + } + + dependencies { + compile("org.springframework.boot:spring-boot-starter-web") { + exclude module: "spring-boot-starter-tomcat" + } + compile("org.springframework.boot:spring-boot-starter-jetty") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testRuntime "org.spockframework:spock-spring:$spockVersion" + testCompile "org.springframework:spring-test" + testCompile "com.jayway.restassured:rest-assured:$restAssuredVersion" + testCompile "com.jayway.restassured:spring-mock-mvc:$restAssuredVersion" + } + + task cleanup(type: Delete) { + delete 'src/test/resources/mappings', 'src/test/resources/stubs' + } + + clean.dependsOn('cleanup') + + test { + testLogging { + exceptionFormat = 'full' + } + } +} + +configure(project(':fraudDetectionService')) { + test.dependsOn('generateWireMockClientStubs') +} + +configure(project(':loanApplicationService')) { + + task copyCollaboratorStubs(type: Copy) { + File fraudBuildDir = project(':fraudDetectionService').buildDir + from(new File(fraudBuildDir, "/production/${project(':fraudDetectionService').name}-stubs/")) + into "src/test/resources/" + } + + generateContractTests.dependsOn('copyCollaboratorStubs') +} + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/1_shouldMarkClientAsNotFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/1_shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..76df090cd6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/1_shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + header('Content-Type', 'application/vnd.fraud.v1+json') + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(client(null), server(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/2_shouldMarkClientAsFraud.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/2_shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..1fc39340b5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/mappings/fraudDetectionService/2_shouldMarkClientAsFraud.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${value(client(regex('[0-9]{10}')), server('1234567890'))}", + "loanAmount":99999} + """ + ) + headers { + header("""Content-Type""", """application/vnd.fraud.v1+json""") + } + + } + response { + status 200 + body( """{ + "fraudCheckStatus": "${value(client('FRAUD'), server(regex('[A-Z]{5}')))}", + "rejectionReason": "Amount too high" +}""") + headers { + header('Content-Type': 'application/vnd.fraud.v1+json') + } + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java new file mode 100644 index 0000000000..bc8131fe49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/Application.java @@ -0,0 +1,18 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run( + org.springframework.cloud.frauddetection.Application.class, args); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java new file mode 100644 index 0000000000..213a037f87 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/FraudDetectionController.java @@ -0,0 +1,39 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheck; +import org.springframework.cloud.frauddetection.model.FraudCheckResult; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; + +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.FRAUD; +import static org.springframework.cloud.frauddetection.model.FraudCheckStatus.OK; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +@RestController +public class FraudDetectionController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + private static final String NO_REASON = null; + private static final String AMOUNT_TOO_HIGH = "Amount too high"; + private static final BigDecimal MAX_AMOUNT = new BigDecimal("5000"); + + @RequestMapping( + value = "/fraudcheck", + method = PUT, + consumes = FRAUD_SERVICE_JSON_VERSION_1, + produces = FRAUD_SERVICE_JSON_VERSION_1) + public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FRAUD, AMOUNT_TOO_HIGH); + } + return new FraudCheckResult(OK, NO_REASON); + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java new file mode 100644 index 0000000000..72551915bc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheck.java @@ -0,0 +1,29 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudCheck { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java new file mode 100644 index 0000000000..95fd5e04c4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckResult.java @@ -0,0 +1,32 @@ +package org.springframework.cloud.frauddetection.model; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudCheckResult() { + } + + public FraudCheckResult(FraudCheckStatus fraudCheckStatus, String rejectionReason) { + this.fraudCheckStatus = fraudCheckStatus; + this.rejectionReason = rejectionReason; + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..b4fd951df2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/resources/application.yml new file mode 100644 index 0000000000..a30a91f034 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8085 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy new file mode 100644 index 0000000000..dfff02db5e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/groovy/org/springframework/cloud/MvcSpec.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud + +import org.springframework.cloud.frauddetection.FraudDetectionController +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import spock.lang.Specification + +class MvcSpec extends Specification { + def setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController()) + } + + void assertThatRejectionReasonIsNull(def rejectionReason) { + assert !rejectionReason + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java new file mode 100644 index 0000000000..8fa2aad6ff --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/fraudDetectionService/src/test/java/org/springframework/cloud/MvcTest.java @@ -0,0 +1,16 @@ +package org.springframework.cloud; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; + +public class MvcTest { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new org.springframework.cloud.frauddetection.FraudDetectionController()); + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1aa6b34ba4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2013-2016 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Wed Jan 28 00:32:44 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew.bat b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/mappings/.gitkeep b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/mappings/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java new file mode 100644 index 0000000000..bc8131fe49 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/Application.java @@ -0,0 +1,18 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run( + org.springframework.cloud.frauddetection.Application.class, args); + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java new file mode 100644 index 0000000000..65ec787113 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/LoanApplicationService.java @@ -0,0 +1,68 @@ +package org.springframework.cloud.frauddetection; + +import org.springframework.cloud.frauddetection.model.FraudCheckStatus; +import org.springframework.cloud.frauddetection.model.FraudServiceRequest; +import org.springframework.cloud.frauddetection.model.FraudServiceResponse; +import org.springframework.cloud.frauddetection.model.LoanApplication; +import org.springframework.cloud.frauddetection.model.LoanApplicationResult; +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class LoanApplicationService { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = + "application/vnd.fraud.v1+json"; + + private final RestTemplate restTemplate; + + private int port = 8080; + + public LoanApplicationService() { + this.restTemplate = new RestTemplate(); + } + + public LoanApplicationResult loanApplication(LoanApplication loanApplication) { + FraudServiceRequest request = + new FraudServiceRequest(loanApplication); + + FraudServiceResponse response = + sendRequestToFraudDetectionService(request); + + return buildResponseFromFraudResult(response); + } + + private FraudServiceResponse sendRequestToFraudDetectionService( + FraudServiceRequest request) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), + FraudServiceResponse.class); + + return response.getBody(); + } + + private LoanApplicationResult buildResponseFromFraudResult(FraudServiceResponse response) { + LoanApplicationStatus applicationStatus = null; + if (FraudCheckStatus.OK == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLIED; + } else if (FraudCheckStatus.FRAUD == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLICATION_REJECTED; + } + + return new LoanApplicationResult(applicationStatus, response.getRejectionReason()); + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java new file mode 100644 index 0000000000..1a85740dde --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/Client.java @@ -0,0 +1,14 @@ +package org.springframework.cloud.frauddetection.model; + +public class Client { + + private String pesel; + + public String getPesel() { + return pesel; + } + + public void setPesel(String pesel) { + this.pesel = pesel; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java new file mode 100644 index 0000000000..b4fd951df2 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java new file mode 100644 index 0000000000..f79ff3b740 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceRequest.java @@ -0,0 +1,34 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class FraudServiceRequest { + + private String clientPesel; + + private BigDecimal loanAmount; + + public FraudServiceRequest() { + } + + public FraudServiceRequest(LoanApplication loanApplication) { + this.clientPesel = loanApplication.getClient().getPesel(); + this.loanAmount = loanApplication.getAmount(); + } + + public String getClientPesel() { + return clientPesel; + } + + public void setClientPesel(String clientPesel) { + this.clientPesel = clientPesel; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java new file mode 100644 index 0000000000..dbf8384a53 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/FraudServiceResponse.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.frauddetection.model; + +public class FraudServiceResponse { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudServiceResponse() { + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java new file mode 100644 index 0000000000..58757331d0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplication.java @@ -0,0 +1,36 @@ +package org.springframework.cloud.frauddetection.model; + +import java.math.BigDecimal; + +public class LoanApplication { + + private Client client; + + private BigDecimal amount; + + private String loanApplicationId; + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getLoanApplicationId() { + return loanApplicationId; + } + + public void setLoanApplicationId(String loanApplicationId) { + this.loanApplicationId = loanApplicationId; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java new file mode 100644 index 0000000000..11c3ee1d2c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationResult.java @@ -0,0 +1,32 @@ +package org.springframework.cloud.frauddetection.model; + +public class LoanApplicationResult { + + private LoanApplicationStatus loanApplicationStatus; + + private String rejectionReason; + + public LoanApplicationResult() { + } + + public LoanApplicationResult(LoanApplicationStatus loanApplicationStatus, String rejectionReason) { + this.loanApplicationStatus = loanApplicationStatus; + this.rejectionReason = rejectionReason; + } + + public LoanApplicationStatus getLoanApplicationStatus() { + return loanApplicationStatus; + } + + public void setLoanApplicationStatus(LoanApplicationStatus loanApplicationStatus) { + this.loanApplicationStatus = loanApplicationStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java new file mode 100644 index 0000000000..bdb886d0fc --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/java/org/springframework/cloud/frauddetection/model/LoanApplicationStatus.java @@ -0,0 +1,5 @@ +package org.springframework.cloud.frauddetection.model; + +public enum LoanApplicationStatus { + LOAN_APPLIED, LOAN_APPLICATION_REJECTED +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/resources/application.yml b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/resources/application.yml new file mode 100644 index 0000000000..e86bbd0e0f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=8090 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy new file mode 100644 index 0000000000..eae9a5c817 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/groovy/org/springframework/cloud/LoanApplicationServiceSpec.groovy @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud + +import org.springframework.cloud.frauddetection.Application +import org.springframework.cloud.frauddetection.LoanApplicationService +import org.springframework.cloud.frauddetection.model.Client +import org.springframework.cloud.frauddetection.model.LoanApplication +import org.springframework.cloud.frauddetection.model.LoanApplicationResult +import org.springframework.cloud.frauddetection.model.LoanApplicationStatus +import com.github.tomakehurst.wiremock.junit.WireMockClassRule +import org.junit.ClassRule +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.SpringApplicationContextLoader +import org.springframework.test.context.ContextConfiguration +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Stepwise + +@ContextConfiguration(loader = SpringApplicationContextLoader, classes = Application) +@Stepwise +class LoanApplicationServiceSpec extends Specification { + + public static int port = org.springframework.util.SocketUtils.findAvailableTcpPort() + + @ClassRule + @Shared + WireMockClassRule wireMockRule = new WireMockClassRule(port) + + @Autowired + LoanApplicationService sut + + def setup() { + sut.port = port + } + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 123.123) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason == null + } + + def 'should be rejected due to abnormal loan amount'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(pesel: '1234567890'), amount: 99_999) + when: + LoanApplicationResult loanApplication = sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLICATION_REJECTED + loanApplication.rejectionReason == 'Amount too high' + } + + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..157726ca2e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "{\"clientPesel\":\"[0-9]{10}\",\"loanAmount\":\"99999\"}" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..afa27159d9 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/loanApplicationService/src/test/resources/mappings/fraudDetectionService/shouldMarkClientAsNotFraud.json @@ -0,0 +1,23 @@ +{ + "request": { + "method": "PUT", + "headers": { + "Content-Type": { + "equalTo": "application/vnd.fraud.v1+json" + } + }, + "url": "/fraudcheck", + "bodyPatterns": [ + { + "matches": "{\"clientPesel\":\"[0-9]{10}\",\"loanAmount\":\"123.123\"}" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": "{\"fraudCheckStatus\":\"OK\",\"rejectionReason\":null}" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/settings.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/settings.gradle new file mode 100644 index 0000000000..48db488159 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/settings.gradle @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +include ':fraudDetectionService' +include ':loanApplicationService' diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/README.adoc b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/README.adoc new file mode 100644 index 0000000000..2488410263 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/README.adoc @@ -0,0 +1,23 @@ + = Spring Cloud Contract Verifier Messaging + +Spring Cloud Contract Verifier allows you to verify your application that uses messaging as means of communication. +All of our integrations are working with Spring but you can also set one yourself. + +== Integrations + +You can use one of the three integration configurations: + +- Apache Camel +- Spring Integration +- Spring Cloud Stream + +If you're using Spring Boot, the aforementioned test configurations will be appended automatically. + +== Manual Integration + +The `spring-cloud-contract-verifier-messaging-core` module contains 3 main interfaces: + +- `ContractVerifierMessage` - describes a message received / sent to a channel / queue / topic etc. +- `ContractVerifierMessageBuilder` - describes how to build a message +- `ContractVerifierMessaging` - class that allows you to build, send and receive messages + diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/build.gradle new file mode 100644 index 0000000000..fd255690d6 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/build.gradle @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + compile "org.apache.camel:camel-spring:${camelVersion}" + compile 'org.slf4j:slf4j-api:1.6.0' +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelMessage.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelMessage.java new file mode 100644 index 0000000000..1d4a94257d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelMessage.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.camel; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.apache.camel.Message; + +import java.util.Map; + +/** + * @author Marcin Grzejszczak + */ +public class CamelMessage implements ContractVerifierMessage { + + private final Message delegate; + + public CamelMessage(Message delegate) { + this.delegate = delegate; + } + + @Override + @SuppressWarnings("unchecked") + public T getPayload() { + return (T) delegate.getBody(); + } + + @Override + public Map getHeaders() { + return delegate.getHeaders(); + } + + @Override + public Object getHeader(String key) { + return getHeaders().get(key); + } + + @Override + public Message convert() { + return delegate; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java new file mode 100644 index 0000000000..71280d4700 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.camel; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.apache.camel.CamelContext; +import org.springframework.context.annotation.Bean; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierCamelConfiguration { + + @Bean ContractVerifierMessaging contractVerifierMessaging(CamelContext context, + ContractVerifierMessageBuilder builder) { + return new ContractVerifierCamelMessaging(context, builder); + } + + @Bean ContractVerifierMessageBuilder contractVerifierMessageBuilder() { + return new ContractVerifierCamelMessageBuilder(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java new file mode 100644 index 0000000000..77aa4f2c91 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.camel; + +import java.util.Map; + +import org.apache.camel.Message; +import org.apache.camel.impl.DefaultMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierCamelMessageBuilder implements + ContractVerifierMessageBuilder { + + @Override + public ContractVerifierMessage create(T payload, Map headers) { + DefaultMessage message = new DefaultMessage(); + message.setBody(payload); + message.setHeaders(headers); + return new CamelMessage<>(message); + } + + @Override + public ContractVerifierMessage create(Message message) { + if (message == null) { + return null; + } + return new CamelMessage<>(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessaging.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessaging.java new file mode 100644 index 0000000000..14b0a7db36 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessaging.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.camel; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.camel.CamelContext; +import org.apache.camel.ConsumerTemplate; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.impl.DefaultExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.springframework.stereotype.Component; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; + +/** + * @author Marcin Grzejszczak + */ +@Component +public class ContractVerifierCamelMessaging implements + ContractVerifierMessaging { + + private static final Logger log = LoggerFactory.getLogger( + ContractVerifierCamelMessaging.class); + + private final CamelContext context; + private final ContractVerifierMessageBuilder builder; + + @Autowired + @SuppressWarnings("unchecked") + public ContractVerifierCamelMessaging(CamelContext context, ContractVerifierMessageBuilder contractVerifierMessageBuilder) { + this.context = context; + this.builder = contractVerifierMessageBuilder; + } + + @Override + public void send(ContractVerifierMessage message, String destination) { + try { + ProducerTemplate producerTemplate = context.createProducerTemplate(); + Exchange exchange = new DefaultExchange(context); + exchange.setIn(message.convert()); + producerTemplate.send(destination, exchange); + } catch (Exception e) { + log.error("Exception occurred while trying to send a message [" + message + "] " + + "to a channel with name [" + destination + "]", e); + throw e; + } + } + + @Override + @SuppressWarnings("unchecked") + public void send(T payload, Map headers, String destination) { + send(builder.create(payload, headers), destination); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + try { + ConsumerTemplate consumerTemplate = context.createConsumerTemplate(); + Exchange exchange = consumerTemplate.receive(destination, timeUnit.toMillis(timeout)); + return builder.create(exchange.getIn()); + } catch (Exception e) { + log.error("Exception occurred while trying to read a message from " + + " a channel with name [" + destination + "]", e); + throw new RuntimeException(e); + } + } + + @Override + public ContractVerifierMessage receiveMessage(String destination) { + return receiveMessage(destination, 5, TimeUnit.SECONDS); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage create(T t, Map headers) { + return builder.create(t, headers); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage create(Message message) { + return builder.create(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..4515c7bbdb --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-camel/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/build.gradle new file mode 100644 index 0000000000..e785784138 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/build.gradle @@ -0,0 +1,23 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + compile "org.springframework:spring-messaging:${springVersion}" + compile 'org.slf4j:slf4j-api:1.6.0' +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationConfiguration.java new file mode 100644 index 0000000000..3f1ddc2c97 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.integration; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierIntegrationConfiguration { + + @Bean ContractVerifierMessaging contractVerifierMessaging(ApplicationContext applicationContext, ContractVerifierMessageBuilder contractVerifierMessageBuilder) { + return new ContractVerifierIntegrationMessaging(applicationContext, contractVerifierMessageBuilder); + } + + @Bean ContractVerifierMessageBuilder contractVerifierMessageBuilder() { + return new ContractVerifierIntegrationMessageBuilder(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessageBuilder.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessageBuilder.java new file mode 100644 index 0000000000..60f10b1493 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessageBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.integration; + +import java.util.Map; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierIntegrationMessageBuilder implements + ContractVerifierMessageBuilder> { + + @Override + public ContractVerifierMessage> create(T payload, Map headers) { + return new IntegrationMessage<>(MessageBuilder.createMessage(payload, new MessageHeaders(headers))); + } + + @Override + public ContractVerifierMessage> create(Message message) { + if (message == null) { + return null; + } + return new IntegrationMessage<>(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessaging.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessaging.java new file mode 100644 index 0000000000..960e07d520 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/ContractVerifierIntegrationMessaging.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.integration; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.PollableChannel; +import org.springframework.stereotype.Component; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; + +/** + * @author Marcin Grzejszczak + */ +@Component +public class ContractVerifierIntegrationMessaging implements + ContractVerifierMessaging> { + + private static final Logger log = LoggerFactory.getLogger( + ContractVerifierIntegrationMessaging.class); + + private final ApplicationContext context; + private final ContractVerifierMessageBuilder builder; + + @Autowired + @SuppressWarnings("unchecked") + public ContractVerifierIntegrationMessaging(ApplicationContext context, ContractVerifierMessageBuilder contractVerifierMessageBuilder) { + this.context = context; + this.builder = contractVerifierMessageBuilder; + } + + @Override + @SuppressWarnings("unchecked") + public void send(T payload, Map headers, String destination) { + send(builder.create(payload, headers), destination); + } + + @Override + public void send(ContractVerifierMessage> message, String destination) { + try { + MessageChannel messageChannel = context.getBean(destination, MessageChannel.class); + messageChannel.send(message.convert()); + } catch (Exception e) { + log.error("Exception occurred while trying to send a message [" + message + "] " + + "to a channel with name [" + destination + "]", e); + throw e; + } + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + try { + PollableChannel messageChannel = context.getBean(destination, PollableChannel.class); + return builder.create(messageChannel.receive(timeUnit.toMillis(timeout))); + } catch (Exception e) { + log.error("Exception occurred while trying to read a message from " + + " a channel with name [" + destination + "]", e); + throw new RuntimeException(e); + } + } + + @Override + public ContractVerifierMessage> receiveMessage(String destination) { + return receiveMessage(destination, 5, TimeUnit.SECONDS); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> create(T t, Map headers) { + return builder.create(t, headers); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> create(Message message) { + return builder.create(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/IntegrationMessage.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/IntegrationMessage.java new file mode 100644 index 0000000000..b75e92eb69 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/java/org/springframework/cloud/contract/verifier/messaging/integration/IntegrationMessage.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.integration; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; + +/** + * @author Marcin Grzejszczak + */ +public class IntegrationMessage implements ContractVerifierMessage> { + + private final Message delegate; + + public IntegrationMessage(Message delegate) { + this.delegate = delegate; + } + + @Override + public T getPayload() { + return delegate.getPayload(); + } + + @Override + public MessageHeaders getHeaders() { + return delegate.getHeaders(); + } + + @Override + public Object getHeader(String key) { + return getHeaders().get(key); + } + + @Override + public Message convert() { + return delegate; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..2251d19bd0 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-integration/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/build.gradle new file mode 100644 index 0000000000..068826284e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/build.gradle @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +repositories { + jcenter() +} + +dependencies { + compile 'com.fasterxml.jackson.core:jackson-databind:2.7.0' + compile 'javax.inject:javax.inject:1' +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierFilter.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierFilter.java new file mode 100644 index 0000000000..3c864f1bee --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierFilter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +/** + * Contract for filtering out messages that do not match the structure in the Contract DSL + * + * @author Marcin Grzejszczak + */ +public interface ContractVerifierFilter { + + /** + * @return @{code true} if the message should be passed through, @{code false} if the message should be filtered out, + */ + boolean matches(ContractVerifierMessage message); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessage.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessage.java new file mode 100644 index 0000000000..c8510cd526 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessage.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +import java.util.Map; + +/** + * Describes a message. Contains payload and headers. A message can be converted + * to another type (e.g. Spring Messaging Message) + * + * @author Marcin Grzejszczak + */ +public interface ContractVerifierMessage { + + /** + * Returns a payload of type {@code PAYLOAD} + */ + PAYLOAD getPayload(); + + /** + * Returns a map of headers + */ + Map getHeaders(); + + /** + * Returns a header for a given key + */ + Object getHeader(String key); + + /** + * Converts the message to {@code TYPE_TO_CONVERT_INTO} type + */ + TYPE_TO_CONVERT_INTO convert(); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessageBuilder.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessageBuilder.java new file mode 100644 index 0000000000..7325ac29de --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessageBuilder.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +import java.util.Map; + +/** + * Contract for creation of (@link ContractVerifierMessage}. You can create a message from + * payload and headers or from some type (e.g. Spring Messaging Message). + * + * @author Marcin Grzejszczak + */ +public interface ContractVerifierMessageBuilder { + + /** + * Creates a {@link ContractVerifierMessage} from payload and headers + */ + ContractVerifierMessage create(PAYLOAD payload, Map headers); + + /** + * Creates a {@link ContractVerifierMessage} from the {@code TYPE_TO_CONVERT_INTO} type + */ + ContractVerifierMessage create(TYPE_TO_CONVERT_INTO typeToConvertInto); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessaging.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessaging.java new file mode 100644 index 0000000000..01c377aa8f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessaging.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Core interface that allows you to build, send and receive messages. + * + * Destination is relevant to the underlaying implementation. Might be a channel, queue, topic etc. + * + * @author Marcin Grzejszczak + */ +public interface ContractVerifierMessaging extends + ContractVerifierMessageBuilder { + /** + * Sends the {@link ContractVerifierMessage} to the given destination. + */ + void send(ContractVerifierMessage message, String destination); + + /** + * Sends the given payload with headers, to the given destination. + */ + void send(PAYLOAD payload, Map headers, String destination); + + /** + * Receives the {@link ContractVerifierMessage} from the given destination. You can provide the timeout + * for receiving that message. + */ + ContractVerifierMessage receiveMessage(String destination, long timeout, TimeUnit timeUnit); + + /** + * Receives the {@link ContractVerifierMessage} from the given destination. A default timeout will be applied. + */ + ContractVerifierMessage receiveMessage(String destination); +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessagingUtil.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessagingUtil.java new file mode 100644 index 0000000000..18de368aa5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierMessagingUtil.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Helper class that allows to build headers in a nice way + * + * @author Marcin Grzejszczak + */ +public class ContractVerifierMessagingUtil { + + public static ContractVerifierHeaders headers() { + return new ContractVerifierHeaders(); + } + + public static class ContractVerifierHeaders implements Map { + + private final Map delegate = new HashMap<>(); + + public ContractVerifierHeaders header(String key, Object value) { + put(key, value); + return this; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public Object get(Object key) { + return delegate.get(key); + } + + @Override + public Object put(String key, Object value) { + return delegate.put(key, value); + } + + @Override + public Object remove(Object key) { + return delegate.remove(key); + } + + @Override + public void putAll(Map m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierObjectMapper.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierObjectMapper.java new file mode 100644 index 0000000000..dfbdcf461d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/ContractVerifierObjectMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Wrapper over {@link ObjectMapper} that won't try to parse + * String but will directly return it. + * + * @author Marcin Grzejszczak + */ +public class ContractVerifierObjectMapper { + + private final ObjectMapper objectMapper; + + public ContractVerifierObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public ContractVerifierObjectMapper() { + this.objectMapper = new ObjectMapper(); + } + + public String writeValueAsString(Object payload) throws JsonProcessingException { + if (payload instanceof String) { + return payload.toString(); + } + return objectMapper.writeValueAsString(payload); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessage.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessage.java new file mode 100644 index 0000000000..c7c37514d5 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessage.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.noop; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; + +import java.util.Map; + +/** + * @author Marcin Grzejszczak + */ +public class NoOpContractVerifierMessage implements ContractVerifierMessage { + @Override + public Object getPayload() { + return null; + } + + @Override + public Map getHeaders() { + return null; + } + + @Override + public Object getHeader(String key) { + return null; + } + + @Override + public Object convert() { + return null; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessageBuilder.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessageBuilder.java new file mode 100644 index 0000000000..edd9a3142c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessageBuilder.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.noop; + +import java.util.Map; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; + +/** + * @author Marcin Grzejszczak + */ +public class NoOpContractVerifierMessageBuilder + implements ContractVerifierMessageBuilder { + @Override + public ContractVerifierMessage create(Object o, Map headers) { + return new NoOpContractVerifierMessage(); + } + + @Override + public ContractVerifierMessage create(Object o) { + return new NoOpContractVerifierMessage(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessaging.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessaging.java new file mode 100644 index 0000000000..da28a92d56 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-messaging-core/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierMessaging.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.noop; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; + +/** + * @author Marcin Grzejszczak + */ +public class NoOpContractVerifierMessaging implements ContractVerifierMessaging { + @Override + public void send(ContractVerifierMessage message, String destination) { + + } + + @Override + public void send(Object payload, Map headers, String destination) { + + } + + @Override + public ContractVerifierMessage receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + return null; + } + + @Override + public ContractVerifierMessage receiveMessage(String destination) { + return null; + } + + @Override + public ContractVerifierMessage create(Object o, Map headers) { + return null; + } + + @Override + public ContractVerifierMessage create(Object o) { + return null; + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/build.gradle b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/build.gradle new file mode 100644 index 0000000000..bfaed27076 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/build.gradle @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +String verifier = "spring-cloud-contract-verifier" + +dependencies { + compile project(":$verifier-root:$verifier-messaging-root:$verifier-messaging-core") + compile "org.springframework.cloud:spring-cloud-stream:${springStreamVersion}" + // for MessageCollector + compile "org.springframework.cloud:spring-cloud-stream-test-support:${springStreamVersion}" +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamAutoConfiguration.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamAutoConfiguration.java new file mode 100644 index 0000000000..00d445917d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamAutoConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.stream; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Marcin Grzejszczak + */ +@Configuration +public class ContractVerifierStreamAutoConfiguration { + + @Bean ContractVerifierMessaging contractVerifierMessaging(ApplicationContext applicationContext, ContractVerifierMessageBuilder contractVerifierMessageBuilder) { + return new ContractVerifierStreamMessaging(applicationContext, contractVerifierMessageBuilder); + } + + @Bean ContractVerifierMessageBuilder contractVerifierMessageBuilder() { + return new ContractVerifierStreamMessageBuilder(); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessageBuilder.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessageBuilder.java new file mode 100644 index 0000000000..90270657b4 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessageBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.stream; + +import java.util.Map; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierStreamMessageBuilder implements + ContractVerifierMessageBuilder> { + + @Override + public ContractVerifierMessage> create(T payload, Map headers) { + return new StreamMessage<>(MessageBuilder.createMessage(payload, new MessageHeaders(headers))); + } + + @Override + public ContractVerifierMessage> create(Message message) { + if (message == null) { + return null; + } + return new StreamMessage<>(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessaging.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessaging.java new file mode 100644 index 0000000000..3b885a4801 --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/ContractVerifierStreamMessaging.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.stream; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessageBuilder; +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessaging; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.config.BindingProperties; +import org.springframework.cloud.stream.config.ChannelBindingServiceProperties; +import org.springframework.cloud.stream.test.binder.MessageCollector; +import org.springframework.context.ApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author Marcin Grzejszczak + */ +public class ContractVerifierStreamMessaging implements + ContractVerifierMessaging> { + + private static final Logger log = LoggerFactory.getLogger(ContractVerifierStreamMessaging.class); + + private final ApplicationContext context; + private final MessageCollector messageCollector; + private final ContractVerifierMessageBuilder builder; + + @Autowired + @SuppressWarnings("unchecked") + public ContractVerifierStreamMessaging(ApplicationContext context, ContractVerifierMessageBuilder builder) { + this.context = context; + this.messageCollector = context.getBean(MessageCollector.class); + this.builder = builder; + } + + @Override + @SuppressWarnings("unchecked") + public void send(T payload, Map headers, String destination) { + send(builder.create(payload, headers), destination); + } + + @Override + public void send(ContractVerifierMessage> message, String destination) { + try { + MessageChannel messageChannel = context.getBean(resolvedDestination(destination), MessageChannel.class); + messageChannel.send(message.convert()); + } catch (Exception e) { + log.error("Exception occurred while trying to send a message [" + message + "] " + + "to a channel with name [" + destination + "]", e); + throw e; + } + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> receiveMessage(String destination, long timeout, TimeUnit timeUnit) { + try { + MessageChannel messageChannel = context.getBean(resolvedDestination(destination), MessageChannel.class); + return builder.create(messageCollector.forChannel(messageChannel).poll(timeout, timeUnit)); + } catch (Exception e) { + log.error("Exception occurred while trying to read a message from " + + " a channel with name [" + destination + "]", e); + throw new RuntimeException(e); + } + } + + private String resolvedDestination(String destination) { + ChannelBindingServiceProperties channelBindingServiceProperties = context.getBean(ChannelBindingServiceProperties.class); + String resolvedDestination = destination; + for (Map.Entry entry : channelBindingServiceProperties.getBindings().entrySet()) { + if (entry.getValue().getDestination().equals(destination)) { + log.debug("Found a channel named [{}] with destination [{}]", entry.getKey(), destination); + return entry.getKey(); + } + } + log.debug("No destination named [{}] was found. Assuming that the destination equals the channel name", destination); + return resolvedDestination; + } + + @Override + public ContractVerifierMessage> receiveMessage(String destination) { + return receiveMessage(destination, 5, TimeUnit.SECONDS); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> create(T t, Map headers) { + return builder.create(t, headers); + } + + @Override + @SuppressWarnings("unchecked") + public ContractVerifierMessage> create(Message message) { + return builder.create(message); + } +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamMessage.java b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamMessage.java new file mode 100644 index 0000000000..7582daf46a --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamMessage.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.stream; + +import org.springframework.cloud.contract.verifier.messaging.ContractVerifierMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; + +/** + * @author Marcin Grzejszczak + */ +public class StreamMessage implements ContractVerifierMessage> { + + private final Message delegate; + + public StreamMessage(Message delegate) { + this.delegate = delegate; + } + + @Override + public T getPayload() { + return delegate.getPayload(); + } + + @Override + public MessageHeaders getHeaders() { + return delegate.getHeaders(); + } + + @Override + public Object getHeader(String key) { + return getHeaders().get(key); + } + + @Override + public Message convert() { + return delegate; + } + +} diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..6b6e2b930d --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-messaging/spring-cloud-contract-verifier-stream/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-testing-utils/src/main/groovy/org/springframework/cloud/contract/verifier/util/AssertionUtil.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-testing-utils/src/main/groovy/org/springframework/cloud/contract/verifier/util/AssertionUtil.groovy new file mode 100644 index 0000000000..8b23a3473c --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-testing-utils/src/main/groovy/org/springframework/cloud/contract/verifier/util/AssertionUtil.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util + +import org.skyscreamer.jsonassert.JSONAssert + +class AssertionUtil { + + private static boolean NON_STRICT = false + + public static void assertThatJsonsAreEqual(String expected, String actual) { + JSONAssert.assertEquals(expected, actual, NON_STRICT) + } +} From 84f83051235238c34abf322c99f59036b61f02a9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 8 Jul 2016 13:07:59 +0200 Subject: [PATCH 10/12] Added missing jars --- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 49519 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53639 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/wrapper/maven-wrapper.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.jar diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c6feb8bb6f76f2553e266ff8bf8867105154237e GIT binary patch literal 49519 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%0A$pwhjXzyA0ZzErH{1@M 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 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2c6137b87896c8f70315ae454e00a969ef5f6019 GIT binary patch literal 53639 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 F{{tUq7iIte literal 0 HcmV?d00001 From 9a8c40f9e0773e59eb670eed995f40fbabd46254 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 8 Jul 2016 13:20:15 +0200 Subject: [PATCH 11/12] Added missing jars --- .../streamService-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1762 bytes ...raudDetectionServer-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1897 bytes .../loanIssuance-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 639 bytes .../camelService-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1773 bytes .../integrationService-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1762 bytes .../streamService-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1769 bytes ...raudDetectionServer-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1897 bytes .../loanIssuance-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 639 bytes ...raudDetectionServer-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 1897 bytes .../loanIssuance-0.0.1-SNAPSHOT-stubs.jar | Bin 0 -> 639 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51018 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53639 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 50514 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 50514 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 50514 bytes 15 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-integration/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/integrationService/0.0.1-SNAPSHOT/integrationService-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT-stubs.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle/wrapper/gradle-wrapper.jar create mode 100755 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/messagingProject/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleProject/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle/wrapper/gradle-wrapper.jar diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-boot/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..a84e468496c7d48104dde102f229a12e49cb1bec GIT binary patch literal 1762 zcmWIWW@h1HVBp|j&|{wF!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9W7$;qWfsl_EwJs?>Q2Aj^i9!u4p-QN!6{Rd(V1{th+lJfJj zgHlUMi}F%a4E55B^7G3oLqj+jm=8ZG4&MvJr4`%^j4a<785meVCY$yh@>9NA|eAD=Q{xc6`6Mc4J@eSr2QMl>d3wcMbPUpDr-_4)2|a&k0u_ckej+y3cLy z^%UFHx7d41>tdG0ZPl>vd=b(*m&MI~ldkrnZFiiMq^x`FiXQGXWRv{*aIWK%?UAmv z@ebRXLY0=eTzGLftnr+t@$pvY*YBqs?04}ES>nv`K>2dS?w0)Eos(ZSYfKGh2&`Ik z=|o=MpLQO;>#r+|Z;Go&cys*9@%#Ac3o9rRApzqG44BW0PP7~Vdd36jD`Eo12svOb z0t4j~X27)e+HxN<5NLb;CA?%e*R6vWeO(1zV(+-9Y5_`{V zQ;b)!;eMn%s)bfEpk=(=#Dap%y!2vl#$#j>Va8pa0D}<-1Q^~rf@pZ@65x%h4ZU;$ zDF%Thjha9bt_`WIK{o-U8di{jiWCSCVEF4;24o_Y8u&DV{0RXdjSGQHB#rRm24sR9 zGbC4G7B>=L1ArNfp`r0A4im5!KCFQ}aq3i`_uh=w%h>=Y@uFGBCR?

Ai(h6<{MwahD zlUYEPO`YKDf7pPh?fqZTiLE9AOJ5&*uI6jNTlgTP_uZB^0#VLZo0gcoT$1^!Uj3xi zi^3-YC+prG|F)Z-qmgTW_7%1hGv4Ip7?-NXu>9@sx>|Y6@ORS(h1?4TQ>#rQmmGhy z`&2onF`M?vx!ZC)*ELFVUsxaI8GW$!`OQxs|7Z$zoqXwb_0oisEj@R3M>M|AJ`~I_ zQzKv&iz%1;3*NTt=G)7^^Do}5`^e1o3HeZ_Sr<~2P( zH|KW#zDb8>96DXIt74fzkatY-vuQJwH#hIS^(%df(w-krM5KNk-@82KnLgJnc~DA1 zM2gvpoP`p=ppybdiXAzT;+J1SY`mQHJ8U4(^1hZUyEQ~W(f4%yQm%mf0)?i6tSW<( ziv-!GI;H&mEnmgsMH^J#SL~lVH|>DLl^@B0(h0%WF3i^2CiaeX@%mY}ZL|%3 zweJikKn{gkJOx;e1dsn^|PRW04*WXbl^_R{e!^wjnA)jN6SJhz{&mbWg`>|>$aKwmL3i7?|X+JN2z0s)4% zjvyLd(4lKXFX%vuL10OvCXj?{Ln`*rO#rDzE>l5#h{a_<8eW9q(*`Q*AOPgv`9LOI z8}^b9WSAT?B2rOHJ_)c{z&yv$(0GrqVff2IR!BLBRtBOw05z8*JD?aCHUxZw=>W7m lPn;W2Q#P_2m|2K)16n%AWiKlmD1_O8unOqAn?TbU7yzYKYMcN7 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-junit/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..e322590636a2642f1814bfec0720ea78f93fc4ec GIT binary patch literal 639 zcmWIWW@h1HVBp|jSnAa2!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9tz+^=Y@uFGB8Ii z

Ai(h6<{Mwai43=AwF(^fwV3JB2h($(?R)e8*T5VV5rQ=`&SS4)L?lj`D<@>Wga zb1^XAs-kSqs`Kuh)FKv;AW7$;qWfsl_EwJs?>QhW@s@9wxibJlF-~F*7nSXfViN)svK; zpBvJATF)oW?*Fb4m5%VWU^`BL9RmvJPzmY zhEIt#a%7L&yRu@E<%GLSqm$>}mh!ZAN%@~={Z-=LY3~l-cXD^4ic?J=Ox|&N_B^+_ z<|($TkFodo?)&fG{3>9d@Ws_q)to(bDb*`Gcb%3Jx_ImO3uW22tNA$%Uqv9BgE&CSnylcK)lNsXbYjuR-o>A}7;2#!KrDw)<23fKS zES1*kee>*G{)vR}X~(Z-M$05q?e~OY1?w~+fSxU77_e-*EeVnPt$~_ z=fB7E1xdc{%L)}!QHhy2%OZQ@`Nup{6(>q|D|U{`+vM>7CMuE@nY7on7Z`u>5BRSx%}3(lGhcA6y9b> zK6O2^bwVt6jm)Qq$Ib1TzHcxu%d&2%^=dX^mw6|#V*B!X{S|e->c9V{ZJM1wJ@4+$ z`yU#$%j?s@ zOKx*}$Q=lBU9*OTTkz%+Gy7+H(s}+aD(~N^I&QgYA%T_$a}x^+GV{`l!FiC8NrV}9(EjkZM?&1}a@3K!D+|V;PW%RDj^q2=XTcfHW=y zGLbaGOCFF3a?Ft2g<0}QfDHg2}* zB4-0_1LbTywqfQ8WZT4W7$;qWfsl_EwJs?>QhBa+>J(j9HyT2XC`wzq#3^G{tB<1I4 z2c?#j7UiX;80w`L<>!}GhK6u5Fh6-v9DWywODnh;7+JnEGBB`!Og8O1$aTno$Km|l z@F}rIj_h%JS5{2Y?D&3h?Z&>`vmVwiDgX1V?;7rzK3!n;9o{<;pA)V=?%r|sb)VbZ z>nXOYZ?X53*2OG~+p1yT`68rsE{mJ}CSC1C+wM3iNm=*U6+PT($R_#q;atZj+aq0T z;~lm&g(@v`x$xp}SmQZOm;uw;Ys-DeK%njUm++F^T(=Hh^mP?k# zet+CKX4$f*-FoMBMB_SNo8+(1zBFe`>dff$_-({|qeZ_= z+B{-PZb!c7ZasEFXu6M9`?jkKHJWRK2OY!eAM3I6sa)VKy#IX zkU`K^-a`g$|AeQ>Pqcrv#yR&_QHJD|GJno5ejRD`FR$vq$=M#=zwPqbLz8QN_14-j zB0^{dFoc%$XGyIBhR{bKR>PBYTvBsVOHxyi!{-h#Y_4L4k8kg3V8Rh_z5X&hWx9xp z{DUCZn4Ll~9Vbj?+GSkQ^z7>Ra^H9HqVC8$$LDzO3rzfS_reu9nR_R{dqvOcxn$zP zZ?#Nf>#e$}&2m3CCx~fniILbM@i9#B>!O6a27ZN}GY<dGae(82s7^T1Q?7!Ai(g}5k$jFmjG{6ZRn*7 zNHGX3Y19OgaBWCs4Y~;+)v$sLRHQ(F0K;F$G9VMF)WD|^UWKufW?7uhyMjt1HW%F%di z!^{uJwh1ufwhcXZ0POKGUR#6&9_ literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-messaging/spring-cloud-contract-stub-runner-stream/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/streamService/0.0.1-SNAPSHOT/streamService-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..b3daab26c4f8c8050b86f1d46fad15f2afdd17d3 GIT binary patch literal 1769 zcmWIWW@h1HVBp|j&|{wF!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9W7$;qWfsl_EwJs?>QhVyNAJuY)U(B28;F*7nSXfViN)svK; zpBh)XND85mi<1C3w-nQYp3kn4~EkHh)9 z;ZtIb9NFXcT7Fas7I?onI$8GitchhEC;r_nJCj_YudisfQ+nstXD71`m={hrxAnEk zKT#Fd%P%qe&zJdZR~P}~0fXry5Ub%yK`yB|sU@i?$blpU3?OdIK=SQ94GJXJ>o3Do zri-Y^KL~P-*(ns$al&M#UB)F%&#sOy_k9;H>W;i~e2(|Nz{D?iFIO+sAHxK{E=ss-;8*B5^RU2zgL~E0?NtN+{o$S2 zzDMQkjZalRm$UV<7O^B=-~Wj5-~#@cCvWL2;dtQIopki*?UPR|Wm_8T=0$Y`>D)1H ziM?mGDaNbVa6eL3)j}&E(DGkyVnIP>UV1S&|1mO&Fyk&(fWZg^0t{~*K{ULO3GhbM zhF-{k6obH$Mol0I*M?N&pql_v4J*$;B?|-yF#L5a12T~c4}2Oy{)7OK#)Uv8l16x` z12RF58IrXyOC1TY0l-|w(9n1lhY8rrAXZ2jgjW6_+lHBuk!||`v=p0rk!?d{YoKkQ zY>me@%>00Cn>aIW+t70d&@NE!z+x9H?O;oB;D7|CoxhHXEGTxt(_er$D;r1^2N2c) LV?PwAj)4IHo`NA~ literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/fraudDetectionServer/0.0.1-SNAPSHOT/fraudDetectionServer-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..fdb70e383f00ec1a31fd06d81aa618347b8aa5b3 GIT binary patch literal 1897 zcmWIWW@h1HVBp|jSmxB}!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9Q}aq3i`_uh=w%h>=Y@uFGBCR?

Ai(h6<{MwahD zlUYEPO`YKDf7pPh?fqZTiLE9AOJ5&*uI6jNTlgTP_uZB^0#VLZo0gcoT$1^!Uj3xi zi^3-YC+prG|F)Z-qmgTW_7%1hGv4Ip7?-NXu>9@sx>|Y6@ORS(h1?4TQ>#rQmmGhy z`&2onF`M?vx!ZC)*ELFVUsxaI8GW$!`OQxs|7Z$zoqXwb_0oisEj@R3M>M|AJ`~I_ zQzKv&iz%1;3*NTt=G)7^^Do}5`^e1o3HeZ_Sr<~2P( zH|KW#zDb8>96DXIt74fzkatY-vuQJwH#hIS^(%df(w-krM5KNk-@82KnLgJnc~DA1 zM2gvpoP`p=ppybdiXAzT;+J1SY`mQHJ8U4(^1hZUyEQ~W(f4%yQm%mf0)?i6tSW<( ziv-!GI;H&mEnmgsMH^J#SL~lVH|>DLl^@B0(h0%WF3i^2CiaeX@%mY}ZL|%3 zweJikKn{gkJOx;e1dsn^|PRW04*WXbl^_R{e!^wjnA)jN6SJhz{&mbWg`>|>$aKwmL3i7?|X+JN2z0s)4% zjvyLd(4lKXFX%vuL10OvCXj?{Ln`*rO#rDzE>l5#h{a_<8eW9q(*`Q*AOPgv`9LOI z8}^b9WSAT?B2rOHJ_)c{z&yv$(0GrqVff2IR!BLBRtBOw05z8*JD?aCHUxZw=>W7m lPn;W2Q#P_2m|2K)16n%AWiKlmD1_O8unOqAn?TbU7yzYKYMcN7 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring-cloud/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..e322590636a2642f1814bfec0720ea78f93fc4ec GIT binary patch literal 639 zcmWIWW@h1HVBp|jSnAa2!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9tz+^=Y@uFGB8Ii z

Ai(h6<{Mwai43=AwF(^fwV3JB2h($(?R)e8*T5VV5rQ=`&SS4)L?lj`D<@>Wga zb1^XAs-kSqs`Kuh)FKv;AQ}aq3i`_uh=w%h>=Y@uFGBCR?

Ai(h6<{MwahD zlUYEPO`YKDf7pPh?fqZTiLE9AOJ5&*uI6jNTlgTP_uZB^0#VLZo0gcoT$1^!Uj3xi zi^3-YC+prG|F)Z-qmgTW_7%1hGv4Ip7?-NXu>9@sx>|Y6@ORS(h1?4TQ>#rQmmGhy z`&2onF`M?vx!ZC)*ELFVUsxaI8GW$!`OQxs|7Z$zoqXwb_0oisEj@R3M>M|AJ`~I_ zQzKv&iz%1;3*NTt=G)7^^Do}5`^e1o3HeZ_Sr<~2P( zH|KW#zDb8>96DXIt74fzkatY-vuQJwH#hIS^(%df(w-krM5KNk-@82KnLgJnc~DA1 zM2gvpoP`p=ppybdiXAzT;+J1SY`mQHJ8U4(^1hZUyEQ~W(f4%yQm%mf0)?i6tSW<( ziv-!GI;H&mEnmgsMH^J#SL~lVH|>DLl^@B0(h0%WF3i^2CiaeX@%mY}ZL|%3 zweJikKn{gkJOx;e1dsn^|PRW04*WXbl^_R{e!^wjnA)jN6SJhz{&mbWg`>|>$aKwmL3i7?|X+JN2z0s)4% zjvyLd(4lKXFX%vuL10OvCXj?{Ln`*rO#rDzE>l5#h{a_<8eW9q(*`Q*AOPgv`9LOI z8}^b9WSAT?B2rOHJ_)c{z&yv$(0GrqVff2IR!BLBRtBOw05z8*JD?aCHUxZw=>W7m lPn;W2Q#P_2m|2K)16n%AWiKlmD1_O8unOqAn?TbU7yzYKYMcN7 literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-stub-runner/spring-cloud-contract-stub-runner-spring/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/loanIssuance/0.0.1-SNAPSHOT/loanIssuance-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000000000000000000000000000000000..e322590636a2642f1814bfec0720ea78f93fc4ec GIT binary patch literal 639 zcmWIWW@h1HVBp|jSnAa2!2kqIAOZ+Df!NnI#8KDN&rP41Apk|;rh2A#(m(~0KrDi+ z(AUw=)6F$FM9tz+^=Y@uFGB8Ii z

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 F{{tUq7iIte literal 0 HcmV?d00001 diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar b/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier/spring-cloud-contract-verifier-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..667288ad6c2b3b87c990ece1267e56f0bcbf3622 GIT binary patch literal 50514 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 Date: Fri, 8 Jul 2016 13:23:20 +0200 Subject: [PATCH 12/12] Added missing target --- .../target/stubs/contracts/Sample.groovy | 18 +++++++++++++++ .../target/stubs/mappings/Sample.json | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/contracts/Sample.groovy create mode 100644 spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/mappings/Sample.json diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/contracts/Sample.groovy b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/contracts/Sample.groovy new file mode 100644 index 0000000000..fb8ef5908e --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/contracts/Sample.groovy @@ -0,0 +1,18 @@ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url('/users') { + + } + headers { + header 'Content-Type': 'application/json' + } + body '''{ "login" : "john", "name": "John The Contract" }''' + } + response { + status 200 + headers { + header 'Location': '/users/john' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/mappings/Sample.json b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/mappings/Sample.json new file mode 100644 index 0000000000..6641a7d26f --- /dev/null +++ b/spring-cloud-contract-verifier/spring-cloud-contract-verifier-maven-plugin/spring-cloud-contract-verifier-maven-plugin/src/test/projects/generatedStubs/target/stubs/mappings/Sample.json @@ -0,0 +1,22 @@ +{ + "request" : { + "url" : "/users", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.login == 'john')]" + }, { + "matchesJsonPath" : "$[?(@.name == 'John The Contract')]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 200, + "headers" : { + "Location" : "/users/john" + } + } +} \ No newline at end of file