diff --git a/README.adoc b/README.adoc index 0e9d56353c..98e3fa145e 100644 --- a/README.adoc +++ b/README.adoc @@ -130,6 +130,7 @@ Assume that you want to send a request containing the ID of a client company and amount it wants to borrow from us. You also want to send it to the /fraudcheck url via the PUT method. +.Groovy DSL [source,groovy,indent=0] ---- package contracts @@ -191,6 +192,63 @@ From the Producer perspective, in the autogenerated producer-side test: */ ---- +.YAML +[source,yml,indent=0] +---- +request: # (1) + method: PUT # (2) + url: /fraudcheck # (3) + body: # (4) + "client.id": 1234567890 + loanAmount: 99999 + headers: # (5) + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] # (6) + type: by_regex + value: "[0-9]{10}" +response: # (7) + status: 200 # (8) + body: # (9) + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: # (10) + Content-Type: application/json;charset=UTF-8 + + +#From the Consumer perspective, when shooting a request in the integration test: +# +#(1) - If the consumer sends a request +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}` +#(7) - then the response will be sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json` +# +#From the Producer perspective, in the autogenerated producer-side test: +# +#(1) - A request will be sent to the producer +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` `1234567890` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(7) - then the test will assert if the response has been sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8` +---- + ==== Client Side Spring Cloud Contract generates stubs, which you can use during client-side testing. @@ -231,7 +289,7 @@ your application behaves in a different way, especially in production. To ensure that your application behaves the way you define in your stub, tests are generated from the stub you provide. -The autogenerated test looks like this: +The autogenerated test looks, more or less, like this: [source,java,indent=0] ---- @@ -418,7 +476,7 @@ clone it. [source,bash,indent=0] ---- -git clone https://your-git-server.com/server-side.git local-http-server-repo +$ git clone https://your-git-server.com/server-side.git local-http-server-repo ---- *Define the contract locally in the repo of Fraud Detection service.* @@ -429,6 +487,7 @@ your expectations. To do so, write the following contract: IMPORTANT: Place the contract under `src/test/resources/contracts/fraud` folder. The `fraud` folder is important because the producer's test base class name references that folder. +.Groovy DSL [source,groovy,indent=0] ---- package contracts @@ -490,8 +549,66 @@ From the Producer perspective, in the autogenerated producer-side test: */ ---- -The Contract is written using a statically typed Groovy DSL. You might wonder what about -those `value(client(...), server(...))` parts. By using this notation, Spring Cloud +.YAML +[source,yml,indent=0] +---- +request: # (1) + method: PUT # (2) + url: /fraudcheck # (3) + body: # (4) + "client.id": 1234567890 + loanAmount: 99999 + headers: # (5) + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] # (6) + type: by_regex + value: "[0-9]{10}" +response: # (7) + status: 200 # (8) + body: # (9) + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: # (10) + Content-Type: application/json;charset=UTF-8 + + +#From the Consumer perspective, when shooting a request in the integration test: +# +#(1) - If the consumer sends a request +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}` +#(7) - then the response will be sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json` +# +#From the Producer perspective, in the autogenerated producer-side test: +# +#(1) - A request will be sent to the producer +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` `1234567890` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(7) - then the test will assert if the response has been sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8` +---- + +The YML contract is quite straight-forward. However when you take a look at the Contract +written using a statically typed Groovy DSL - you might wonder what the +`value(client(...), server(...))` parts are. By using this notation, Spring Cloud Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case of an identifier or a timestamp, you need not hardcode a value. You want to allow some different ranges of values. To enable ranges of values, you can set regular expressions @@ -565,8 +682,8 @@ stubs. You need to skip the test generation and execution. When you execute: [source,bash,indent=0] ---- -cd local-http-server-repo -./mvnw clean install -DskipTests +$ cd local-http-server-repo +$ ./mvnw clean install -DskipTests ---- In the logs, you see something like this: @@ -691,8 +808,8 @@ return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON); [source,bash,indent=0] ---- -git checkout -b contract-change-pr master -git pull https://your-git-server.com/server-side-fork.git contract-change-pr +$ git checkout -b contract-change-pr master +$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr ---- You must add the dependencies needed by the autogenerated tests: @@ -802,8 +919,9 @@ public void validate_shouldMarkClientAsFraud() throws Exception { } ---- -As you can see, all the `producer()` parts of the Contract that were present in the +If you used the Groovy DSL, you can see, all the `producer()` parts of the Contract that were present in the `value(consumer(...), producer(...))` blocks got injected into the test. +In case of using YAML, the same applied for the `matchers` sections of the `response`. Note that, on the producer side, you are also doing TDD. The expectations are expressed in the form of a test. This test sends a request to our own application with the URL, @@ -837,9 +955,9 @@ Once you finish your work, you can deploy your change. First, merge the branch: [source,bash,indent=0] ---- -git checkout master -git merge --no-ff contract-change-pr -git push origin master +$ git checkout master +$ git merge --no-ff contract-change-pr +$ git push origin master ---- Your CI might run something like `./mvnw clean deploy`, which would publish both the @@ -853,8 +971,8 @@ As a developer of the Loan Issuance service (a consumer of the Fraud Detection s [source,bash,indent=0] ---- -git checkout master -git merge --no-ff contract-change-pr +$ git checkout master +$ git merge --no-ff contract-change-pr ---- *Work online.* @@ -1149,6 +1267,23 @@ Spring Boot embedded servers, and Wiremock itself has "native" support for a par version of Jetty (currently 9.2). To use the native Jetty, you need to add the native Wiremock dependencies and exclude the Spring Boot container (if there is one). +=== Customization of WireMock configuration + +You can register a bean of `org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer` type +in order to customize the WireMock configuration (e.g. add custom transformers). +Example: + +[source,java,indent=0] +---- + @Bean WireMockConfigurationCustomizer optionsCustomizer() { + return new WireMockConfigurationCustomizer() { + @Override public void customize(WireMockConfiguration options) { +// perform your customization here + } + }; + } +---- + === Generating Stubs using REST Docs https://projects.spring.io/spring-restdocs[Spring REST Docs] can be used to generate @@ -1300,12 +1435,13 @@ Consider the following test: .accept(MediaType.APPLICATION_PDF) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .content("{\"foo\": 23 }")) + .content("{\"foo\": 23, \"bar\" : \"baz\" }")) .andExpect(status().isOk()) .andExpect(content().string("bar")) // first WireMock .andDo(WireMockRestDocs.verify() .jsonPath("$[?(@.foo >= 20)]") + .jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]") .contentType(MediaType.valueOf("application/json")) .stub("shouldGrantABeerIfOldEnough")) // then Contract DSL documentation diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index 3233e7f23d..718a5bad2f 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -1,33 +1,36 @@ == Contract DSL -IMPORTANT: Remember that, inside the contract file, you have to provide the fully +Spring Cloud Contract supports out of the box 2 types of DSL. One written in +`Groovy` and one written in `YAML`. + +If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself. + +IMPORTANT: Remember that, inside the Groovy contract file, you have to provide the fully qualified name to the `Contract` class and `make` static imports, such as `org.springframework.cloud.spec.Contract.make { ... }`. You can also provide an import to the `Contract` class: `import org.springframework.cloud.spec.Contract` and then call `Contract.make { ... }`. -Contract DSL is written in Groovy, but do not be alarmed if you have not used Groovy -before. Knowledge of the language is not really needed, as the Contract DSL uses only a -tiny subset of it (only literals, method calls and closures). Also, the DSL is statically -typed, to make it programmer-readable without any knowledge of the DSL itself. - TIP: Spring Cloud Contract supports defining multiple contracts in a single file. -The Contract is present in the `spring-cloud-contract-spec` module of the -https://github.com/spring-cloud/spring-cloud-contract/tree/master/spring-cloud-contract-verifier[Spring -Cloud Contract Verifier repository]. - -The following is a complete example of a contract definition: +The following is a complete example of a Groovy 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] ---- -NOTE: The preceding example does not contain all the features of the DSL appear. The -remainder of this section describes the other features. +The following is a complete example of a YAML contract definition: -You can compile Contracts to WireMock stubs mapping using standalone maven command: +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0] +---- + +TIP: You can compile contracts to stubs mapping using standalone maven command: `mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert` === Limitations @@ -41,7 +44,7 @@ to turn it on, please set the value of the following system property to `true`: You can also provide the `assertJsonSize` property in the plugin configuration. WARNING: Because JSON structure can have any form, it can be impossible to parse it -properly when using the `value(consumer(...), producer(...))` notation in `GString`. That +properly when using the Groovy DSL and the `value(consumer(...), producer(...))` notation in `GString`. That is why you should use the Groovy Map notation. === Common Top-Level elements @@ -60,11 +63,18 @@ The following sections describe the most common top-level elements: You can add a `description` to your contract. The description is arbitrary text. The following code shows an example: +.Groovy DSL [source,groovy,indent=0] ---- include::{contract_spec_path}/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy[tags=description,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0] +---- + [[contract-dsl-name]] ==== Name @@ -78,17 +88,36 @@ generated test not compile. Also, remember that, if you provide the same name fo multiple contracts, your autogenerated tests fail to compile and your generated stubs override each other. +.Groovy DSL +[source,groovy,indent=0] +---- +include::{contract_spec_path}/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy[tags=name,indent=0] +---- + +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=name,indent=0] +---- + [[contract-dsl-ignoring-contracts]] ==== Ignoring Contracts If you want to ignore a contract, you can either set a value of ignored contracts in the plugin configuration or set the `ignored` property on the contract itself: +.Groovy DSL [source,groovy,indent=0] ---- include::{contract_spec_path}/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy[tags=ignored,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=ignored,indent=0] +---- + [[contract-dsl-passing-values-from-files]] ==== Passing Values from Files @@ -108,11 +137,18 @@ following resources in our project. Further assume that your contract is as follows: +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/resources/classpath/readFromFile.groovy[indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_from_file.yml[indent=0] +---- + Further assume that the JSON files is as follows: *request.json* @@ -128,9 +164,8 @@ include::{verifier_core_path}/src/test/resources/classpath/response.json[indent= ---- When test or stub generation takes place, the contents of the file is passed to the body -of a request or a response. That works because of the `file(...)` method. The argument of -that method needs to be a file with location relative to the folder in which the contract -lays. +of a request or a response. The name of the file needs to be a file with location +relative to the folder in which the contract lays. [[contract-dsl-http-top-level-elements]] ==== HTTP Top-Level Elements @@ -138,62 +173,124 @@ lays. The following methods can be called in the top-level closure of a contract definition. `request` and `response` are mandatory. `priority` is optional. +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=http_dsl,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=priority,indent=0] +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,indent=0] +... +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response,indent=0] +... +---- + +IMPORTANT: If you want to make your contract have a **higher** value of priority +you need to pass a **lower** number to the `priority` tag / method. E.g. `priority` with +value `5` has **higher** priority than `priority` with value `10`. + === Request -The HTTP protocol requires only **method and address** to be specified in a request. The +The HTTP protocol requires only **method and url** to be specified in a request. The same information is mandatory in request definition of the Contract. +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=request,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request_obligatory,indent=0] +---- + It is possible to specify an absolute rather than relative `url`, but using `urlPath` is the recommended way, as doing so makes the tests **host-independent**. +.Groovy DSL [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`. +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_rest_with_path.yml[tags=url_path,indent=0] +---- +`request` may contain **query parameters**. + +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=urlpath,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,indent=0] +... +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=query_params,indent=0] +---- + `request` may contain additional **request headers**, as shown in the following example: +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=headers,indent=0] ---- -`request` may contain a **request body**, as shown in the following example: +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,indent=0] +... +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=headers,indent=0] +---- +`request` may contain a **request body**: + +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=body,indent=0] ---- -`request` may contain **multipart** elements. To include multipart elements, call the -`multipart()` method, as shown in the following example +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,indent=0] +... +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=body,indent=0] +---- +`request` may contain **multipart** elements. To include multipart elements, use the +`multipart` method/section, as shown in the following examples + +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=multipartdsl,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_multipart.yml[indent=0] +---- + In the preceding example, we define parameters in either of two ways: +.Groovy DSL * Directly, by using the map notation, where the value can be a dynamic property (such as `formParameter: $(consumer(...), producer(...))`). * By using the `named(...)` method that lets you set a named parameter. A named parameter @@ -201,6 +298,20 @@ can set a `name` and `content`. You can call it either via a method with two ar such as `named("fileName", "fileContent")`, or via a map notation, such as `named(name: "fileName", content: "fileContent")`. +.YAML +* The multipart parameters are set via `multipart.params` section +* The named parameters (the `fileName` and `fileContent` for a given parameter name) +can be set via the `multipart.named` section. That section contains +the `paramName` (name of the parameter), `fileName` (name of the file), +`fileContent` (content of the file) fields +* The dynamic bits can be set via the `matchers.multipart` section +** for parameters use the `params` section that can accept +`regex` or a `predefined` regular expression +** for named params use the `named` section where first you +define the parameter name via `paramName` and then you can pass the +parametrization of either `fileName` or `fileContent` via +`regex` or a `predefined` regular expression + From this contract, the generated test is as follows: [source,java,indent=0] @@ -227,17 +338,26 @@ The WireMock stub is as follows: include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy[tags=multipartwiremock,indent=0] ---- - === Response The response must contain an **HTTP status code** and may contain other information. The following code shows an example: +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=response,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response,indent=0] +... +include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response_obligatory,indent=0] +---- + + Besides status, the response may contain **headers** and a **body**, both of which are specified the same way as in the request (see the previous paragraph). @@ -245,12 +365,19 @@ specified the same way as in the request (see the previous paragraph). The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not want to force the consumers to stub their clocks to always return the same value of time -so that it gets matched by the stub. You can provide the dynamic parts in your contracts +so that it gets matched by the stub. + +For Groovy DSL you can provide the dynamic parts in your contracts in two ways: pass them directly in the body or set them in separate sections called `testMatchers` and `stubMatchers`. +For YAML you can only use the `matchers` section. + ==== Dynamic properties inside the body +IMPORTANT: This section is valid only for Groovy DSL. Check out the +<> section for YAML examples of a similar feature. + You can set the properties inside the body either with the `value` method or, if you use the Groovy map notation, with `$()`. The following example shows how to set dynamic properties with the value method: @@ -278,6 +405,9 @@ method. Subsequent sections take a closer look at what you can do with those val ==== Regular expressions +IMPORTANT: This section is valid only for Groovy DSL. Check out the +<> section for YAML examples of a similar feature. + You can use regular expressions to write your requests in Contract DSL. Doing so 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 regular expressions when you @@ -319,6 +449,9 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract ==== Passing Optional Parameters +IMPORTANT: This section is valid only for Groovy DSL. Check out the +<> section for YAML examples of a similar feature. + It is possible to provide optional parameters in your contract. However, you can provide optional parameters only for the following: @@ -351,6 +484,9 @@ include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/spr ==== Executing Custom Methods on the Server Side +IMPORTANT: This section is valid only for Groovy DSL. Check out the +<> section for YAML examples of a similar feature. + You can define a method call that executes on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" in the configuration. The following code shows an example of the contract portion of the test case: @@ -414,7 +550,9 @@ It should resemble the following code: ==== Referencing the Request from the Response The best situation is to provide fixed values, but sometimes you need to reference a -request in your response. To do so, you can use the `fromRequest()` method, which lets +request in your response. + +If you're writing contracts using Groovy DSL, you can use the `fromRequest()` method, which lets you reference a bunch of elements from the HTTP request. You can use the following options: @@ -430,13 +568,36 @@ given name. * `fromRequest().body(String jsonPath)`: Returns the element from the request that matches the JSON Path. +If you're using the YAML contract definition you have to use the +http://handlebarsjs.com/[Handlebars] `{{{ }}}` notation with custom, Spring Cloud Contract + functions to achieve this. + +* `{{{ request.url }}}`: Returns the request URL and query parameters. +* `{{{ request.query.key.[index] }}}`: Returns the nth query parameter with a given name. +E.g. for key `foo`, first entry `{{{ request.query.foo.[0] }}}` +* `{{{ request.path }}}`: Returns the full path. +* `{{{ request.path.[index] }}}`: Returns the nth path element. E.g. +for first entry ```{{{ request.path.[0] }}} +* `{{{ request.headers.key }}}`: Returns the first header with a given name. +* `{{{ request.headers.key.[index] }}}`: Returns the nth header with a given name. +* `{{{ request.body }}}`: Returns the full request body. +* `{{{ jsonpath this 'your.json.path' }}}`: Returns the element from the request that +matches the JSON Path. E.g. for json path `$.foo` - `{{{ jsonpath this '$.foo' }}}` + Consider the following contract: +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=template_contract,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_reference_request.yml[indent=0] +---- + Running a JUnit test generation leads to a test that resembles the following example: [source,java,indent=0] @@ -561,6 +722,7 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract IMPORTANT: Remember to override the `applyGlobally()` method and set it to `false` if you want the transformation to be applied only for a mapping that explicitly requires it. +[[contract-matchers]] ==== Dynamic Properties in the Matchers Sections If you work with https://docs.pact.io/[Pact], the following discussion may seem familiar. @@ -577,6 +739,8 @@ contract. Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the following matching possibilities: +.Groovy DSL + * For `stubMatchers`: ** `byEquality()`: The value taken from the response via the provided JSON Path must be equal to the value provided in the contract. @@ -615,13 +779,63 @@ following, depending on the JSON path: *** `Number`: If you point to `Integer`, `Double`, or other kind of number. *** `Boolean`: If you point to a `Boolean`. +.YAML + +_Please read the Groovy section for detailed explanation of +what the types mean_ + +For YAML the structure of a matcher looks like this + +[source,yml,indent=0] +---- +- path: $.foo + type: by_regex + value: bar +---- + +Or if you want to use one of the predefined regular expressions +`[only_alpha_unicode, number, any_boolean, ip_address, hostname, +email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]`: + +[source,yml,indent=0] +---- +- path: $.foo + type: by_regex + predefined: only_alpha_unicode +---- + +Below you can find the allowed list of `type`s. + +* For `stubMatchers`: +** `by_equality` +** `by_regex` +** `by_date` +** `by_timestamp` +** `by_time` +* For `testMatchers`: +** `by_equality` +** `by_regex` +** `by_date` +** `by_timestamp` +** `by_time` +** `by_type` +*** there are 2 additional fields accepted: `minOccurrence` and `maxOccurrence`. +** `by_command` + Consider the following example: +.Groovy DSL [source,groovy,indent=0] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy[tags=matchers,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_matchers.yml[indent=0] +---- + In the preceding example, you can see the dynamic portions of the contract in the `matchers` sections. For the request part, you can see that, for all fields but `valueWithoutAMatcher`, the values of the regular expressions that the stub should @@ -784,6 +998,7 @@ If you're using asynchronous communication on the server side (your controllers returning `Callable`, `DeferredResult`, and so on), then, inside your contract, you must provide a `sync()` method in the `response` section. The following code shows an example: +.Groovy DSL [source,groovy,indent=0] ---- org.springframework.cloud.contract.spec.Contract.make { @@ -799,6 +1014,13 @@ org.springframework.cloud.contract.spec.Contract.make { } ---- +.YAML +[source,yml,indent=0] +---- +response: + async: true +---- + === Working with Context Paths Spring Cloud Contract supports context paths. @@ -871,11 +1093,18 @@ following sections explain the differences: The output message can be triggered by calling a method (such as a `Scheduler` when a was started and a message was sent), as shown in the following example: +.Groovy DSL [source,groovy] ---- include::{tests_path}/samples-messaging-integration/src/test/groovy/com/example/IntegrationMessagingApplicationSpec.groovy[tags=method_trigger,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_message_method.yml[indent=0] +---- + In the previous example case, the output message is sent to `output` if a method called `bookReturnedTriggered` is executed. On the message *publisher's* side, we generate a test that calls that method to trigger the message. On the *consumer* side, you can use @@ -887,11 +1116,18 @@ the `some_label` to trigger the message. The output message can be triggered by receiving a message, as shown in the following example: +.Groovy DSL [source,groovy] ---- include::{tests_path}/samples-messaging-integration/src/test/groovy/com/example/IntegrationMessagingApplicationSpec.groovy[tags=message_trigger,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_message_input_message.yml[indent=0] +---- + In the preceding example, the output message is sent to `output` if a proper message is received on the `input` destination. On the message *publisher's* side, the engine generates a test that sends the input message to the defined destination. On the @@ -901,6 +1137,8 @@ generates a test that sends the input message to the defined destination. On the [[contract-dsl-consumer-producer]] ==== Consumer/Producer +IMPORTANT: This section is valid only for Groovy DSL. + In HTTP, you have a notion of `client`/`stub and `server`/`test` notation. You can also use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also provides the `consumer` and `producer` methods, as presented in the following example @@ -915,21 +1153,28 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract [[contract-dsl-common]] ==== Common -In the `input {}` or `outputMessage {}` section you can call `assertThat` with the name +In the `input` or `outputMessage` section you can call `assertThat` with the name of a `method` (e.g. `assertThatMessageIsOnTheQueue()`) that you have defined in the -base class or in a static import. Spring Cloud Pipelines will execute that method -in the genertaed test. +base class or in a static import. Spring Cloud Contract will execute that method +in the generated test. === Multiple Contracts in One File You can define multiple contracts in one file. Such a contract might resemble the following example: +.Groovy DSL [source,groovy,indent=0] ---- include::{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/multiple-contracts/src/test/resources/contracts/com/hello/v1/WithList.groovy[lines=18..-1,indent=0] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/multiple_contracts.yml[indent=0] +---- + In the preceding example, one contract has the `name` field and the other does not. This leads to generation of two tests that look more or less like this: @@ -1001,6 +1246,8 @@ your tests far more meaningful. == Customization +IMPORTANT: This section is valid only for Groovy DSL + You can customize the Spring Cloud Contract Verifier by extending the DSL, as shown in the remainder of this section. @@ -1102,13 +1349,6 @@ can generate stubs for other HTTP server implementations). === Custom Contract Converter -Assume that your contract is written in a YAML file as follows: - -[source,yml] ----- -include::{verifier_core_path}/src/test/resources/contract.yml[indent=0] ----- - The `ContractConverter` interface lets you register your own implementation of a contract structure converter. The following code listing shows the `ContractConverter` interface: @@ -1128,15 +1368,8 @@ The following example shows a typical `spring.factories` file: [source] ---- -include::{verifier_core_path}/src/test/resources/META-INF/spring.factories[indent=0] ----- - -The following example shows a typical YAML implementation that matches the preceding -example: - -[source,groovy] ----- -include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy[indent=0,lines=16..-1] +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.verifier.converter.YamlContractConverter ---- ==== Pact Converter diff --git a/docs/src/main/asciidoc/verifier_faq.adoc b/docs/src/main/asciidoc/verifier_faq.adoc index 845d07dce1..d39a5206a4 100644 --- a/docs/src/main/asciidoc/verifier_faq.adoc +++ b/docs/src/main/asciidoc/verifier_faq.adoc @@ -15,6 +15,10 @@ Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Cont - Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory - Spring Cloud integration - no discovery service is needed for integration tests +=== I don't want to write a contract in Groovy! + +No problem. You can write a contract in YAML! + === What is this value(consumer(), producer()) ? One of the biggest challenges related to stubs is their reusability. Only if they can be vastly used, will they serve their purpose. @@ -370,4 +374,5 @@ for this for WireMock. In case of other HTTP server stubs you'll have to impleme ==== Can I reference text from file? Yes! With version 1.2.0 we've added such a possibility. It's enough to call `file(...)` method in the -DSL and provide a path relative to where the contract lays. \ No newline at end of file +DSL and provide a path relative to where the contract lays. +If you're using YAML just use the `bodyFromFile` property. \ No newline at end of file diff --git a/docs/src/main/asciidoc/verifier_introduction.adoc b/docs/src/main/asciidoc/verifier_introduction.adoc index 1fadbffad2..4e9b2d4259 100644 --- a/docs/src/main/asciidoc/verifier_introduction.adoc +++ b/docs/src/main/asciidoc/verifier_introduction.adoc @@ -106,11 +106,18 @@ Assume that you want to send a request containing the ID of a client company and amount it wants to borrow from us. You also want to send it to the /fraudcheck url via the PUT method. +.Groovy DSL [source,groovy,indent=0] ---- include::{introduction_url}/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[] ---- +.YAML +[source,yml,indent=0] +---- +include::{introduction_url}/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml[] +---- + ==== Client Side Spring Cloud Contract generates stubs, which you can use during client-side testing. @@ -144,7 +151,7 @@ your application behaves in a different way, especially in production. To ensure that your application behaves the way you define in your stub, tests are generated from the stub you provide. -The autogenerated test looks like this: +The autogenerated test looks, more or less, like this: [source,java,indent=0] ---- @@ -260,7 +267,7 @@ clone it. [source,bash,indent=0] ---- -git clone https://your-git-server.com/server-side.git local-http-server-repo +$ git clone https://your-git-server.com/server-side.git local-http-server-repo ---- *Define the contract locally in the repo of Fraud Detection service.* @@ -271,13 +278,21 @@ your expectations. To do so, write the following contract: IMPORTANT: Place the contract under `src/test/resources/contracts/fraud` folder. The `fraud` folder is important because the producer's test base class name references that folder. +.Groovy DSL [source,groovy,indent=0] ---- include::{introduction_url}/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[] ---- -The Contract is written using a statically typed Groovy DSL. You might wonder what about -those `value(client(...), server(...))` parts. By using this notation, Spring Cloud +.YAML +[source,yml,indent=0] +---- +include::{introduction_url}/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml[] +---- + +The YML contract is quite straight-forward. However when you take a look at the Contract +written using a statically typed Groovy DSL - you might wonder what the +`value(client(...), server(...))` parts are. By using this notation, Spring Cloud Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case of an identifier or a timestamp, you need not hardcode a value. You want to allow some different ranges of values. To enable ranges of values, you can set regular expressions @@ -333,8 +348,8 @@ stubs. You need to skip the test generation and execution. When you execute: [source,bash,indent=0] ---- -cd local-http-server-repo -./mvnw clean install -DskipTests +$ cd local-http-server-repo +$ ./mvnw clean install -DskipTests ---- In the logs, you see something like this: @@ -440,8 +455,8 @@ include::{introduction_url}/samples/standalone/dsl/http-server/src/main/java/com [source,bash,indent=0] ---- -git checkout -b contract-change-pr master -git pull https://your-git-server.com/server-side-fork.git contract-change-pr +$ git checkout -b contract-change-pr master +$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr ---- You must add the dependencies needed by the autogenerated tests: @@ -511,8 +526,9 @@ public void validate_shouldMarkClientAsFraud() throws Exception { } ---- -As you can see, all the `producer()` parts of the Contract that were present in the +If you used the Groovy DSL, you can see, all the `producer()` parts of the Contract that were present in the `value(consumer(...), producer(...))` blocks got injected into the test. +In case of using YAML, the same applied for the `matchers` sections of the `response`. Note that, on the producer side, you are also doing TDD. The expectations are expressed in the form of a test. This test sends a request to our own application with the URL, @@ -543,9 +559,9 @@ Once you finish your work, you can deploy your change. First, merge the branch: [source,bash,indent=0] ---- -git checkout master -git merge --no-ff contract-change-pr -git push origin master +$ git checkout master +$ git merge --no-ff contract-change-pr +$ git push origin master ---- Your CI might run something like `./mvnw clean deploy`, which would publish both the @@ -559,8 +575,8 @@ As a developer of the Loan Issuance service (a consumer of the Fraud Detection s [source,bash,indent=0] ---- -git checkout master -git merge --no-ff contract-change-pr +$ git checkout master +$ git merge --no-ff contract-change-pr ---- *Work online.* diff --git a/docs/src/main/asciidoc/verifier_messaging.adoc b/docs/src/main/asciidoc/verifier_messaging.adoc index 911429fdef..368a1344ef 100644 --- a/docs/src/main/asciidoc/verifier_messaging.adoc +++ b/docs/src/main/asciidoc/verifier_messaging.adoc @@ -89,11 +89,18 @@ it is resolved as a channel name. For *Camel*, that's a certain component (for e Here is an example for Camel. For the given contract: +.Groovy DSL [source,groovy] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_dsl] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario1.yml[indent=0] +---- + The following JUnit test is created: [source,groovy] @@ -112,11 +119,18 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract Here is an example for Camel. For the given contract: +.Groovy DSL [source,groovy] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_dsl] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario2.yml[indent=0] +---- + The following JUnit test is created: [source,groovy] @@ -135,11 +149,18 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract Here is an example for Camel. For the given contract: +.Groovy DSL [source,groovy] ---- include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl] ---- +.YAML +[source,yml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0] +---- + The following JUnit test is created: [source,groovy] diff --git a/docs/src/main/asciidoc/verifier_setup.adoc b/docs/src/main/asciidoc/verifier_setup.adoc index 9237889ca0..baed9d65c8 100644 --- a/docs/src/main/asciidoc/verifier_setup.adoc +++ b/docs/src/main/asciidoc/verifier_setup.adoc @@ -875,7 +875,8 @@ exclude the unwanted dependencies. You can handle scenarios with Spring Cloud Contract Verifier. All you need to do is to stick to the proper naming convention while creating your contracts. The convention -requires including an order number followed by an underscore, as shown in this example: +requires including an order number followed by an underscore. This will work regardles + of whether you're working with YAML or Groovy. Example: [source,indent=0] ---- diff --git a/samples/standalone/dsl/http-client/build.gradle b/samples/standalone/dsl/http-client/build.gradle index 8c205cbed3..752be56a53 100644 --- a/samples/standalone/dsl/http-client/build.gradle +++ b/samples/standalone/dsl/http-client/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" } } diff --git a/samples/standalone/dsl/http-server/build.gradle b/samples/standalone/dsl/http-server/build.gradle index 708e19075e..95237b337a 100644 --- a/samples/standalone/dsl/http-server/build.gradle +++ b/samples/standalone/dsl/http-server/build.gradle @@ -9,7 +9,7 @@ buildscript { } // end::repos[] dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}" } } diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy index 31c3d0e215..524caa690b 100644 --- a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy @@ -1,9 +1,9 @@ package contracts.fraudname org.springframework.cloud.contract.spec.Contract.make { + // highest priority + priority(1) request { - // highest priority - priority(1) method PUT() url '/frauds/name' body([ diff --git a/samples/standalone/messaging/stream-sink/build.gradle b/samples/standalone/messaging/stream-sink/build.gradle index 804e0a1224..aa3a71be22 100644 --- a/samples/standalone/messaging/stream-sink/build.gradle +++ b/samples/standalone/messaging/stream-sink/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" } } diff --git a/samples/standalone/messaging/stream-source/build.gradle b/samples/standalone/messaging/stream-source/build.gradle index f37f6bd09c..d44e965526 100644 --- a/samples/standalone/messaging/stream-source/build.gradle +++ b/samples/standalone/messaging/stream-source/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}" classpath "com.jayway.restassured:rest-assured:2.9.0" classpath "com.jayway.restassured:spring-mock-mvc:2.9.0" diff --git a/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.groovy b/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.groovy deleted file mode 100644 index 462d11ae48..0000000000 --- a/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package contracts - -org.springframework.cloud.contract.spec.Contract.make { - request { - method(GET()) - url("/foo") - } - response { - status(200) - } -} diff --git a/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.yml b/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.yml new file mode 100644 index 0000000000..6360b45b39 --- /dev/null +++ b/samples/standalone/messaging/stream-source/src/test/resources/contracts/shouldReturnOkWhenServiceIsHealthy.yml @@ -0,0 +1,6 @@ +description: Should return 200 for /foo +request: + url: /foo + method: GET +response: + status: 200 \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/build.gradle b/samples/standalone/pact/pact-http-client/build.gradle index f477e647d2..0d73f96f4d 100644 --- a/samples/standalone/pact/pact-http-client/build.gradle +++ b/samples/standalone/pact/pact-http-client/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" } } diff --git a/samples/standalone/pact/pact-http-server/build.gradle b/samples/standalone/pact/pact-http-server/build.gradle index 6833482dc8..1edf17f642 100644 --- a/samples/standalone/pact/pact-http-server/build.gradle +++ b/samples/standalone/pact/pact-http-server/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}" //tag::pact_dependency[] classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}" diff --git a/samples/standalone/pom.xml b/samples/standalone/pom.xml index 3d10bea281..e4729d4b2d 100644 --- a/samples/standalone/pom.xml +++ b/samples/standalone/pom.xml @@ -26,6 +26,7 @@ dsl messaging pact + yml diff --git a/samples/standalone/restdocs/http-client/build.gradle b/samples/standalone/restdocs/http-client/build.gradle index 612371779f..3457d39d16 100644 --- a/samples/standalone/restdocs/http-client/build.gradle +++ b/samples/standalone/restdocs/http-client/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "http://repo.spring.io/release" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" } } diff --git a/samples/standalone/restdocs/http-server/build.gradle b/samples/standalone/restdocs/http-server/build.gradle index f4c0302c39..7a23f84dfb 100644 --- a/samples/standalone/restdocs/http-server/build.gradle +++ b/samples/standalone/restdocs/http-server/build.gradle @@ -10,7 +10,7 @@ buildscript { maven { url "http://repo.spring.io/plugins-staging-local/" } } dependencies { - classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE" + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" } } diff --git a/samples/standalone/yml/http-client/.gitignore b/samples/standalone/yml/http-client/.gitignore new file mode 100644 index 0000000000..b05ba7bd6d --- /dev/null +++ b/samples/standalone/yml/http-client/.gitignore @@ -0,0 +1,6 @@ + +target/ + +.gradle +build/ + diff --git a/samples/standalone/yml/http-client/.mvn/jvm.config b/samples/standalone/yml/http-client/.mvn/jvm.config new file mode 100644 index 0000000000..894bef17a5 --- /dev/null +++ b/samples/standalone/yml/http-client/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true \ No newline at end of file diff --git a/samples/standalone/yml/http-client/.mvn/maven.config b/samples/standalone/yml/http-client/.mvn/maven.config new file mode 100644 index 0000000000..affad39a42 --- /dev/null +++ b/samples/standalone/yml/http-client/.mvn/maven.config @@ -0,0 +1 @@ +-T2 \ No newline at end of file diff --git a/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.jar b/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..c6feb8bb6f Binary files /dev/null and b/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.jar differ diff --git a/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.properties b/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..6637cedb28 --- /dev/null +++ b/samples/standalone/yml/http-client/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file diff --git a/samples/standalone/yml/http-client/README.adoc b/samples/standalone/yml/http-client/README.adoc new file mode 100644 index 0000000000..cdcb0e4a56 --- /dev/null +++ b/samples/standalone/yml/http-client/README.adoc @@ -0,0 +1,26 @@ += Http Client + +== Prerequisites + +First you have to publish to Maven Local the stubs of the *http-server* module + +== How to run it? + +Run + +[source=groovy] +-------- +./gradlew clean build +-------- + +or + +-------- +./mvnw clean package +-------- + +To + +- build the app +- use spring-cloud-contract-stub-runner-spring[Stub Runner Spring] to download the stub of `Http Server` +- run the tests against stubbed server \ No newline at end of file diff --git a/samples/standalone/yml/http-client/build.gradle b/samples/standalone/yml/http-client/build.gradle new file mode 100644 index 0000000000..752be56a53 --- /dev/null +++ b/samples/standalone/yml/http-client/build.gradle @@ -0,0 +1,64 @@ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" + } +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } +} + +apply plugin: 'groovy' +apply plugin: 'org.springframework.boot' +apply plugin: 'maven-publish' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$BOM_VERSION" + } +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testCompile "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" +} + +test { + systemProperty 'spring.profiles.active', 'gradle' + testLogging { + exceptionFormat = 'full' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.14' +} + +task resolveDependencies { + doLast { + project.rootProject.allprojects.each { subProject -> + subProject.buildscript.configurations.each { configuration -> + configuration.resolve() + } + subProject.configurations.each { configuration -> + configuration.resolve() + } + } + } +} diff --git a/samples/standalone/yml/http-client/gradle.properties b/samples/standalone/yml/http-client/gradle.properties new file mode 100644 index 0000000000..0c444e9a19 --- /dev/null +++ b/samples/standalone/yml/http-client/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.daemon=false +BOM_VERSION=Edgware.BUILD-SNAPSHOT \ No newline at end of file diff --git a/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.jar b/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..3baa851b28 Binary files /dev/null and b/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.properties b/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ff3de523bd --- /dev/null +++ b/samples/standalone/yml/http-client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 19 15:39:05 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip diff --git a/samples/standalone/yml/http-client/gradlew b/samples/standalone/yml/http-client/gradlew new file mode 100755 index 0000000000..27309d9231 --- /dev/null +++ b/samples/standalone/yml/http-client/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# 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 +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +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" -a "$nonstop" = "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/samples/standalone/yml/http-client/gradlew.bat b/samples/standalone/yml/http-client/gradlew.bat new file mode 100644 index 0000000000..832fdb6079 --- /dev/null +++ b/samples/standalone/yml/http-client/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 + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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= + +@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/samples/standalone/yml/http-client/mvnw b/samples/standalone/yml/http-client/mvnw new file mode 100755 index 0000000000..fc7efd17d0 --- /dev/null +++ b/samples/standalone/yml/http-client/mvnw @@ -0,0 +1,234 @@ +#!/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 + +# 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_CMD_LINE_ARGS + diff --git a/samples/standalone/yml/http-client/mvnw.cmd b/samples/standalone/yml/http-client/mvnw.cmd new file mode 100644 index 0000000000..001048081d --- /dev/null +++ b/samples/standalone/yml/http-client/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/samples/standalone/yml/http-client/pom.xml b/samples/standalone/yml/http-client/pom.xml new file mode 100644 index 0000000000..b768a828d7 --- /dev/null +++ b/samples/standalone/yml/http-client/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + com.example + http-client-yml + 0.0.1-SNAPSHOT + + Spring Cloud Contract Verifier Http Client Sample + Spring Cloud Contract Verifier Http Client Sample + + + org.springframework.boot + spring-boot-starter-parent + 1.5.9.RELEASE + + + + + UTF-8 + 1.8 + Edgware.BUILD-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.apache.httpcomponents + httpclient + + + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner + test + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-deploy-plugin + 2.8.2 + + true + + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + + build + + + target + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + + integration + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + gradle + test + + ./gradlew + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + windows + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + gradle + test + + gradlew.bat + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + + diff --git a/samples/standalone/yml/http-client/settings.gradle b/samples/standalone/yml/http-client/settings.gradle new file mode 100644 index 0000000000..e12fd21d66 --- /dev/null +++ b/samples/standalone/yml/http-client/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'http-client-yml-gradle' diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/Application.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/Application.java new file mode 100644 index 0000000000..6f36301172 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/Application.java @@ -0,0 +1,13 @@ +package com.example.loan; + +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/samples/standalone/yml/http-client/src/main/java/com/example/loan/LoanApplicationService.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/LoanApplicationService.java new file mode 100644 index 0000000000..cf26b27f35 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/LoanApplicationService.java @@ -0,0 +1,93 @@ +package com.example.loan; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.example.loan.model.FraudCheckStatus; +import com.example.loan.model.FraudServiceRequest; +import com.example.loan.model.FraudServiceResponse; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; +import com.example.loan.model.Response; + +@Service +public class LoanApplicationService { + + private final RestTemplate restTemplate; + + private int port = 6565; + + @Autowired + public LoanApplicationService(RestTemplateBuilder builder) { + this.restTemplate = builder.build(); + } + + 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, MediaType.APPLICATION_JSON_VALUE); + + // tag::client_call_server[] + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), + FraudServiceResponse.class); + // end::client_call_server[] + + 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 int countAllFrauds() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/frauds", HttpMethod.GET, + new HttpEntity<>(httpHeaders), + Response.class); + return response.getBody().getCount(); + } + + public int countDrunks() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/drunks", HttpMethod.GET, + new HttpEntity<>(httpHeaders), + Response.class); + return response.getBody().getCount(); + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Client.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Client.java new file mode 100644 index 0000000000..abf66106b5 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Client.java @@ -0,0 +1,21 @@ +package com.example.loan.model; + +public class Client { + + private String pesel; + + public Client() { + } + + public Client(String pesel) { + this.pesel = pesel; + } + + public String getPesel() { + return pesel; + } + + public void setPesel(String pesel) { + this.pesel = pesel; + } +} diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java new file mode 100644 index 0000000000..c5217ef471 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package com.example.loan.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java new file mode 100644 index 0000000000..5cc40027bb --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java @@ -0,0 +1,37 @@ +package com.example.loan.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; + +public class FraudServiceRequest { + + @JsonProperty("client.id") + private String clientId; + + private BigDecimal loanAmount; + + public FraudServiceRequest() { + } + + public FraudServiceRequest(LoanApplication loanApplication) { + this.clientId = loanApplication.getClient().getPesel(); + this.loanAmount = loanApplication.getAmount(); + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java new file mode 100644 index 0000000000..af58a03223 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java @@ -0,0 +1,30 @@ +package com.example.loan.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FraudServiceResponse { + + private FraudCheckStatus fraudCheckStatus; + + @JsonProperty("rejection.reason") + 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/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplication.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplication.java new file mode 100644 index 0000000000..27d66f884c --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplication.java @@ -0,0 +1,44 @@ +package com.example.loan.model; + +import java.math.BigDecimal; + +public class LoanApplication { + + private Client client; + + private BigDecimal amount; + + private String loanApplicationId; + + public LoanApplication() { + } + + public LoanApplication(Client client, double amount) { + this.client = client; + this.amount = BigDecimal.valueOf(amount); + } + + 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/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java new file mode 100644 index 0000000000..ffd44e1a26 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java @@ -0,0 +1,32 @@ +package com.example.loan.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/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java new file mode 100644 index 0000000000..34cf91a89f --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java @@ -0,0 +1,5 @@ +package com.example.loan.model; + +public enum LoanApplicationStatus { + LOAN_APPLIED, LOAN_APPLICATION_REJECTED +} diff --git a/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Response.java b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Response.java new file mode 100644 index 0000000000..8335ee0cc6 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/java/com/example/loan/model/Response.java @@ -0,0 +1,20 @@ +package com.example.loan.model; + +public class Response { + private int count; + + public Response(int count) { + this.count = count; + } + + public Response() { + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } +} \ No newline at end of file diff --git a/samples/standalone/yml/http-client/src/main/resources/application.yml b/samples/standalone/yml/http-client/src/main/resources/application.yml new file mode 100644 index 0000000000..4b7fe13158 --- /dev/null +++ b/samples/standalone/yml/http-client/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port: 8090 \ No newline at end of file diff --git a/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceContextPathTests.java b/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceContextPathTests.java new file mode 100644 index 0000000000..522edf2998 --- /dev/null +++ b/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceContextPathTests.java @@ -0,0 +1,54 @@ +package com.example.loan; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.example.loan.model.Client; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.NONE, properties="server.context-path=/app") +@AutoConfigureStubRunner(ids = {"com.example:http-server-yml:+:stubs:6565"}, workOffline = true) +@DirtiesContext +public class LoanApplicationServiceContextPathTests { + + @Autowired + private LoanApplicationService service; + + @Test + public void shouldSuccessfullyApplyForLoan() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 123.123); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLIED); + assertThat(loanApplication.getRejectionReason()).isNull(); + } + + @Test + public void shouldBeRejectedDueToAbnormalLoanAmount() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 99999); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED); + assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); + } + +} diff --git a/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java b/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java new file mode 100644 index 0000000000..ada18d72bb --- /dev/null +++ b/samples/standalone/yml/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java @@ -0,0 +1,74 @@ +package com.example.loan; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.example.loan.model.Client; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::autoconfigure_stubrunner[] +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = {"com.example:http-server-yml:+:stubs:6565"}, workOffline = true) +@DirtiesContext +public class LoanApplicationServiceTests { +// end::autoconfigure_stubrunner[] + + @Autowired + private LoanApplicationService service; + + @Test + public void shouldSuccessfullyApplyForLoan() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 123.123); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLIED); + assertThat(loanApplication.getRejectionReason()).isNull(); + } + + // tag::client_tdd[] + @Test + public void shouldBeRejectedDueToAbnormalLoanAmount() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 99999); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED); + assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); + } + // end::client_tdd[] + + @Test + public void shouldSuccessfullyGetAllFrauds() { + // when: + int count = service.countAllFrauds(); + // then: + assertThat(count).isEqualTo(200); + } + + @Test + public void shouldSuccessfullyGetAllDrunks() { + // when: + int count = service.countDrunks(); + // then: + assertThat(count).isEqualTo(100); + } + +} diff --git a/samples/standalone/yml/http-client/src/test/resources/application-gradle.yaml b/samples/standalone/yml/http-client/src/test/resources/application-gradle.yaml new file mode 100644 index 0000000000..3ea79d4f5f --- /dev/null +++ b/samples/standalone/yml/http-client/src/test/resources/application-gradle.yaml @@ -0,0 +1,3 @@ +stubrunner: + work-offline: true + stubs.ids: 'com.example:http-server-dsl-gradle:+:stubs:6565' \ No newline at end of file diff --git a/samples/standalone/yml/http-client/src/test/resources/application-test-repo.yaml b/samples/standalone/yml/http-client/src/test/resources/application-test-repo.yaml new file mode 100644 index 0000000000..0fe0e54c25 --- /dev/null +++ b/samples/standalone/yml/http-client/src/test/resources/application-test-repo.yaml @@ -0,0 +1,3 @@ +stubrunner: + ids: 'com.example:http-server-dsl:+:stubs:8080' + repositoryRoot: http://repo.spring.io/libs-snapshot diff --git a/samples/standalone/yml/http-client/src/test/resources/application.yaml b/samples/standalone/yml/http-client/src/test/resources/application.yaml new file mode 100644 index 0000000000..21459f2953 --- /dev/null +++ b/samples/standalone/yml/http-client/src/test/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 6565 diff --git a/samples/standalone/yml/http-server/.gitignore b/samples/standalone/yml/http-server/.gitignore new file mode 100644 index 0000000000..b05ba7bd6d --- /dev/null +++ b/samples/standalone/yml/http-server/.gitignore @@ -0,0 +1,6 @@ + +target/ + +.gradle +build/ + diff --git a/samples/standalone/yml/http-server/.mvn/jvm.config b/samples/standalone/yml/http-server/.mvn/jvm.config new file mode 100644 index 0000000000..894bef17a5 --- /dev/null +++ b/samples/standalone/yml/http-server/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true \ No newline at end of file diff --git a/samples/standalone/yml/http-server/.mvn/maven.config b/samples/standalone/yml/http-server/.mvn/maven.config new file mode 100644 index 0000000000..7681bc67b9 --- /dev/null +++ b/samples/standalone/yml/http-server/.mvn/maven.config @@ -0,0 +1 @@ +-T2 diff --git a/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.jar b/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..c6feb8bb6f Binary files /dev/null and b/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.jar differ diff --git a/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.properties b/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..6637cedb28 --- /dev/null +++ b/samples/standalone/yml/http-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file diff --git a/samples/standalone/yml/http-server/README.adoc b/samples/standalone/yml/http-server/README.adoc new file mode 100644 index 0000000000..2fd3fbc40d --- /dev/null +++ b/samples/standalone/yml/http-server/README.adoc @@ -0,0 +1,20 @@ += Http Server + +Run + +[source=groovy] +-------- +./gradlew clean build publishToMavenLocal +-------- + +or + +-------- +./mvnw clean install +-------- + +To + +- build the app +- generate and run Spring Cloud Contract Verifier tests +- publish the fatJar and the stubs to Maven Local \ No newline at end of file diff --git a/samples/standalone/yml/http-server/build.gradle b/samples/standalone/yml/http-server/build.gradle new file mode 100644 index 0000000000..95237b337a --- /dev/null +++ b/samples/standalone/yml/http-server/build.gradle @@ -0,0 +1,78 @@ +// tag::repos[] +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } + } +// end::repos[] + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE" + classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}" + } +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +// tag::deps_repos[] +repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } +} +// end::deps_repos[] + +apply plugin: 'groovy' +apply plugin: 'org.springframework.boot' +apply plugin: 'spring-cloud-contract' +apply plugin: 'maven-publish' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$BOM_VERSION" + } +} + +contracts { + packageWithBaseClasses = 'com.example.fraud' +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +} + +test { + systemProperty 'spring.profiles.active', 'gradle' + testLogging { + exceptionFormat = 'full' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.14' +} + +clean.doFirst { + delete "~/.m2/repository/com/example/http-server-dsl-gradle" +} + +task resolveDependencies { + doLast { + project.rootProject.allprojects.each { subProject -> + subProject.buildscript.configurations.each { configuration -> + configuration.resolve() + } + subProject.configurations.each { configuration -> + configuration.resolve() + } + } + } +} diff --git a/samples/standalone/yml/http-server/gradle.properties b/samples/standalone/yml/http-server/gradle.properties new file mode 100644 index 0000000000..b99fcb4df7 --- /dev/null +++ b/samples/standalone/yml/http-server/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.daemon=false +verifierVersion=1.2.2.BUILD-SNAPSHOT +BOM_VERSION=Edgware.BUILD-SNAPSHOT \ No newline at end of file diff --git a/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.jar b/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..3baa851b28 Binary files /dev/null and b/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.properties b/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..95d7279349 --- /dev/null +++ b/samples/standalone/yml/http-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 19 15:38:58 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip diff --git a/samples/standalone/yml/http-server/gradlew b/samples/standalone/yml/http-server/gradlew new file mode 100755 index 0000000000..27309d9231 --- /dev/null +++ b/samples/standalone/yml/http-server/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# 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 +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +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" -a "$nonstop" = "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/samples/standalone/yml/http-server/gradlew.bat b/samples/standalone/yml/http-server/gradlew.bat new file mode 100644 index 0000000000..832fdb6079 --- /dev/null +++ b/samples/standalone/yml/http-server/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 + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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= + +@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/samples/standalone/yml/http-server/mvnw b/samples/standalone/yml/http-server/mvnw new file mode 100755 index 0000000000..fc7efd17d0 --- /dev/null +++ b/samples/standalone/yml/http-server/mvnw @@ -0,0 +1,234 @@ +#!/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 + +# 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_CMD_LINE_ARGS + diff --git a/samples/standalone/yml/http-server/mvnw.cmd b/samples/standalone/yml/http-server/mvnw.cmd new file mode 100644 index 0000000000..001048081d --- /dev/null +++ b/samples/standalone/yml/http-server/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/samples/standalone/yml/http-server/pom.xml b/samples/standalone/yml/http-server/pom.xml new file mode 100644 index 0000000000..271415af62 --- /dev/null +++ b/samples/standalone/yml/http-server/pom.xml @@ -0,0 +1,256 @@ + + + 4.0.0 + + com.example + http-server-yml + 0.0.1-SNAPSHOT + + Spring Cloud Contract Verifier Http Server Sample + Spring Cloud Contract Verifier Http Server Sample + + + org.springframework.boot + spring-boot-starter-parent + 1.5.9.RELEASE + + + + + UTF-8 + 1.8 + 1.2.2.BUILD-SNAPSHOT + Edgware.BUILD-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.cloud + spring-cloud-starter-contract-verifier + test + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + ${spring-cloud-contract.version} + true + + com.example.fraud + + + + + maven-deploy-plugin + 2.8.2 + + true + + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + + build + + + target + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + [${spring-cloud-contract.version},) + + convert + generateTests + + + + + + + + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + + + integration + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + gradle + test + + ./gradlew + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + windows + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + gradle + test + + gradlew.bat + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + diff --git a/samples/standalone/yml/http-server/settings.gradle b/samples/standalone/yml/http-server/settings.gradle new file mode 100644 index 0000000000..c2d11e3eb9 --- /dev/null +++ b/samples/standalone/yml/http-server/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'http-server-yml-gradle' diff --git a/samples/standalone/yml/http-server/src/main/java/com/example/fraud/Application.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/Application.java new file mode 100644 index 0000000000..9117009b09 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/Application.java @@ -0,0 +1,17 @@ +package com.example.fraud; + +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/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudDetectionController.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudDetectionController.java new file mode 100644 index 0000000000..764f8a6e96 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudDetectionController.java @@ -0,0 +1,40 @@ +package com.example.fraud; + +import java.math.BigDecimal; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.fraud.model.FraudCheck; +import com.example.fraud.model.FraudCheckResult; +import com.example.fraud.model.FraudCheckStatus; + +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +@RestController +public class FraudDetectionController { + + 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"); + + // tag::server_api[] + @RequestMapping(value = "/fraudcheck", method = PUT) + public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { + // end::server_api[] + // tag::new_impl[] + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH); + } + // end::new_impl[] + // tag::initial_impl[] + return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON); + // end::initial_impl[] + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudNameController.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudNameController.java new file mode 100644 index 0000000000..4ba6881594 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudNameController.java @@ -0,0 +1,69 @@ +package com.example.fraud; + +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Marcin Grzejszczak + */ +@RestController +class FraudNameController { + + private final FraudVerifier fraudVerifier; + + FraudNameController(FraudVerifier fraudVerifier) { + this.fraudVerifier = fraudVerifier; + } + + @PutMapping(value = "/frauds/name") + public NameResponse checkByName(@RequestBody NameRequest request) { + boolean fraud = this.fraudVerifier.isFraudByName(request.getName()); + if (fraud) { + return new NameResponse("Sorry " + request.getName() + " but you're a fraud"); + } + return new NameResponse("Don't worry " + request.getName() + " you're not a fraud"); + } +} + +interface FraudVerifier { + boolean isFraudByName(String name); +} + +class NameRequest { + private String name; + + public NameRequest(String name) { + this.name = name; + } + + public NameRequest() { + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } +} + +class NameResponse { + private String result; + + public NameResponse(String result) { + this.result = result; + } + + public NameResponse() { + } + + public String getResult() { + return this.result; + } + + public void setResult(String result) { + this.result = result; + } +} diff --git a/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudStatsController.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudStatsController.java new file mode 100644 index 0000000000..ac9340611c --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/FraudStatsController.java @@ -0,0 +1,51 @@ +package com.example.fraud; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class FraudStatsController { + + private final StatsProvider statsProvider; + + public FraudStatsController(StatsProvider statsProvider) { + this.statsProvider = statsProvider; + } + + @GetMapping(value = "/frauds") + public Response countAllFrauds() { + return new Response(this.statsProvider.count(FraudType.ALL)); + } + + @GetMapping(value = "/drunks") + public Response countAllDrunks() { + return new Response(this.statsProvider.count(FraudType.DRUNKS)); + } +} + +enum FraudType { + DRUNKS, ALL +} + +interface StatsProvider { + int count(FraudType fraudType); +} + +class Response { + private int count; + + public Response(int count) { + this.count = count; + } + + public Response() { + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheck.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheck.java new file mode 100644 index 0000000000..e98c2b60d2 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheck.java @@ -0,0 +1,32 @@ +package com.example.fraud.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; + +public class FraudCheck { + + @JsonProperty("client.id") + private String clientId; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java new file mode 100644 index 0000000000..2f30912063 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java @@ -0,0 +1,35 @@ +package com.example.fraud.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + @JsonProperty("rejection.reason") + 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/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java new file mode 100644 index 0000000000..5a6adcf274 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package com.example.fraud.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/samples/standalone/yml/http-server/src/main/resources/application.yml b/samples/standalone/yml/http-server/src/main/resources/application.yml new file mode 100644 index 0000000000..1c421cf2b7 --- /dev/null +++ b/samples/standalone/yml/http-server/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port=0 \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudBase.java b/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudBase.java new file mode 100644 index 0000000000..142bffa8df --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudBase.java @@ -0,0 +1,29 @@ +package com.example.fraud; + +import org.junit.Before; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; + +public class FraudBase { + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(), + new FraudStatsController(stubbedStatsProvider())); + } + + private StatsProvider stubbedStatsProvider() { + return fraudType -> { + switch (fraudType) { + case DRUNKS: + return 100; + case ALL: + return 200; + } + return 0; + }; + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudnameBase.java b/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudnameBase.java new file mode 100644 index 0000000000..ab8288eeed --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/java/com/example/fraud/FraudnameBase.java @@ -0,0 +1,17 @@ +package com.example.fraud; + +import org.junit.Before; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; + +public class FraudnameBase { + + private static final String FRAUD_NAME = "fraud"; + + FraudVerifier fraudVerifier = FRAUD_NAME::equals; + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new FraudNameController(this.fraudVerifier)); + } +} \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml new file mode 100644 index 0000000000..5180a42bd6 --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml @@ -0,0 +1,52 @@ +request: # (1) + method: PUT # (2) + url: /fraudcheck # (3) + body: # (4) + "client.id": 1234567890 + loanAmount: 99999 + headers: # (5) + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] # (6) + type: by_regex + value: "[0-9]{10}" +response: # (7) + status: 200 # (8) + body: # (9) + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: # (10) + Content-Type: application/json;charset=UTF-8 + + +#From the Consumer perspective, when shooting a request in the integration test: +# +#(1) - If the consumer sends a request +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}` +#(7) - then the response will be sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json` +# +#From the Producer perspective, in the autogenerated producer-side test: +# +#(1) - A request will be sent to the producer +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `clientId` `1234567890` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(7) - then the test will assert if the response has been sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8` \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.yaml b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.yaml new file mode 100644 index 0000000000..7857099211 --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.yaml @@ -0,0 +1,25 @@ +request: + method: PUT + url: /fraudcheck + body: + "client.id": 1234567890 + loanAmount: 123.123 + headers: + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] + type: by_regex + value: "[0-9]{10}" +response: + status: 200 + body: + fraudCheckStatus: "OK" + "rejection.reason": null + headers: + Content-Type: application/json;charset=UTF-8 + matchers: + body: + - path: $.['rejection.reason'] + type: by_command + value: assertThatRejectionReasonIsNull($it) \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.yml b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.yml new file mode 100644 index 0000000000..f66eb51be3 --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.yml @@ -0,0 +1,21 @@ +--- +name: "should count all frauds" +request: + method: GET + url: /frauds +response: + status: 200 + body: + count: 200 + headers: + Content-Type: application/json;charset=UTF-8 +--- +request: + method: GET + url: /drunks +response: + status: 200 + body: + count: 100 + headers: + Content-Type: application/json;charset=UTF-8 \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.yml b/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.yml new file mode 100644 index 0000000000..29859f0b97 --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.yml @@ -0,0 +1,15 @@ +# highest priority +priority: 1 +request: + method: PUT + url: /frauds/name + body: + name: "fraud" + headers: + Content-Type: application/json;charset=UTF-8 +response: + status: 200 + body: + result: "Sorry {{{ jsonpath this '$.name' }}} but you're a fraud" + headers: + Content-Type: "{{{ request.headers.Content-Type.0 }}}" \ No newline at end of file diff --git a/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.yml b/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.yml new file mode 100644 index 0000000000..af59b2ad65 --- /dev/null +++ b/samples/standalone/yml/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.yml @@ -0,0 +1,18 @@ +request: + method: PUT + url: /frauds/name + body: + name: "non fraud" + headers: + Content-Type: application/json + matchers: + body: + - path: $.name + type: by_regex + predefined: only_alpha_unicode +response: + status: 200 + body: + result: "Don't worry {{{ jsonpath this '$.name' }}} you're not a fraud" + headers: + Content-Type: "{{{ request.headers.Content-Type.0 }}};charset=UTF-8" \ No newline at end of file diff --git a/samples/standalone/yml/pom.xml b/samples/standalone/yml/pom.xml new file mode 100644 index 0000000000..cc41273a7f --- /dev/null +++ b/samples/standalone/yml/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + org.springframework.cloud + spring-cloud-contract-samples-standalone + 1.2.2.BUILD-SNAPSHOT + .. + + + spring-cloud-contract-samples-yml + pom + + Spring Cloud Contract Standalone Dsl Test Samples + Spring Cloud Contract Standalone Test Samples used for end to end tests + + + 1.2.2.BUILD-SNAPSHOT + + + + http-server + http-client + + + + + + maven-deploy-plugin + 2.8.2 + + true + + + + + + diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy index 1b7eecd32c..100210ca99 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy @@ -31,7 +31,8 @@ import groovy.transform.ToString class Body extends DslProperty { Body(Map body) { - super(extractValue(body, { DslProperty p -> p.clientValue}), extractValue(body, {DslProperty p -> p.serverValue})) + super(extractValue(body, { p -> p instanceof DslProperty ? ((DslProperty) p).clientValue : p }), + extractValue(body, { p -> p instanceof DslProperty ? ((DslProperty) p).serverValue : p })) } private static Map extractValue(Map body, Closure valueProvider) { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy index b865c32d62..f2b5374e0a 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -15,7 +15,7 @@ import groovy.transform.ToString @ToString(includeFields = true, includePackage = false) class BodyMatchers { private final RegexPatterns regexPatterns = new RegexPatterns() - private final List jsonPathRegexMatchers = [] + protected final List jsonPathRegexMatchers = [] void jsonPath(String path, MatchingTypeValue matchingType) { this.jsonPathRegexMatchers << new JsonPathBodyMatcher(path, matchingType) diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy index 61278dbeba..1798ad43c9 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy @@ -91,6 +91,18 @@ class Headers { } as Map } + /** + * Converts the headers into their stub side representations and returns as + * a map of String key => Object value. + */ + Map asTestSideMap() { + def acc = [:].withDefault { [] as Collection } + return entries.inject(acc as Map) { Map map, Header header -> + map[header.name] = header.serverValue + return map + } as Map + } + boolean equals(o) { if (this.is(o)) return true if (getClass() != o.class) return false diff --git a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy index 397d1d93ae..e69d20ad88 100644 --- a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy +++ b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/ContractSpec.groovy @@ -94,6 +94,15 @@ then: // end::description[] } + def 'should set a name'() { + given: + // tag::name[] + org.springframework.cloud.contract.spec.Contract.make { + name("some_special_name") + } + // end::name[] + } + def 'should mark a contract ignored'() { given: // tag::ignored[] diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java index 8e4ee733b2..9a80136acd 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.spec.ContractConverter; import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStub; +import org.springframework.cloud.contract.verifier.converter.YamlContractConverter; import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -165,11 +166,15 @@ class StubRepository { BasicFileAttributes attrs) throws IOException { File file = path.toFile(); ContractConverter converter = contractConverter(file); - if (isContractDescriptor(file) && isStubPerConsumerPathMatching(file)) { - contractDescriptors - .addAll(ContractVerifierDslConverter.convertAsCollection(file.getParentFile(), file)); - } else if (converter != null && isStubPerConsumerPathMatching(file)) { - contractDescriptors.addAll(converter.convertFrom(file)); + if (isStubPerConsumerPathMatching(file)) { + if (isContractDescriptor(file)) { + contractDescriptors + .addAll(ContractVerifierDslConverter.convertAsCollection(file.getParentFile(), file)); + } else if (YamlContractConverter.INSTANCE.isAccepted(file)) { + contractDescriptors.addAll(YamlContractConverter.INSTANCE.convertFrom(file)); + } else if (converter != null) { + contractDescriptors.addAll(converter.convertFrom(file)); + } } return super.visitFile(path, attrs); } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/build.gradle index 5674941167..2d383f25d3 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/build.gradle @@ -23,7 +23,7 @@ ext { ] } -project.version = findProperty('verifierVersion') ?: '1.5.4.RELEASE' +project.version = findProperty('verifierVersion') ?: '1.5.9.RELEASE' apply plugin: 'groovy' apply from: "$rootDir/gradle/release.gradle" apply plugin: 'eclipse' diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle index e1f690423a..aa5fa89e2d 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/build.gradle @@ -20,7 +20,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE") + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE") } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle index c690f91b42..80388a96d9 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleProject/build.gradle @@ -20,7 +20,7 @@ buildscript { mavenLocal() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE") + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE") } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle index 12068579b3..25b5133ae9 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle @@ -20,7 +20,7 @@ buildscript { mavenLocal() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.4.RELEASE") + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE") } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateStubsMojo.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateStubsMojo.java index b7ee1636fa..3398186708 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateStubsMojo.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateStubsMojo.java @@ -40,7 +40,9 @@ import org.codehaus.plexus.archiver.jar.JarArchiver; public class GenerateStubsMojo extends AbstractMojo { private static final String STUB_MAPPING_FILE_PATTERN = "**/*.json"; - private static final String CONTRACT_FILE_PATTERN = "**/*.groovy"; + private static final String GROOVY_CONTRACT_FILE_PATTERN = "**/*.groovy"; + private static final String YAML_CONTRACT_FILE_PATTERN = "**/*.yaml"; + private static final String YML_CONTRACT_FILE_PATTERN = "**/*.yml"; @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) @@ -110,7 +112,10 @@ public class GenerateStubsMojo extends AbstractMojo { try { if (this.attachContracts) { this.archiver.addDirectory(stubsOutputDir, - new String[] { STUB_MAPPING_FILE_PATTERN, CONTRACT_FILE_PATTERN }, + new String[] { STUB_MAPPING_FILE_PATTERN, + GROOVY_CONTRACT_FILE_PATTERN, + YAML_CONTRACT_FILE_PATTERN, + YML_CONTRACT_FILE_PATTERN }, excludedFilesEmpty() ? new String[0] : this.excludedFiles); } else { @@ -134,7 +139,9 @@ public class GenerateStubsMojo extends AbstractMojo { private String[] excludes() { List excludes = new ArrayList<>(); - excludes.add(CONTRACT_FILE_PATTERN); + excludes.add(GROOVY_CONTRACT_FILE_PATTERN); + excludes.add(YAML_CONTRACT_FILE_PATTERN); + excludes.add(YML_CONTRACT_FILE_PATTERN); if (!excludedFilesEmpty()) { excludes.addAll(Arrays.asList(this.excludedFiles)); } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/plugin-extension/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/plugin-extension/pom.xml index 3c718f96f0..d83b95cf92 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/plugin-extension/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/plugin-extension/pom.xml @@ -28,7 +28,7 @@ org.springframework.boot spring-boot-starter-parent - 1.5.4.RELEASE + 1.5.9.RELEASE diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml index 77aacd7970..fe239312b9 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-groovy/pom.xml @@ -28,7 +28,7 @@ org.springframework.boot spring-boot-starter-parent - 1.5.4.RELEASE + 1.5.9.RELEASE diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-java/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-java/pom.xml index 3c718f96f0..d83b95cf92 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-java/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/spring-boot-java/pom.xml @@ -28,7 +28,7 @@ org.springframework.boot spring-boot-starter-parent - 1.5.4.RELEASE + 1.5.9.RELEASE diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index c34abb4dd3..558d557f0f 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -21,6 +21,10 @@ org.springframework.boot spring-boot-test-autoconfigure + + com.fasterxml.jackson.core + jackson-databind + org.springframework spring-messaging @@ -81,6 +85,10 @@ com.github.jknack handlebars + + org.yaml + snakeyaml + cglib cglib @@ -105,11 +113,6 @@ slf4j-simple test - - org.yaml - snakeyaml - test - io.rest-assured rest-assured diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy index eb725aab4e..c3de1c34a7 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy @@ -187,7 +187,6 @@ class ClassPresenceChecker { boolean isClassPresent(String className) { try { - Class.forName(className) return true } catch (ClassNotFoundException e) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy index 66e97b2e83..4984a2e0a0 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy @@ -63,12 +63,24 @@ class MethodBuilder { static String methodName(ContractMetadata contract, File stubsFile, Contract stubContent) { if (stubContent.name) { - return NamesUtil.camelCase(NamesUtil.convertIllegalPackageChars(stubContent.name)) + String name = NamesUtil.camelCase(NamesUtil.convertIllegalPackageChars(stubContent.name)) + if (log.isDebugEnabled()) { + log.debug("Overriding the default test name with [" + name + "]") + } + return name } else if (contract.convertedContract.size() > 1) { int index = contract.convertedContract.findIndexOf { it == stubContent} - return "${camelCasedMethodFromFileName(stubsFile)}_${index}" + String name = "${camelCasedMethodFromFileName(stubsFile)}_${index}" + if (log.isDebugEnabled()) { + log.debug("Scenario found. The method name will be [" + name + "]") + } + return name } - return camelCasedMethodFromFileName(stubsFile) + String name = camelCasedMethodFromFileName(stubsFile) + if (log.isDebugEnabled()) { + log.debug("The method name will be [" + name + "]") + } + return name } private static String camelCasedMethodFromFileName(File stubsFile) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy new file mode 100644 index 0000000000..df528651fe --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy @@ -0,0 +1,177 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 + +/** + * YAML representation of a {@link org.springframework.cloud.contract.spec.Contract} + * + * @since 1.2.1 + * @author Marcin Grzejszczak + */ +@CompileStatic +class YamlContract { + public Request request + public Response response + public Input input + public OutputMessage outputMessage + public String description + public String label + public String name + public Integer priority + public boolean ignored + + @CompileStatic + static class Request { + public String method + public String url + public String urlPath + public Map queryParameters = [:] + public Map headers = [:] + public Object body + public String bodyFromFile + public StubMatchers matchers = new StubMatchers() + public Multipart multipart + } + + @CompileStatic + static class Multipart { + public Map params = [:] + public List named = [] + } + + @CompileStatic + static class Named { + public String paramName + public String fileName + public String fileContent + } + + @CompileStatic + static class StubMatchers { + public List body = [] + public List headers = [] + public MultipartStubMatcher multipart + } + + @CompileStatic + static class BodyStubMatcher { + public String path + public StubMatcherType type + public String value + public PredefinedRegex predefined + } + + @CompileStatic + static class MultipartStubMatcher { + public List params = [] + public List named = [] + } + + @CompileStatic + static class MultipartNamedStubMatcher { + public String paramName + public ValueMatcher fileName + public ValueMatcher fileContent + } + + @CompileStatic + static class ValueMatcher { + public String regex + public PredefinedRegex predefined + } + + @CompileStatic + static class BodyTestMatcher { + public String path + public TestMatcherType type + public String value + public Integer minOccurrence + public Integer maxOccurrence + public PredefinedRegex predefined + } + + @CompileStatic + static class KeyValueMatcher { + public String key + public String regex + public PredefinedRegex predefined + } + + @CompileStatic + static class TestHeaderMatcher { + public String key + public String regex + public String command + public PredefinedRegex predefined + } + + @CompileStatic + static enum PredefinedRegex { + only_alpha_unicode, number, any_boolean, ip_address, hostname, + email, url, uuid, iso_date, iso_date_time, iso_time, + iso_8601_with_offset, non_empty, non_blank + } + + @CompileStatic + static enum StubMatcherType { + by_date, by_time, by_timestamp, by_regex, by_equality + } + + @CompileStatic + static enum TestMatcherType { + by_date, by_time, by_timestamp, by_regex, by_equality, by_type, by_command + } + + @CompileStatic + static class Response { + public int status + public Map headers = [:] + public Object body + public String bodyFromFile + public TestMatchers matchers = new TestMatchers() + public Boolean async + } + + @CompileStatic + static class TestMatchers { + public List body = [] + public List headers = [] + } + + @CompileStatic + static class Input { + public String messageFrom + public String triggeredBy + public Map messageHeaders = [:] + public Object messageBody + public String messageBodyFromFile + public String assertThat + public StubMatchers matchers = new StubMatchers() + } + + @CompileStatic + static class OutputMessage { + public String sentTo + public Map headers = [:] + public Object body + public String bodyFromFile + public String assertThat + public TestMatchers matchers = new TestMatchers() + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy new file mode 100644 index 0000000000..ab600fca7a --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy @@ -0,0 +1,556 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 java.nio.file.Files +import java.util.regex.Pattern + +import com.fasterxml.jackson.databind.ObjectMapper +import groovy.transform.CompileStatic +import org.yaml.snakeyaml.Yaml + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Headers +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.MatchingTypeValue +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.RegexPatterns +import org.springframework.cloud.contract.verifier.converter.YamlContract.BodyStubMatcher +import org.springframework.cloud.contract.verifier.converter.YamlContract.BodyTestMatcher +import org.springframework.cloud.contract.verifier.converter.YamlContract.Input +import org.springframework.cloud.contract.verifier.converter.YamlContract.Named +import org.springframework.cloud.contract.verifier.converter.YamlContract.OutputMessage +import org.springframework.cloud.contract.verifier.converter.YamlContract.PredefinedRegex +import org.springframework.cloud.contract.verifier.converter.YamlContract.Request +import org.springframework.cloud.contract.verifier.converter.YamlContract.Response +import org.springframework.cloud.contract.verifier.converter.YamlContract.KeyValueMatcher +import org.springframework.cloud.contract.verifier.converter.YamlContract.StubMatcherType +import org.springframework.cloud.contract.verifier.converter.YamlContract.StubMatchers +import org.springframework.cloud.contract.verifier.converter.YamlContract.TestHeaderMatcher +import org.springframework.cloud.contract.verifier.converter.YamlContract.TestMatcherType +import org.springframework.cloud.contract.verifier.util.MapConverter + +/** + * Simple converter from and to a {@link YamlContract} to a collection of {@link Contract} + * + * @since 1.2.1 + * @author Marcin Grzejszczak + */ +@CompileStatic +class YamlContractConverter implements ContractConverter> { + + public static final YamlContractConverter INSTANCE = new YamlContractConverter() + + @Override + boolean isAccepted(File file) { + String name = file.getName() + return name.endsWith(".yml") || name.endsWith(".yaml") + } + + @Override + Collection convertFrom(File contractFile) { + ClassLoader classLoader = YamlContractConverter.getClassLoader() + ObjectMapper mapper = new ObjectMapper() + try { + return new Yaml().loadAll(Files.newInputStream(contractFile.toPath())).collect { + YamlContract yamlContract = mapper.convertValue(it, YamlContract.class); + Thread.currentThread().setContextClassLoader(updatedClassLoader(contractFile.getParentFile(), classLoader)) + return Contract.make { + if (yamlContract.description) description(yamlContract.description) + if (yamlContract.label) label(yamlContract.label) + if (yamlContract.name) name(yamlContract.name) + if (yamlContract.priority) priority(yamlContract.priority) + if (yamlContract.ignored) ignored() + if (yamlContract.request?.method) { + request { + method(yamlContract.request?.method) + if (yamlContract.request?.url) { + url(yamlContract.request?.url) { + if (yamlContract.request.queryParameters) { + queryParameters { + yamlContract.request.queryParameters.each { String key, Object value -> + if (value instanceof List) { + ((List) value).each { + parameter(key, it) + } + } else { + parameter(key, value) + } + } + } + } + } + } + if (yamlContract.request?.urlPath) { + urlPath(yamlContract.request?.urlPath) { + if (yamlContract.request.queryParameters) { + queryParameters { + yamlContract.request.queryParameters.each { String key, Object value -> + if (value instanceof List) { + ((List) value).each { + parameter(key, it) + } + } else { + parameter(key, value) + } + } + } + } + } + } + if (yamlContract.request?.headers) { + headers { + yamlContract.request?.headers?.each { String key, Object value -> + KeyValueMatcher matcher = yamlContract.request.matchers.headers.find { it.key == key } + if (value instanceof List) { + ((List) value).each { + Object clientValue = clientValue(it, matcher, key) + header(key, new DslProperty(clientValue, it)) + } + } else { + Object clientValue = clientValue(value, matcher, key) + header(key, new DslProperty(clientValue, value)) + } + } + } + } + if (yamlContract.request.body) body(yamlContract.request.body) + if (yamlContract.request.bodyFromFile) body(file(yamlContract.request.bodyFromFile)) + if (yamlContract.request.multipart) { + Map multipartMap = [:] + Map multiPartParams = yamlContract.request.multipart.params.collectEntries { String paramKey, String paramValue -> + KeyValueMatcher matcher = yamlContract.request.matchers.multipart.params.find { + it.key == paramKey + } + Object value = paramValue + if (matcher) { + value = matcher.regex ? Pattern.compile(matcher.regex) : + predefinedToPattern(matcher.predefined) + } + return [(paramKey), new DslProperty<>(value, paramValue)] + } as Map + multipartMap.putAll(multiPartParams) + yamlContract.request.multipart.named.each { Named namedParam -> + YamlContract.MultipartNamedStubMatcher matcher = yamlContract.request.matchers.multipart.named.find { + it.paramName == namedParam.paramName + } + Object fileNameValue = namedParam.fileName + Object fileContentValue = namedParam.fileContent + if (matcher && matcher.fileName) { + fileNameValue = matcher.fileName.regex ? Pattern.compile(matcher.fileName.regex) : + predefinedToPattern(matcher.fileName.predefined) + } + if (matcher && matcher.fileContent) { + fileContentValue = matcher.fileContent.regex ? Pattern.compile(matcher.fileContent.regex) : + predefinedToPattern(matcher.fileContent.predefined) + } + multipartMap.put(namedParam.paramName, new NamedProperty(new DslProperty<>(fileNameValue, namedParam.fileName), + new DslProperty<>(fileContentValue, namedParam.fileContent))) + } + multipart(multipartMap) + } + stubMatchers { + yamlContract.request.matchers?.body?.each { BodyStubMatcher matcher -> + MatchingTypeValue value = null + switch (matcher.type) { + case StubMatcherType.by_date: + value = byDate() + break + case StubMatcherType.by_time: + value = byTime() + break + case StubMatcherType.by_timestamp: + value = byTimestamp() + break + case StubMatcherType.by_regex: + String regex = matcher.value + if (matcher.predefined) { + regex = predefinedToPattern(matcher.predefined).pattern() + } + value = byRegex(regex) + break + case StubMatcherType.by_equality: + value = byEquality() + break + } + jsonPath(matcher.path, value) + } + } + } + response { + status(yamlContract.response.status) + headers { + yamlContract.response?.headers?.each { String key, Object value -> + TestHeaderMatcher matcher = yamlContract.response.matchers.headers.find { it.key == key } + if (value instanceof List) { + ((List) value).each { + Object serverValue = serverValue(it, matcher, key) + header(key, new DslProperty(it, serverValue)) + } + } else { + Object serverValue = serverValue(value, matcher, key) + header(key, new DslProperty(value, serverValue)) + } + } + } + if (yamlContract.response.body) body(yamlContract.response.body) + if (yamlContract.response.bodyFromFile) body(file(yamlContract.response.bodyFromFile)) + if (yamlContract.response.async) async() + testMatchers { + yamlContract.response?.matchers?.body?.each { BodyTestMatcher testMatcher -> + MatchingTypeValue value = null + switch (testMatcher.type) { + case TestMatcherType.by_date: + value = byDate() + break + case TestMatcherType.by_time: + value = byTime() + break + case TestMatcherType.by_timestamp: + value = byTimestamp() + break + case TestMatcherType.by_regex: + String regex = testMatcher.value + if (testMatcher.predefined) { + regex = predefinedToPattern(testMatcher.predefined).pattern() + } + value = byRegex(regex) + break + case TestMatcherType.by_equality: + value = byEquality() + break + case TestMatcherType.by_type: + value = byType() { + if (testMatcher.minOccurrence != null) minOccurrence(testMatcher.minOccurrence) + if (testMatcher.maxOccurrence != null) maxOccurrence(testMatcher.maxOccurrence) + } + break + case TestMatcherType.by_command: + value = byCommand(testMatcher.value) + break + } + jsonPath(testMatcher.path, value) + } + } + } + } + if (yamlContract.input) { + input { + if (yamlContract.input.messageFrom) messageFrom(yamlContract.input.messageFrom) + if (yamlContract.input.assertThat) assertThat(yamlContract.input.assertThat) + if (yamlContract.input.triggeredBy) triggeredBy(yamlContract.input.triggeredBy) + messageHeaders { + yamlContract.input?.messageHeaders?.each { String key, Object value -> + KeyValueMatcher matcher = yamlContract.input.matchers?.headers?.find { it.key == key } + Object clientValue = clientValue(value, matcher, key) + header(key, new DslProperty(clientValue, value)) + } + } + if (yamlContract.input.messageBody) messageBody(yamlContract.input.messageBody) + if (yamlContract.input.messageBodyFromFile) messageBody(file(yamlContract.input.messageBodyFromFile)) + stubMatchers { + yamlContract.input.matchers.body?.each { BodyStubMatcher matcher -> + MatchingTypeValue value = null + switch (matcher.type) { + case StubMatcherType.by_date: + value = byDate() + break + case StubMatcherType.by_time: + value = byTime() + break + case StubMatcherType.by_timestamp: + value = byTimestamp() + break + case StubMatcherType.by_regex: + value = byRegex(matcher.value) + break + case StubMatcherType.by_equality: + value = byEquality() + break + } + jsonPath(matcher.path, value) + } + } + } + } + OutputMessage outputMsg = yamlContract.outputMessage + if (outputMsg) { + outputMessage { + if (outputMsg.assertThat) assertThat(outputMsg.assertThat) + if (outputMsg.sentTo) sentTo(outputMsg.sentTo) + headers { + outputMsg.headers?.each { String key, Object value -> + TestHeaderMatcher matcher = outputMsg.matchers?.headers?.find { it.key == key } + Object serverValue = serverValue(value, matcher, key) + header(key, new DslProperty(value, serverValue)) + } + } + if (outputMsg.body) body(outputMsg.body) + if (outputMsg.bodyFromFile) body(file(outputMsg.bodyFromFile)) + if (outputMsg.matchers) { + testMatchers { + yamlContract.outputMessage?.matchers?.body?.each { BodyTestMatcher testMatcher -> + MatchingTypeValue value = null + switch (testMatcher.type) { + case TestMatcherType.by_date: + value = byDate() + break + case TestMatcherType.by_time: + value = byTime() + break + case TestMatcherType.by_timestamp: + value = byTimestamp() + break + case TestMatcherType.by_regex: + value = byRegex(testMatcher.value) + break + case TestMatcherType.by_equality: + value = byEquality() + break + case TestMatcherType.by_type: + value = byType() { + if (testMatcher.minOccurrence != null) minOccurrence(testMatcher.minOccurrence) + if (testMatcher.maxOccurrence != null) maxOccurrence(testMatcher.maxOccurrence) + } + break + case TestMatcherType.by_command: + value = byCommand(testMatcher.value) + break + } + jsonPath(testMatcher.path, value) + } + } + } + } + } + } + } + } + catch (FileNotFoundException e) { + throw new IllegalStateException(e) + } + catch (IllegalStateException ise) { + throw ise + } + catch (Exception e1) { + throw new IllegalStateException("Exception occurred while processing the file [" + contractFile + "]", e1) + } finally { + Thread.currentThread().setContextClassLoader(classLoader) + } + } + + protected Object serverValue(Object value, TestHeaderMatcher matcher, String key) { + Object serverValue = value + if (matcher?.regex) { + serverValue = Pattern.compile(matcher.regex) + Pattern pattern = (Pattern) serverValue + assertPatternMatched(pattern, value, key) + } else if (matcher?.predefined) { + Pattern pattern = predefinedToPattern(matcher.predefined) + serverValue = pattern + assertPatternMatched(pattern, value, key) + } else if (matcher?.command) { + serverValue = new ExecutionProperty(matcher.command) + } + return serverValue + } + + protected Object clientValue(Object value, KeyValueMatcher matcher, String key) { + Object clientValue = value + if (matcher?.regex) { + clientValue = Pattern.compile(matcher.regex) + Pattern pattern = (Pattern) clientValue + assertPatternMatched(pattern, value, key) + } else if (matcher?.predefined) { + Pattern pattern = predefinedToPattern(matcher.predefined) + clientValue = pattern + assertPatternMatched(pattern, value, key) + } + return clientValue + } + + private void assertPatternMatched(Pattern pattern, value, String key) { + boolean matches = pattern.matcher(value.toString()).matches() + if (!matches) throw new IllegalStateException("Broken headers! A header with " + + "key [${key}] with value [${value}] is not matched by regex [${pattern.pattern()}]") + } + + protected Pattern predefinedToPattern(PredefinedRegex predefinedRegex) { + RegexPatterns patterns = new RegexPatterns() + switch (predefinedRegex) { + case PredefinedRegex.only_alpha_unicode: + return Pattern.compile(patterns.onlyAlphaUnicode()) + case PredefinedRegex.number: + return Pattern.compile(patterns.number()) + case PredefinedRegex.any_boolean: + return Pattern.compile(patterns.anyBoolean()) + case PredefinedRegex.ip_address: + return Pattern.compile(patterns.ipAddress()) + case PredefinedRegex.hostname: + return Pattern.compile(patterns.hostname()) + case PredefinedRegex.email: + return Pattern.compile(patterns.email()) + case PredefinedRegex.url: + return Pattern.compile(patterns.url()) + case PredefinedRegex.uuid: + return Pattern.compile(patterns.uuid()) + case PredefinedRegex.iso_date: + return Pattern.compile(patterns.isoDate()) + case PredefinedRegex.iso_date_time: + return Pattern.compile(patterns.isoDateTime()) + case PredefinedRegex.iso_time: + return Pattern.compile(patterns.isoTime()) + case PredefinedRegex.iso_8601_with_offset: + return Pattern.compile(patterns.iso8601WithOffset()) + case PredefinedRegex.non_empty: + return Pattern.compile(patterns.nonEmpty()) + case PredefinedRegex.non_blank: + return Pattern.compile(patterns.nonBlank()) + } + } + + protected String file(String relativePath) { + URL resource = Thread.currentThread().getContextClassLoader().getResource(relativePath) + if (resource == null) { + throw new IllegalStateException("File [${relativePath}] is not present") + } + return new File(resource.toURI()).text + } + + protected static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { + ClassLoader urlCl = URLClassLoader + .newInstance([rootFolder.toURI().toURL()] as URL[], classLoader) + Thread.currentThread().setContextClassLoader(urlCl) + return urlCl + } + + @Override + List convertTo(Collection contracts) { + return contracts.collect { Contract contract -> + YamlContract yamlContract = new YamlContract() + if (contract?.request?.method) { + yamlContract.request = new Request() + yamlContract.request.with { + method = contract?.request?.method?.clientValue + url = contract?.request?.url?.clientValue + headers = (contract?.request?.headers as Headers)?.asTestSideMap() + body = MapConverter.getTestSideValues(contract?.request?.body) + matchers = new StubMatchers() + contract?.request?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + matchers.body << new BodyStubMatcher( + path: matcher.path(), + type: stubMatcherType(matcher.matchingType()), + value: matcher.value()?.toString() + ) + } + } + yamlContract.response = new Response() + yamlContract.response.with { + status = contract?.response?.status?.clientValue as Integer + headers = (contract?.response?.headers as Headers)?.asStubSideMap() + body = MapConverter.getStubSideValues(contract?.response?.body) + contract?.response?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + matchers.body << new BodyTestMatcher( + path: matcher.path(), + type: testMatcherType(matcher.matchingType()), + value: matcher.value()?.toString(), + minOccurrence: matcher.minTypeOccurrence(), + maxOccurrence: matcher.maxTypeOccurrence() + ) + } + } + } + if (contract.input) { + yamlContract.input = new Input() + yamlContract.input.assertThat = contract?.input?.assertThat?.toString() + yamlContract.input.triggeredBy = contract?.input?.triggeredBy?.toString() + yamlContract.input.messageHeaders = (contract?.input?.messageHeaders as Headers)?.asTestSideMap() + yamlContract.input.messageBody = MapConverter.getTestSideValues(contract?.input?.messageBody) + yamlContract.input.messageFrom = contract?.input?.messageFrom?.serverValue + yamlContract.input.matchers.body.each { + contract?.input?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + yamlContract.input.matchers.body << new BodyStubMatcher( + path: matcher.path(), + type: stubMatcherType(matcher.matchingType()), + value: matcher.value()?.toString() + ) + } + } + } + if (contract.outputMessage) { + yamlContract.outputMessage = new OutputMessage() + yamlContract.outputMessage.headers = (contract?.outputMessage?.headers as Headers)?.asStubSideMap() + yamlContract.outputMessage.body = MapConverter.getStubSideValues(contract?.outputMessage?.body) + yamlContract.outputMessage.matchers.body.each { + contract?.input?.matchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + yamlContract.outputMessage.matchers.body << new BodyTestMatcher( + path: matcher.path(), + type: testMatcherType(matcher.matchingType()), + value: matcher.value()?.toString(), + minOccurrence: matcher.minTypeOccurrence(), + maxOccurrence: matcher.maxTypeOccurrence() + ) + } + } + } + return yamlContract + } + } + + protected TestMatcherType testMatcherType(MatchingType matchingType) { + switch (matchingType) { + case MatchingType.EQUALITY: + return TestMatcherType.by_equality + case MatchingType.TYPE: + return TestMatcherType.by_type + case MatchingType.COMMAND: + return TestMatcherType.by_command + case MatchingType.DATE: + return TestMatcherType.by_date + case MatchingType.TIME: + return TestMatcherType.by_time + case MatchingType.TIMESTAMP: + return TestMatcherType.by_timestamp + case MatchingType.REGEX: + return TestMatcherType.by_regex + } + return null + } + + protected StubMatcherType stubMatcherType(MatchingType matchingType) { + switch (matchingType) { + case MatchingType.EQUALITY: + return StubMatcherType.by_equality + case MatchingType.TYPE: + case MatchingType.COMMAND: + throw new UnsupportedOperationException("No type for client side") + case MatchingType.DATE: + return StubMatcherType.by_date + case MatchingType.TIME: + return StubMatcherType.by_time + case MatchingType.TIMESTAMP: + return StubMatcherType.by_timestamp + case MatchingType.REGEX: + return StubMatcherType.by_regex + } + return null + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index eecac825f5..1550e13773 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -26,6 +26,8 @@ import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode +import groovy.util.logging.Slf4j + import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.Body import org.springframework.cloud.contract.spec.internal.DslProperty @@ -53,6 +55,7 @@ import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.bu */ @TypeChecked @PackageScope +@Slf4j class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { private final Request request @@ -211,7 +214,12 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { case MatchingStrategy.Type.ABSENT: return WireMock.absent() default: - return WireMock."${value.type.name}"(value.clientValue) + try { + return WireMock."${value.type.name}"(value.clientValue) + } catch (Throwable t) { + log.error("Exception occurred while trying to call WireMock.${value.type.name}(${value.clientValue})", t) + throw t + } } default: return WireMock.equalTo(object.toString()) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy index 37da2a9593..805608c63a 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy @@ -22,6 +22,7 @@ import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.cloud.contract.verifier.converter.YamlContractConverter import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter import org.springframework.core.io.support.SpringFactoriesLoader @@ -101,6 +102,8 @@ class ContractFileScanner { boolean included = includeMatcher ? file.absolutePath.matches(includeMatcher) : true if (contractFile && included) { addContractToTestGeneration(result, files, file, index, ContractVerifierDslConverter.convertAsCollection(baseDir, file)) + } else if (YamlContractConverter.INSTANCE.isAccepted(file) && included) { + addContractToTestGeneration(result, files, file, index, YamlContractConverter.INSTANCE.convertFrom(file)) } else if (!contractFile && included) { addContractToTestGeneration(converters, result, files, file, index) } else { @@ -144,8 +147,13 @@ class ContractFileScanner { if (hasScenarioFilenamePattern(path)) { order = index } - result.put(file.parentFile.toPath(), new ContractMetadata(path, matchesPattern(file, ignoreMatchers), - files.size(), order, convertedContract)) + Path parent = file.parentFile.toPath() + ContractMetadata metadata = new ContractMetadata(path, matchesPattern(file, ignoreMatchers), + files.size(), order, convertedContract) + if (log.isDebugEnabled()) { + log.debug("Creating a contract entry for path [" + path + "] and metadata [" + metadata + "]") + } + result.put(parent, metadata) } private boolean hasScenarioFilenamePattern(Path path) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy deleted file mode 100644 index 92e729cf16..0000000000 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2013-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 - -/** - * Yaml representation of a {@link org.springframework.cloud.contract.spec.Contract} - */ -class YamlContract { - public Request request = new Request() - public Response response = new Response() - - static class Request { - public String method - public String url - public Map headers = [:] - public Map body = [:] - } - - static class Response { - public int status - public Map headers = [:] - public Map body = [:] - } -} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy deleted file mode 100644 index f55114457b..0000000000 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2013-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 java.nio.file.Files - -import groovy.transform.CompileStatic -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.ContractConverter -import org.springframework.cloud.contract.spec.internal.Headers -import org.yaml.snakeyaml.Yaml - -/** - * Simple converter from and to a {@link YamlContract} to a collection of {@link Contract} - */ -@CompileStatic -class YamlContractConverter implements ContractConverter> { - - @Override - public boolean isAccepted(File file) { - String name = file.getName() - return name.endsWith(".yml") || name.endsWith(".yaml") - } - - @Override - public Collection convertFrom(File file) { - try { - YamlContract yamlContract = new Yaml().loadAs( - Files.newInputStream(file.toPath()), YamlContract.class) - return [Contract.make { - request { - method(yamlContract?.request?.method) - url(yamlContract?.request?.url) - headers { - yamlContract?.request?.headers?.each { String key, Object value -> - header(key, value) - } - } - body(yamlContract?.request?.body) - } - response { - status(yamlContract?.response?.status) - headers { - yamlContract?.response?.headers?.each { String key, Object value -> - header(key, value) - } - } - body(yamlContract?.response?.body) - } - }] - } - catch (FileNotFoundException e) { - throw new IllegalStateException(e) - } - } - - @Override - public List convertTo(Collection contracts) { - return contracts.collect { Contract contract -> - YamlContract yamlContract = new YamlContract() - yamlContract.request.with { - method = contract?.request?.method?.clientValue - url = contract?.request?.url?.clientValue - headers = (contract?.request?.headers as Headers)?.asStubSideMap() - body = contract?.request?.body?.clientValue as Map - } - yamlContract.response.with { - status = contract?.response?.status?.clientValue as Integer - headers = (contract?.response?.headers as Headers)?.asStubSideMap() - body = contract?.response?.body?.clientValue as Map - } - return yamlContract - } - } -} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index 5b0545316d..960c232065 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -16,38 +16,388 @@ package org.springframework.cloud.contract.verifier.converter -import org.springframework.cloud.contract.spec.Contract +import java.util.regex.Pattern + +import spock.lang.Shared import spock.lang.Specification +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.RegexPatterns +import org.springframework.cloud.contract.spec.internal.Url +import org.springframework.cloud.contract.verifier.util.MapConverter + /** * @author Marcin Grzejszczak */ class YamlContractConverterSpec extends Specification { - URL ymlUrl = YamlContractConverterSpec.getResource("/contract.yml") - File yml = new File(ymlUrl.toURI()) + @Shared URL ymlUrl = YamlContractConverterSpec.getResource("/yml/contract.yml") + @Shared File ymlWithRest = new File(ymlUrl.toURI()) + @Shared URL ymlUrl2 = YamlContractConverterSpec.getResource("/yml/contract_rest.yml") + @Shared File ymlWithRest2 = new File(ymlUrl2.toURI()) + @Shared URL ymlUrl3 = YamlContractConverterSpec.getResource("/yml/contract_rest_with_path.yml") + @Shared File ymlWithRest3 = new File(ymlUrl3.toURI()) + URL ymlMsgUrl = YamlContractConverterSpec.getResource("/yml/contract_message.yml") + File ymlMessaging = new File(ymlMsgUrl.toURI()) + URL ymlMsgMethodUrl = YamlContractConverterSpec.getResource("/yml/contract_message_method.yml") + File ymlMessagingMethod = new File(ymlMsgMethodUrl.toURI()) + URL ymlMsgMsgUrl = YamlContractConverterSpec.getResource("/yml/contract_message_input_message.yml") + File ymlMessagingMsg = new File(ymlMsgMsgUrl.toURI()) + URL ymlBodyFile = YamlContractConverterSpec.getResource("/yml/contract_from_file.yml") + File ymlBody = new File(ymlBodyFile.toURI()) + URL ymlReferenceFile = YamlContractConverterSpec.getResource("/yml/contract_reference_request.yml") + File ymlReference = new File(ymlReferenceFile.toURI()) + URL ymlMatchersFile = YamlContractConverterSpec.getResource("/yml/contract_matchers.yml") + File ymlMatchers = new File(ymlMatchersFile.toURI()) + URL ymlMultipleFile = YamlContractConverterSpec.getResource("/yml/multiple_contracts.yml") + File ymlMultiple = new File(ymlMultipleFile.toURI()) YamlContractConverter converter = new YamlContractConverter() - def "should convert YAML to DSL"() { + def "should convert YAML with REST to DSL for [#yamlFile]"() { given: + assert converter.isAccepted(yamlFile) + when: + Collection contracts = converter.convertFrom(yamlFile) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.description == "Some description" + contract.name == "some name" + contract.priority == 8 + contract.ignored == true + Url url = yamlFile == ymlWithRest3 ? + contract.request.urlPath : contract.request.url + url.clientValue == "/foo" + url.queryParameters.parameters[0].name == "a" + url.queryParameters.parameters[0].serverValue == "b" + url.queryParameters.parameters[1].name == "b" + url.queryParameters.parameters[1].serverValue == "c" + contract.request.method.clientValue == "PUT" + contract.request.headers.entries.find { it.name == "foo" && + ((Pattern) it.clientValue).pattern == "bar" && it.serverValue == "bar" } + contract.request.headers.entries.find { it.name == "fooReq" && + it.serverValue == "baz" } + contract.request.body.clientValue == [foo: "bar"] + contract.request.matchers.jsonPathRegexMatchers[0].path() == '$.foo' + contract.request.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.request.matchers.jsonPathRegexMatchers[0].value() == 'bar' + and: + contract.response.status.clientValue == 200 + contract.response.headers.entries.find { it.name == "foo2" && + ((Pattern) it.serverValue).pattern == "bar" && it.clientValue == "bar" } + contract.response.headers.entries.find { it.name == "foo3" && + ((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)" } + contract.response.headers.entries.find { it.name == "fooRes" && + it.clientValue == "baz" } + contract.response.body.clientValue == [foo2: "bar", foo3: "baz"] + contract.response.matchers.jsonPathRegexMatchers[0].path() == '$.foo2' + contract.response.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.response.matchers.jsonPathRegexMatchers[0].value() == 'bar' + contract.response.matchers.jsonPathRegexMatchers[1].path() == '$.foo3' + contract.response.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND + contract.response.matchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)') + where: + yamlFile << [ymlWithRest, ymlWithRest2, ymlWithRest3] + } + + def "should convert YAML with REST to DSL with advanced request referencing"() { + given: + assert converter.isAccepted(ymlReference) + when: + Collection contracts = converter.convertFrom(ymlReference) + then: + contracts.size() == 1 + Contract contract = contracts.first() + Url url = contract.request.url + url.clientValue == "/api/v1/xxxx" + url.queryParameters.parameters[0].name == "foo" + url.queryParameters.parameters[0].serverValue == "bar" + url.queryParameters.parameters[1].name == "foo" + url.queryParameters.parameters[1].serverValue == "bar2" + contract.request.method.clientValue == "GET" + contract.request.headers.entries.findAll { it.name == "Authorization" } + .collect { it.clientValue } == ["secret", "secret2"] + contract.request.body.clientValue == [foo: "bar", baz: 5] + and: + contract.response.status.clientValue == 200 + contract.response.headers.entries + .find { it.name == "Authorization" }.clientValue == '''foo {{{ request.headers.Authorization.0 }}} bar''' + with(MapConverter.getTestSideValues(contract.response.body)) { + it.url == "{{{ request.url }}}" + it.path == "{{{ request.path }}}" + it.pathIndex == "{{{ request.path.1 }}}" + it.param == "{{{ request.query.foo }}}" + it.paramIndex == "{{{ request.query.foo.1 }}}" + it.authorization == "{{{ request.headers.Authorization.0 }}}" + it.authorization2 == "{{{ request.headers.Authorization.1 }}" + it.fullBody == "{{{ request.body }}}" + it.responseFoo == '''{{{ jsonpath this '$.foo' }}}''' + it.responseBaz == '''{{{ jsonpath this '$.baz' }}}''' + it.responseBaz2 == '''Bla bla {{{ jsonpath this '$.foo' }}} bla bla''' + } + } + + def "should convert YAML with REST matchers to DSL"() { + given: + assert converter.isAccepted(ymlMatchers) + when: + Collection contracts = converter.convertFrom(ymlMatchers) + then: + contracts.size() == 1 + Contract contract = contracts.first() + RegexPatterns patterns = new RegexPatterns() + contract.request.headers.entries.find { it.name == "Content-Type" && + ((Pattern) it.clientValue).pattern == "application/json.*" && it.serverValue == "application/json" } + contract.request.matchers.jsonPathRegexMatchers[0].path() == '$.duck' + contract.request.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.request.matchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}' + contract.request.matchers.jsonPathRegexMatchers[1].path() == '$.duck' + contract.request.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY + contract.request.matchers.jsonPathRegexMatchers[2].path() == '$.alpha' + contract.request.matchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX + contract.request.matchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode() + contract.request.matchers.jsonPathRegexMatchers[3].path() == '$.alpha' + contract.request.matchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY + contract.request.matchers.jsonPathRegexMatchers[4].path() == '$.number' + contract.request.matchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX + contract.request.matchers.jsonPathRegexMatchers[4].value() == patterns.number() + contract.request.matchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' + contract.request.matchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX + contract.request.matchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean() + contract.request.matchers.jsonPathRegexMatchers[6].path() == '$.date' + contract.request.matchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE + contract.request.matchers.jsonPathRegexMatchers[6].value() == patterns.isoDate() + contract.request.matchers.jsonPathRegexMatchers[7].path() == '$.dateTime' + contract.request.matchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP + contract.request.matchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime() + contract.request.matchers.jsonPathRegexMatchers[8].path() == '$.time' + contract.request.matchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME + contract.request.matchers.jsonPathRegexMatchers[8].value() == patterns.isoTime() + contract.request.matchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']" + contract.request.matchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY + and: + contract.response.status.clientValue == 200 + contract.response.matchers.jsonPathRegexMatchers[0].path() == '$.duck' + contract.response.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.response.matchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}' + contract.response.matchers.jsonPathRegexMatchers[1].path() == '$.duck' + contract.response.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY + contract.response.matchers.jsonPathRegexMatchers[2].path() == '$.alpha' + contract.response.matchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX + contract.response.matchers.jsonPathRegexMatchers[2].value() == patterns.onlyAlphaUnicode() + contract.response.matchers.jsonPathRegexMatchers[3].path() == '$.alpha' + contract.response.matchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY + contract.response.matchers.jsonPathRegexMatchers[4].path() == '$.number' + contract.response.matchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX + contract.response.matchers.jsonPathRegexMatchers[4].value() == patterns.number() + contract.response.matchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' + contract.response.matchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX + contract.response.matchers.jsonPathRegexMatchers[5].value() == patterns.anyBoolean() + contract.response.matchers.jsonPathRegexMatchers[6].path() == '$.date' + contract.response.matchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE + contract.response.matchers.jsonPathRegexMatchers[6].value() == patterns.isoDate() + contract.response.matchers.jsonPathRegexMatchers[7].path() == '$.dateTime' + contract.response.matchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP + contract.response.matchers.jsonPathRegexMatchers[7].value() == patterns.isoDateTime() + contract.response.matchers.jsonPathRegexMatchers[8].path() == '$.time' + contract.response.matchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME + contract.response.matchers.jsonPathRegexMatchers[8].value() == patterns.isoTime() + contract.response.matchers.jsonPathRegexMatchers[9].path() == '$.valueWithTypeMatch' + contract.response.matchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin' + contract.response.matchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1 + contract.response.matchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax' + contract.response.matchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3 + contract.response.matchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax' + contract.response.matchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1 + contract.response.matchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3 + contract.response.matchers.jsonPathRegexMatchers[13].path() == '$.valueWithMinEmpty' + contract.response.matchers.jsonPathRegexMatchers[13].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[13].minTypeOccurrence() == 0 + contract.response.matchers.jsonPathRegexMatchers[14].path() == '$.valueWithMaxEmpty' + contract.response.matchers.jsonPathRegexMatchers[14].matchingType() == MatchingType.TYPE + contract.response.matchers.jsonPathRegexMatchers[14].maxTypeOccurrence() == 0 + contract.response.matchers.jsonPathRegexMatchers[15].path() == '$.duck' + contract.response.matchers.jsonPathRegexMatchers[15].matchingType() == MatchingType.COMMAND + contract.response.matchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') + } + + def "should convert YAML with REST with response from request"() { + given: + assert converter.isAccepted(ymlBody) + when: + Collection contracts = converter.convertFrom(ymlBody) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.request.body.clientValue == '''{ "hello" : "request" }''' + and: + contract.response.body.clientValue == '''{ "hello" : "response" }''' + } + + def "should convert YAML with REST with multipart"() { + given: + URL ymlUrl = YamlContractConverterSpec.getResource("/yml/contract_multipart.yml") + File yml = new File(ymlUrl.toURI()) + and: assert converter.isAccepted(yml) when: Collection contracts = converter.convertFrom(yml) then: contracts.size() == 1 Contract contract = contracts.first() - contract.request.url.clientValue == "/foo" - contract.request.method.clientValue == "PUT" - contract.request.headers.entries.find { it.name == "foo" && it.clientValue == "bar" } - contract.request.body.clientValue == [foo: "bar"] - contract.response.status.clientValue == 200 - contract.response.headers.entries.find { it.name == "foo2" && it.clientValue == "bar" } - contract.response.body.clientValue == [foo2: "bar"] + RegexPatterns patterns = new RegexPatterns() + def stubSide = MapConverter.getStubSideValues(contract.request.multipart) + stubSide.formParameter.pattern() == ".+" + stubSide.someBooleanParameter.pattern() == patterns.anyBoolean() + def testSide = MapConverter.getTestSideValues(contract.request.multipart) + testSide.formParameter == '"formParameterValue"' + testSide.someBooleanParameter == "true" + ((NamedProperty) testSide.file).name.serverValue == "filename.csv" + ((NamedProperty) testSide.file).name.clientValue.pattern() == patterns.nonEmpty() + ((NamedProperty) testSide.file).value.serverValue == "file content" + ((NamedProperty) testSide.file).value.clientValue.pattern() == patterns.nonEmpty() + and: + contract.response.status.serverValue == 200 + } + + def "should convert YAML with messaging to DSL"() { + given: + assert converter.isAccepted(ymlMessaging) + when: + Collection contracts = converter.convertFrom(ymlMessaging) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.description == "Some description" + contract.name == "some name" + contract.label == "some_label" + contract.ignored == true + contract.input.assertThat.toString() == "bar()" + contract.input.messageFrom.serverValue == "foo" + contract.input.triggeredBy.toString() == "foo()" + contract.input.messageHeaders.entries.find { it.name == "foo" && + ((Pattern) it.clientValue).pattern == "bar" && it.serverValue == "bar" } + contract.input.messageBody.clientValue == [foo: "bar"] + contract.input.matchers.jsonPathRegexMatchers[0].path() == '$.bar' + contract.input.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.input.matchers.jsonPathRegexMatchers[0].value() == 'bar' + and: + contract.outputMessage.assertThat.toString() == "baz()" + contract.outputMessage.headers.entries.find { it.name == "foo2" && + ((Pattern) it.serverValue).pattern == "bar" && it.clientValue == "bar" } + contract.outputMessage.headers.entries.find { it.name == "foo3" && + ((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)" } + contract.outputMessage.headers.entries.find { it.name == "fooRes" && + it.clientValue == "baz" } + contract.outputMessage.body.clientValue == [foo2: "bar", foo3: "baz"] + contract.outputMessage.matchers.jsonPathRegexMatchers[0].path() == '$.foo2' + contract.outputMessage.matchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX + contract.outputMessage.matchers.jsonPathRegexMatchers[0].value() == 'bar' + contract.outputMessage.matchers.jsonPathRegexMatchers[1].path() == '$.foo3' + contract.outputMessage.matchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND + contract.outputMessage.matchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)') + } + + def "should convert YAML with messaging triggered by a method to DSL"() { + given: + assert converter.isAccepted(ymlMessagingMethod) + when: + Collection contracts = converter.convertFrom(ymlMessagingMethod) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.description == "Some description" + contract.label == "some_label" + contract.input.triggeredBy.toString() == "bookReturnedTriggered()" + and: + contract.outputMessage.sentTo.clientValue == "output" + contract.outputMessage.headers.entries.find { + it.name == "BOOK-NAME" && it.clientValue == "foo" } + contract.outputMessage.body.clientValue == [bookName: "foo"] + } + + def "should convert YAML with messaging triggered by a message to DSL"() { + given: + assert converter.isAccepted(ymlMessagingMsg) + when: + Collection contracts = converter.convertFrom(ymlMessagingMsg) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.description == "Some description" + contract.label == "some_label" + contract.input.messageFrom.serverValue == "input" + contract.input.messageHeaders.entries.find { it.name == "sample" && + it.serverValue == "header" } + contract.input.messageBody.clientValue == [bookName: "foo"] + and: + contract.outputMessage.sentTo.clientValue == "output" + contract.outputMessage.headers.entries.find { + it.name == "BOOK-NAME" && it.clientValue == "foo" } + contract.outputMessage.body.clientValue == [bookName: "foo"] + } + + def "should assert request headers when converting YAML to DSL"() { + given: + File yml = new File(YamlContractConverterSpec.getResource("/yml/contract_broken_request_headers.yml").toURI()) + and: + assert converter.isAccepted(yml) + when: + converter.convertFrom(yml) + then: + IllegalStateException e = thrown(IllegalStateException) + e.message.contains("Broken headers! A header with key [foo] with value [bar] is not matched by regex [barrrr]") + } + + def "should assert response headers when converting YAML to DSL"() { + given: + File yml = new File(YamlContractConverterSpec.getResource("/yml/contract_broken_response_headers.yml").toURI()) + and: + assert converter.isAccepted(yml) + when: + converter.convertFrom(yml) + then: + IllegalStateException e = thrown(IllegalStateException) + e.message.contains("Broken headers! A header with key [foo2] with value [bar] is not matched by regex [barrrr]") + } + + def "should parse multiple documents into a list of contracts"() { + given: + assert converter.isAccepted(ymlMultiple) + when: + Collection contracts = converter.convertFrom(ymlMultiple) + then: + contracts.size() == 2 + and: + contracts.first().request.url.clientValue == "/users/1" + contracts.last().request.url.clientValue == "/users/2" + } + + def "should parse messaging contract for [#file]"() { + given: + assert converter.isAccepted(file) + when: + Collection contracts = converter.convertFrom(file) + then: + contracts.size() == 1 + and: + contracts.first().input != null || contracts.first().outputMessage != null + where: + file << [1,2,3].collect { + new File(YamlContractConverterSpec.getResource("/yml/contract_message_scenario${it}.yml").toURI()) + } } def "should convert DSL to YAML"() { given: - assert converter.isAccepted(yml) + assert converter.isAccepted(ymlWithRest) and: List contracts = [Contract.make { request { diff --git a/spring-cloud-contract-verifier/src/test/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/src/test/resources/META-INF/spring.factories index 92ce449691..6a7c8460f0 100644 --- a/spring-cloud-contract-verifier/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-contract-verifier/src/test/resources/META-INF/spring.factories @@ -1,7 +1,3 @@ -# Converters -org.springframework.cloud.contract.spec.ContractConverter=\ -org.springframework.cloud.contract.verifier.converter.YamlContractConverter - # tag::extension[] org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions=\ org.springframework.cloud.contract.verifier.dsl.wiremock.TestWireMockExtensions diff --git a/spring-cloud-contract-verifier/src/test/resources/contract.yml b/spring-cloud-contract-verifier/src/test/resources/contract.yml index 783dafde0e..d50e335f0b 100644 --- a/spring-cloud-contract-verifier/src/test/resources/contract.yml +++ b/spring-cloud-contract-verifier/src/test/resources/contract.yml @@ -1,13 +1,45 @@ +description: Some description +name: some name +priority: 8 +ignored: true request: url: /foo + queryParameters: + a: b + b: c method: PUT headers: foo: bar + fooReq: baz body: foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar response: status: 200 headers: foo2: bar + foo3: foo33 + fooRes: baz body: - foo2: bar \ No newline at end of file + foo2: bar + foo3: baz + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml new file mode 100644 index 0000000000..0e25f7399a --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract.yml @@ -0,0 +1,69 @@ +#tag::description[] +description: Some description +#end::description[] +#tag::name[] +name: some name +#end::name[] +#tag::priority[] +priority: 8 +#end::priority[] +#tag::ignored[] +ignored: true +#end::ignored[] +#tag::request[] +request: +#end::request[] +#tag::request_obligatory[] + method: PUT + url: /foo +#end::request_obligatory[] +#tag::query_params[] + queryParameters: + a: b + b: c +#tag::query_params[] +#tag::headers[] + headers: + foo: bar + fooReq: baz +#end::headers[] +#tag::body[] + body: + foo: bar +#end::body[] +#tag::request_matcher[] + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +#end::request_matcher[] +#tag::response[] +response: +#end::response[] +#tag::response_obligatory[] + status: 200 +#end::response_obligatory[] + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_request_headers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_request_headers.yml new file mode 100644 index 0000000000..75bacd859d --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_request_headers.yml @@ -0,0 +1,34 @@ +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: barrrr +response: + status: 200 + headers: + foo2: bar + fooRes: baz + body: + foo2: bar + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + headers: + - key: foo2 + regex: bar \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_response_headers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_response_headers.yml new file mode 100644 index 0000000000..4683e4cf33 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_broken_response_headers.yml @@ -0,0 +1,12 @@ +request: + url: /foo + method: PUT +response: + status: 200 + headers: + foo2: bar + fooRes: baz + matchers: + headers: + - key: foo2 + regex: barrrr \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_from_file.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_from_file.yml new file mode 100644 index 0000000000..3743ce3f1a --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_from_file.yml @@ -0,0 +1,7 @@ +request: + method: GET + url: /foo + bodyFromFile: request.json +response: + status: 200 + bodyFromFile: response.json \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml new file mode 100644 index 0000000000..a9c8e9b161 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml @@ -0,0 +1,124 @@ +request: + method: GET + urlPath: /get + body: + duck: 123 + alpha: "abc" + number: 123 + aBoolean: true + date: "2017-01-01" + dateTime: "2017-01-01T01:23:45" + time: "01:02:34" + valueWithoutAMatcher: "foo" + valueWithTypeMatch: "string" + key: + "complex.key": 'foo' + matchers: + headers: + - key: Content-Type + regex: "application/json.*" + body: + - path: $.duck + type: by_regex + value: "[0-9]{3}" + - path: $.duck + type: by_equality + - path: $.alpha + type: by_regex + predefined: only_alpha_unicode + - path: $.alpha + type: by_equality + - path: $.number + type: by_regex + predefined: number + - path: $.aBoolean + type: by_regex + predefined: any_boolean + - path: $.date + type: by_date + - path: $.dateTime + type: by_timestamp + - path: $.time + type: by_time + - path: "$.['key'].['complex.key']" + type: by_equality + headers: + Content-Type: application/json +response: + status: 200 + body: + duck: 123 + alpha: "abc" + number: 123 + aBoolean: true + date: "2017-01-01" + dateTime: "2017-01-01T01:23:45" + time: "01:02:34" + valueWithoutAMatcher: "foo" + valueWithTypeMatch: "string" + valueWithMin: + - 1 + - 2 + - 3 + valueWithMax: + - 1 + - 2 + - 3 + valueWithMinMax: + - 1 + - 2 + - 3 + valueWithMinEmpty: [] + valueWithMaxEmpty: [] + key: + 'complex.key' : 'foo' + matchers: + headers: + - key: Content-Type + regex: "application/json.*" + body: + - path: $.duck + type: by_regex + value: "[0-9]{3}" + - path: $.duck + type: by_equality + - path: $.alpha + type: by_regex + predefined: only_alpha_unicode + - path: $.alpha + type: by_equality + - path: $.number + type: by_regex + predefined: number + - path: $.aBoolean + type: by_regex + predefined: any_boolean + - path: $.date + type: by_date + - path: $.dateTime + type: by_timestamp + - path: $.time + type: by_time + - path: $.valueWithTypeMatch + type: by_type + - path: $.valueWithMin + type: by_type + minOccurrence: 1 + - path: $.valueWithMax + type: by_type + maxOccurrence: 3 + - path: $.valueWithMinMax + type: by_type + minOccurrence: 1 + maxOccurrence: 3 + - path: $.valueWithMinEmpty + type: by_type + minOccurrence: 0 + - path: $.valueWithMaxEmpty + type: by_type + maxOccurrence: 0 + - path: $.duck + type: by_command + value: assertThatValueIsANumber($it) + headers: + Content-Type: application/json \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message.yml new file mode 100644 index 0000000000..5ea2bb9eeb --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message.yml @@ -0,0 +1,43 @@ +description: Some description +label: some_label +name: some name +ignored: true +input: + messageFrom: foo + triggeredBy: foo() + messageHeaders: + foo: bar + messageBody: + foo: bar + assertThat: bar() + matchers: + body: + - path: $.bar + type: by_regex + value: bar + headers: + - key: foo + regex: bar +outputMessage: + sentTo: bar + headers: + foo2: bar + foo3: bar3 + fooRes: baz + body: + foo2: bar + foo3: baz + assertThat: baz() + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_input_message.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_input_message.yml new file mode 100644 index 0000000000..4155e7ef95 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_input_message.yml @@ -0,0 +1,23 @@ +# Human readable description +description: Some description +# Label by means of which the output message can be triggered +label: some_label +# input is a message +input: + messageFrom: input + # has the following body + messageBody: + bookName: 'foo' + # and the following headers + messageHeaders: + sample: 'header' +# 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: + BOOK-NAME: foo \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_method.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_method.yml new file mode 100644 index 0000000000..5cb80ad6e6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_method.yml @@ -0,0 +1,17 @@ +# Human readable description +description: Some description +# Label by means of which the output message can be triggered +label: some_label +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: + BOOK-NAME: foo \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario1.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario1.yml new file mode 100644 index 0000000000..8c1cea9f7d --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario1.yml @@ -0,0 +1,10 @@ +label: some_label +input: + triggeredBy: bookReturnedTriggered +outputMessage: + sentTo: activemq:output + body: + bookName: foo + headers: + BOOK-NAME: foo + contentType: application/json diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario2.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario2.yml new file mode 100644 index 0000000000..6edd133b6f --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario2.yml @@ -0,0 +1,13 @@ +label: some_label +input: + messageFrom: jms:input + messageBody: + bookName: 'foo' + messageHeaders: + sample: header +outputMessage: + sentTo: jms:output + body: + bookName: foo + headers: + BOOK-NAME: foo diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario3.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario3.yml new file mode 100644 index 0000000000..88037cf2fa --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_message_scenario3.yml @@ -0,0 +1,8 @@ +label: some_label +input: + messageFrom: jms:delete + messageBody: + bookName: 'foo' + messageHeaders: + sample: header + assertThat: bookWasDeleted() diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_multipart.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_multipart.yml new file mode 100644 index 0000000000..33b2ee1617 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_multipart.yml @@ -0,0 +1,29 @@ +request: + method: PUT + url: /multipart + headers: + Content-Type: multipart/form-data;boundary=AaB03x + multipart: + params: + # key (parameter name), value (parameter value) pair + formParameter: '"formParameterValue"' + someBooleanParameter: true + named: + - paramName: file + fileName: filename.csv + fileContent: file content + matchers: + multipart: + params: + - key: formParameter + regex: ".+" + - key: someBooleanParameter + predefined: any_boolean + named: + - paramName: file + fileName: + predefined: non_empty + fileContent: + predefined: non_empty +response: + status: 200 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_reference_request.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_reference_request.yml new file mode 100644 index 0000000000..09d43ff13e --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_reference_request.yml @@ -0,0 +1,30 @@ +request: + method: GET + url: /api/v1/xxxx + queryParameters: + foo: + - bar + - bar2 + headers: + Authorization: + - secret + - secret2 + body: + foo: bar + baz: 5 +response: + status: 200 + headers: + Authorization: "foo {{{ request.headers.Authorization.0 }}} bar" + body: + url: "{{{ request.url }}}" + path: "{{{ request.path }}}" + pathIndex: "{{{ request.path.1 }}}" + param: "{{{ request.query.foo }}}" + paramIndex: "{{{ request.query.foo.1 }}}" + authorization: "{{{ request.headers.Authorization.0 }}}" + authorization2: "{{{ request.headers.Authorization.1 }}" + fullBody: "{{{ request.body }}}" + responseFoo: "{{{ jsonpath this '$.foo' }}}" + responseBaz: "{{{ jsonpath this '$.baz' }}}" + responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla" \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml new file mode 100644 index 0000000000..d50e335f0b --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest.yml @@ -0,0 +1,45 @@ +description: Some description +name: some name +priority: 8 +ignored: true +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml new file mode 100644 index 0000000000..1d0ad18e6b --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_with_path.yml @@ -0,0 +1,47 @@ +description: Some description +name: some name +priority: 8 +ignored: true +#tag::url_path[] +request: + method: PUT + urlPath: /foo +#end::url_path[] + queryParameters: + a: b + b: c + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/multiple_contracts.yml b/spring-cloud-contract-verifier/src/test/resources/yml/multiple_contracts.yml new file mode 100644 index 0000000000..f77f3586a0 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/multiple_contracts.yml @@ -0,0 +1,14 @@ +--- +name: should post a user +request: + method: POST + url: /users/1 +response: + status: 200 + +--- +request: + method: POST + url: /users/2 +response: + status: 200 \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/request.json b/spring-cloud-contract-verifier/src/test/resources/yml/request.json new file mode 100644 index 0000000000..8cd6b86c42 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/request.json @@ -0,0 +1 @@ +{ "hello" : "request" } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/response.json b/spring-cloud-contract-verifier/src/test/resources/yml/response.json new file mode 100644 index 0000000000..f90d205256 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/response.json @@ -0,0 +1 @@ +{ "hello" : "response" } \ No newline at end of file