From d6f2227f89242b82be9fabba3f5b18bbb57b18ef Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 2 Mar 2017 10:14:10 +0100 Subject: [PATCH] Referencing request from response (#238) The best situation is to provide fixed values but sometimes you need to reference a request in your response. In order to do this you can profit from the fromRequest() method that allows you to reference a bunch of elements from the HTTP request. You can use the following options: - `fromRequest().url()` - return the request URL - `fromRequest().query(String key)` - return the first query parameter with a given name - `fromRequest().query(String key, int index)` - return the nth query parameter with a given name - `fromRequest().header(String key)` - return the first header with a given name - `fromRequest().header(String key, int index)` - return the nth header with a given name - `fromRequest().body()` - return the full request body - `fromRequest().body(String jsonPath)` - return the element from the request that matches the JSON Path fixes #237 --- README.adoc | 37 +- circle.yml | 6 +- docs/src/main/asciidoc/verifier/contract.adoc | 116 +++++- .../main/asciidoc/verifier/introduction.adoc | 5 + pom.xml | 5 + .../fraud/FraudDetectionController.java | 4 +- .../example/fraud/FraudNameController.java | 73 ++++ .../example/fraud/FraudStatsController.java | 1 - .../java/com/example/fraud/FraudBase.java | 1 - .../java/com/example/fraud/FraudnameBase.java | 17 + .../shouldReturnAFraudForTheName.groovy | 25 ++ .../shouldReturnNonFraudForTheName.groovy | 23 ++ .../contract/spec/ContractTemplate.groovy | 72 ++++ .../contract/spec/internal/Common.groovy | 2 +- .../contract/spec/internal/FromRequest.groovy | 81 ++++ .../HandlebarsContractTemplate.groovy | 69 ++++ .../contract/spec/internal/Response.groovy | 4 + .../wiremock/WireMockHttpServerStub.java | 25 +- spring-cloud-contract-verifier/pom.xml | 19 + .../verifier/builder/BlockBuilder.groovy | 5 + ...kMethodRequestProcessingBodyBuilder.groovy | 3 + .../builder/MessagingMethodBodyBuilder.groovy | 9 +- .../verifier/builder/MethodBodyBuilder.groovy | 70 +++- ...kMethodRequestProcessingBodyBuilder.groovy | 5 +- .../RequestProcessingMethodBodyBuilder.groovy | 24 +- .../SpockMessagingMethodBodyBuilder.groovy | 3 + ...kMethodRequestProcessingBodyBuilder.groovy | 7 + .../TestSideRequestTemplateModel.groovy | 83 ++++ .../handlebars/HandlebarsEscapeHelper.groovy | 40 ++ .../HandlebarsJsonPathHelper.groovy | 45 +++ .../wiremock/BaseWireMockStubStrategy.groovy | 61 ++- .../WireMockRequestStubStrategy.groovy | 15 +- .../WireMockResponseStubStrategy.groovy | 3 +- .../HandlebarsTemplateProcessor.groovy | 80 ++++ .../template/TemplateProcessor.groovy | 41 ++ .../verifier/util/ContentUtils.groovy | 2 +- .../util/JsonToJsonPathsConverter.groovy | 3 +- .../verifier/util/MapConverter.groovy | 18 +- .../MockMvcMethodBodyBuilderSpec.groovy | 66 ++- .../verifier/dsl/WireMockGroovyDslSpec.groovy | 377 +++++++++++++----- .../wiremock/WireMockStubMappingSpec.groovy | 18 + 41 files changed, 1407 insertions(+), 156 deletions(-) create mode 100644 samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java create mode 100644 samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java create mode 100644 samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy create mode 100644 samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy diff --git a/README.adoc b/README.adoc index 94d6ca3ec1..a88022179c 100644 --- a/README.adoc +++ b/README.adoc @@ -256,7 +256,7 @@ public class ApplicationTests { public void contextLoads() throws Exception { mockMvc.perform(post("/resource") .content("{\"id\":\"123456\",\"message\":\"Hello World\"}")) - .andExpect(status.isOk()) + .andExpect(status().isOk()) .andDo(verify().jsonPath("$.id") .stub("resource")); } @@ -279,7 +279,7 @@ created stub. Example: public void contextLoads() throws Exception { mockMvc.perform(post("/resource") .content("{\"id\":\"123456\",\"message\":\"Hello World\"}")) - .andExpect(status.isOk()) + .andExpect(status().isOk()) .andDo(verify() .wiremock(WireMock.post( urlPathEquals("/resource")) @@ -397,7 +397,7 @@ the generated document (example for Asciidoc) will contain a formatted contract === Spring Cloud Contract Verifier -:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/1.0.x +:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master === Introduction @@ -947,16 +947,28 @@ That's because all the generated tests will extend that class. Over there you ca ---- package com.example.fraud; -import com.example.fraud.FraudDetectionController; -import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; - import org.junit.Before; +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + public class FraudBase { @Before public void setup() { - RestAssuredMockMvc.standaloneSetup(new FraudDetectionController()); + 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) { @@ -1379,15 +1391,15 @@ Example of a `pom.xml` inside the `server` folder. org.springframework.boot spring-boot-starter-parent - 1.4.2.BUILD-SNAPSHOT + 1.5.0.BUILD-SNAPSHOT UTF-8 1.8 - 1.0.4.BUILD-SNAPSHOT - Camden.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT + Dalston.BUILD-SNAPSHOT true @@ -1601,6 +1613,11 @@ The rest of the flow looks the same. Yes! Check out the https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_different_base_classes_for_contracts[Different base classes for contracts] sections of either Gradle or Maven plugins. +==== Can I reference the request from the response? + +Yes! With version 1.1.0 we've added such a possibility. On the HTTP stub server side we're providing support +for this for WireMock. In case of other HTTP server stubs you'll have to implement the approach yourself. + === Links Here you can find interesting links related to Spring Cloud Contract Verifier: diff --git a/circle.yml b/circle.yml index 9651f9412f..522ab63b4d 100644 --- a/circle.yml +++ b/circle.yml @@ -6,8 +6,8 @@ machine: java: version: oraclejdk8 environment: - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx768m" + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx768m -XX:+HeapDumpOnOutOfMemoryError"' TERM: dumb dependencies: pre: @@ -40,4 +40,4 @@ notify: webhooks: # A list of hook hashes, containing the url field # gitter hook - - url: https://webhooks.gitter.im/e/ece0ece9a76a5af2aa91 \ No newline at end of file + - url: https://webhooks.gitter.im/e/ece0ece9a76a5af2aa91 diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index c7a456a633..ea6e1e0f2e 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -256,6 +256,120 @@ IMPORTANT: You can't use both a String and `execute` to perform concatenation. E To make this work just call `header('Authorization', execute('authToken()'))` and ensure that the `authToken()` method returns everything that you need. +===== Referencing request from response + +The best situation is to provide fixed values but sometimes you need to reference a request in your response. +In order to do this you can profit from the `fromRequest()` method that allows you to reference a bunch +of elements from the HTTP request. You can use the following options: + +- `fromRequest().url()` - return the request URL +- `fromRequest().query(String key)` - return the first query parameter with a given name +- `fromRequest().query(String key, int index)` - return the nth query parameter with a given name +- `fromRequest().header(String key)` - return the first header with a given name +- `fromRequest().header(String key, int index)` - return the nth header with a given name +- `fromRequest().body()` - return the full request body +- `fromRequest().body(String jsonPath)` - return the element from the request that matches the JSON Path + +Let's take a look at the following contract + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=template_contract,indent=0] +---- + +Running a JUnit test generation will lead in creation of a test looking more or less like this + +[source,java,indent=0] +---- + // given: + MockMvcRequestSpecification request = given() + .header("Authorization", "secret") + .header("Authorization", "secret2") + .body("{\"foo\":\"bar\",\"baz\":5}"); + + // when: + ResponseOptions response = given().spec(request) + .queryParam("foo","bar") + .queryParam("foo","bar2") + .get("/api/v1/xxxx"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Authorization")).isEqualTo("foo secret bar"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("url").isEqualTo("/api/v1/xxxx"); + assertThatJson(parsedJson).field("fullBody").isEqualTo("{\"foo\":\"bar\",\"baz\":5}"); + assertThatJson(parsedJson).field("paramIndex").isEqualTo("bar2"); + assertThatJson(parsedJson).field("responseFoo").isEqualTo("bar"); + assertThatJson(parsedJson).field("authorization2").isEqualTo("secret2"); + assertThatJson(parsedJson).field("responseBaz").isEqualTo(5); + assertThatJson(parsedJson).field("responseBaz2").isEqualTo("Bla bla bar bla bla"); + assertThatJson(parsedJson).field("param").isEqualTo("bar"); + assertThatJson(parsedJson).field("authorization").isEqualTo("secret"); +---- + +As you can see elements from the request have been properly referenced in the response. + +The generated WireMock stub will look more or less like this: + +[source,json,indent=0] +---- +{ + "request" : { + "urlPath" : "/api/v1/xxxx", + "method" : "POST", + "headers" : { + "Authorization" : { + "equalTo" : "secret2" + } + }, + "queryParameters" : { + "foo" : { + "equalTo" : "bar2" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.baz == 5)]" + }, { + "matchesJsonPath" : "$[?(@.foo == 'bar')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"url\":\"{{{request.url}}}\",\"param\":\"{{{request.query.foo.[0]}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\",\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\"}", + "headers" : { + "Authorization" : "{{{request.headers.Authorization.[0]}}}" + }, + "transformers" : [ "response-template" ] + } +} +---- + +So sending a request as the one presented in the `request` part of the contract will lead in sending the following +response body + +[source,json,indent=0] +---- +{ + "url" : "/api/v1/xxxx?foo=bar&foo=bar2", + "param" : "bar", + "paramIndex" : "bar2", + "authorization" : "secret", + "authorization2" : "secret2", + "fullBody" : "{\"foo\":\"bar\",\"baz\":5}", + "responseFoo" : "bar", + "responseBaz" : 5, + "responseBaz2" : "Bla bla bar bla bla" +} +---- + +IMPORTANT: This feature will work only with WireMock having version greater or equal to 2.5.1. We're using WireMock's +`response-template` response transformer. It's using Handlebars to convert the Mustache `{{{ }}}` templates into +proper values. Additionally we're registering 2 helper functions. `escapejsonbody` - that escapes the request +body in a format that can be embedded in a JSON. Another is `jsonpath` that for a given parameter knows how to +find an object in the request body. + ===== Dynamic properties in matchers sections If you've been working with https://docs.pact.io/[Pact] this might seem familiar. Quite a few users @@ -924,4 +1038,4 @@ include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/ that way you'll be able to pick a folder with the source of your stubs. IMPORTANT: If you don't provide any implementation then the default one - Aether based that will download stubs from a remote repo -will be picked. If you provide more than one then the first one on the list will be picked. +will be picked. If you provide more than one then the first one on the list will be picked. \ 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 f6871dee08..c57a6780fc 100644 --- a/docs/src/main/asciidoc/verifier/introduction.adoc +++ b/docs/src/main/asciidoc/verifier/introduction.adoc @@ -807,3 +807,8 @@ The rest of the flow looks the same. Yes! Check out the https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_different_base_classes_for_contracts[Different base classes for contracts] sections of either Gradle or Maven plugins. + +==== Can I reference the request from the response? + +Yes! With version 1.1.0 we've added such a possibility. On the HTTP stub server side we're providing support +for this for WireMock. In case of other HTTP server stubs you'll have to implement the approach yourself. diff --git a/pom.xml b/pom.xml index 177eca8bec..c1fd514827 100644 --- a/pom.xml +++ b/pom.xml @@ -137,6 +137,11 @@ pact-jvm-model 2.4.18 + + com.github.jknack + handlebars + 4.0.6 + org.springframework.cloud spring-cloud-contract-dependencies diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java index 82ae6bee85..d172969868 100644 --- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java @@ -1,5 +1,7 @@ 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; @@ -8,8 +10,6 @@ import com.example.fraud.model.FraudCheck; import com.example.fraud.model.FraudCheckResult; import com.example.fraud.model.FraudCheckStatus; -import java.math.BigDecimal; - import static org.springframework.web.bind.annotation.RequestMethod.PUT; @RestController diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java new file mode 100644 index 0000000000..ea6ff8f2c0 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java @@ -0,0 +1,73 @@ +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 static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + + private final FraudVerifier fraudVerifier; + + FraudNameController(FraudVerifier fraudVerifier) { + this.fraudVerifier = fraudVerifier; + } + + @PutMapping( + value = "/frauds/name", + produces = FRAUD_SERVICE_JSON_VERSION_1) + 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/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java index 5b27668600..ab8d933814 100644 --- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java @@ -55,4 +55,3 @@ class Response { this.count = count; } } - diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java index 639d686c2d..8b3ab5877b 100644 --- a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java +++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java @@ -5,7 +5,6 @@ import org.junit.Before; import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; public class FraudBase { - @Before public void setup() { RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(), diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java new file mode 100644 index 0000000000..2b8992819c --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java @@ -0,0 +1,17 @@ +package com.example.fraud; + +import org.junit.Before; + +import com.jayway.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/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy new file mode 100644 index 0000000000..f2f96c2676 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy @@ -0,0 +1,25 @@ +package contracts.fraudname + +org.springframework.cloud.contract.spec.Contract.make { + request { + // highest priority + priority(1) + method PUT() + url '/frauds/name' + body([ + name: "fraud" + ]) + headers { + contentType("application/vnd.fraud.v1+json") + } + } + response { + status 200 + body([ + result: "Sorry ${fromRequest().body('$.name')} but you're a fraud" + ]) + headers { + header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8") + } + } +} \ No newline at end of file diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy new file mode 100644 index 0000000000..eddea317a7 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy @@ -0,0 +1,23 @@ +package contracts.fraudname + +org.springframework.cloud.contract.spec.Contract.make { + request { + method PUT() + url '/frauds/name' + body([ + name: $(anyAlphaUnicode()) + ]) + headers { + contentType("application/vnd.fraud.v1+json") + } + } + response { + status 200 + body([ + result: "Don't worry ${fromRequest().body('$.name')} you're not a fraud" + ]) + headers { + header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8") + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy new file mode 100644 index 0000000000..5d7cd1270c --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy @@ -0,0 +1,72 @@ +package org.springframework.cloud.contract.spec + +/** + * Contract for defining templated responses. + * + * If no implementation is provided then Handlebars will be picked as a default implementation. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +interface ContractTemplate { + + /** + * Handlebars is using the Mustache template thus it looks like this + * {{{ Mustache }}}. In this case the opening template would return {{{ + */ + String openingTemplate() + + /** + * Handlebars is using the Mustache template thus it looks like this + * {{{ Mustache }}}. In this case the closing template would return }}} + */ + String closingTemplate() + + /** + * Returns the template for retrieving a URL from request + */ + String url() + + /** + * Returns the template for retrieving first value of a query parameter e.g. {{{ request.query.search }}} + * @param key + */ + String query(String key) + + /** + * Returns the template for retrieving nth value of a query parameter (zero indexed) e.g. {{{ request.query.search.[5] }}} + * @param key + * @param index + */ + String query(String key, int index) + + /** + * Returns the template for retrieving the first value of a request header e.g. {{{ request.headers.X-Request-Id }}} + * @param key + */ + String header(String key) + + /** + * Returns the template for retrieving the nth value of a request header (zero indexed) e.g. {{{ request.headers.X-Request-Id.[5] }}} + * @param key + * @param index + */ + String header(String key, int index) + + /** + * Request body text (avoid for non-text bodies) e.g. {{{ request.body }}} . The body will not be escaped + * so you won't be able to directly embed it in a JSON for example. + */ + String body() + + /** + * Request body text (avoid for non-text bodies) e.g. {{{ escapejsonbody }}} . The body will not be escaped + * so you will be able to embed it + */ + String escapedBody() + + /** + * Request body text for the given JsonPath. e.g. {{{ jsonpath this '$.a.b.c' }}} + */ + String body(String jsonPath) +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy index e9213bef4a..7db6aca4b9 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy @@ -205,4 +205,4 @@ class Common { void assertThatSidesMatch(Object firstSide, Object secondSide) { // do nothing } -} +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy new file mode 100644 index 0000000000..7c504f7388 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy @@ -0,0 +1,81 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.ContractTemplate +/** + * Helper class to reference the request body parameters + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ + +@CompileStatic +class FromRequest { + + private final ContractTemplate template + + FromRequest() { + this.template = template() + } + + private ContractTemplate template() { + return new HandlebarsContractTemplate() + } + + /** + * URL path and query + */ + DslProperty url() { + return new DslProperty(template.url()) + } + + /** + * First value of a query parameter e.g. request.query.search + * @param key + */ + DslProperty query(String key) { + return new DslProperty(template.query(key)) + } + + /** + * nth value of a query parameter (zero indexed) e.g. request.query.search.[5] + * @param key + * @param index + */ + DslProperty query(String key, int index) { + return new DslProperty(template.query(key, index)) + } + + /** + * First value of a request header e.g. request.headers.X-Request-Id + * @param key + */ + DslProperty header(String key) { + return new DslProperty(template.header(key)) + } + + /** + * nth value of a request header (zero indexed) e.g. request.headers.X-Request-Id + * @param key + */ + DslProperty header(String key, int index) { + return new DslProperty(template.header(key, index)) + } + + /** + * Request body text (avoid for non-text bodies) + */ + DslProperty body() { + return new DslProperty(template.body()) + } + + /** + * Request body text for the given JsonPath + */ + DslProperty body(String jsonPath) { + return new DslProperty(template.body(jsonPath)) + } + +} + + diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy new file mode 100644 index 0000000000..33d49aa348 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy @@ -0,0 +1,69 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.ContractTemplate + +/** + * Represents the structure of templates using Handlebars compatible with + * WireMock template model requirements. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@CompileStatic +class HandlebarsContractTemplate implements ContractTemplate { + + @Override + String openingTemplate() { + return "{{{" + } + + @Override + String closingTemplate() { + return "}}}" + } + + @Override + String url() { + return wrapped("request.url") + } + + @Override + String query(String key) { + return query(key, 0) + } + + @Override + String query(String key, int index) { + return wrapped("request.query.${key}.[${index}]") + } + + @Override + String header(String key) { + return header(key, 0) + } + + @Override + String header(String key, int index) { + return wrapped("request.headers.${key}.[${index}]") + } + + @Override + String body() { + return wrapped("request.body") + } + + @Override + String escapedBody() { + return wrapped("escapejsonbody") + } + + @Override + String body(String jsonPath) { + return wrapped("jsonpath this '${jsonPath}'") + } + + private String wrapped(String text) { + return openingTemplate() + text + closingTemplate() + } +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy index 6112243b12..4f6c032f87 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -118,6 +118,10 @@ class Response extends Common { closure() } + FromRequest fromRequest() { + return new FromRequest() + } + @Override DslProperty value(ClientDslProperty client, ServerDslProperty server) { if (client.clientValue instanceof Pattern) { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java index 727de1c5d7..889713b19b 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java @@ -5,18 +5,24 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.contract.stubrunner.HttpServerStub; +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper; +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper; import org.springframework.cloud.contract.wiremock.WireMockSpring; import org.springframework.util.ClassUtils; import org.springframework.util.SocketUtils; import org.springframework.util.StreamUtils; +import com.github.jknack.handlebars.Helper; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; import com.github.tomakehurst.wiremock.stubbing.StubMapping; /** @@ -34,9 +40,24 @@ public class WireMockHttpServerStub implements HttpServerStub { private WireMockConfiguration config() { if (ClassUtils.isPresent("org.springframework.cloud.contract.wiremock.WireMockSpring", null)) { - return WireMockSpring.options(); + return WireMockSpring.options() + .extensions(responseTemplateTransformer()); } - return new WireMockConfiguration(); + return new WireMockConfiguration().extensions(responseTemplateTransformer()); + } + + private ResponseTemplateTransformer responseTemplateTransformer() { + return new ResponseTemplateTransformer(false, helpers()); + } + + /** + * Override this if you want to register your own helpers + */ + protected Map helpers() { + Map helpers = new HashMap<>(); + helpers.put(HandlebarsJsonPathHelper.NAME, new HandlebarsJsonPathHelper()); + helpers.put(HandlebarsEscapeHelper.NAME, new HandlebarsEscapeHelper()); + return helpers; } @Override diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index dd4909600b..ebdee3b938 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -77,6 +77,10 @@ org.skyscreamer jsonassert + + com.github.jknack + handlebars + cglib cglib @@ -122,6 +126,21 @@ 1.2 test + + jetty-server + org.eclipse.jetty + test + + + jetty-servlet + org.eclipse.jetty + test + + + jetty-servlets + org.eclipse.jetty + test + diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy index c19d25c558..de3f392561 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy @@ -114,6 +114,11 @@ class BlockBuilder { return character == "{" || character == toAdd } + BlockBuilder updateContents(String contents) { + this.builder.replace(0, this.builder.length(), contents) + return this + } + @Override String toString() { return builder.toString() diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy index 77bf86d2c1..41caff1368 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy @@ -160,6 +160,9 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ @Override protected String postProcessJsonPathCall(String jsonPath) { + if (templateProcessor.containsTemplateEntry(jsonPath)) { + return jsonPath + } return jsonPath.replace('$', '\\$') } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy index af6d47ae19..73e250fe34 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy @@ -24,6 +24,7 @@ import org.springframework.cloud.contract.spec.internal.Input import org.springframework.cloud.contract.spec.internal.OutputMessage import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.ContentUtils import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader @@ -45,7 +46,7 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { protected final OutputMessage outputMessage MessagingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { - super(configProperties) + super(configProperties, stubDefinition) this.inputMessage = stubDefinition.input this.outputMessage = stubDefinition.outputMessage } @@ -93,6 +94,12 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { } } + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) { + String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString() + processHeaderElement(blockBuilder, property, gstringValue) + } + protected ContentType getResponseContentType() { ContentType contentType = recognizeContentTypeFromHeader(outputMessage.headers) if (contentType == ContentType.UNKNOWN) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index 6b896bfb0d..b103e710d0 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -16,20 +16,37 @@ package org.springframework.cloud.contract.verifier.builder +import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.apache.commons.lang3.StringEscapeUtils -import org.springframework.cloud.contract.spec.internal.* -import org.springframework.cloud.contract.verifier.util.MapConverter; +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractTemplate +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.BodyMatchers +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.OptionalProperty +import org.springframework.cloud.contract.spec.internal.QueryParameter import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor +import org.springframework.cloud.contract.verifier.template.TemplateProcessor import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.JsonPaths import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.util.SerializationUtils +import java.lang.invoke.MethodHandles import java.util.regex.Pattern import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue @@ -46,10 +63,26 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.extr @PackageScope abstract class MethodBodyBuilder { - protected final ContractVerifierConfigProperties configProperties + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()) - protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties) { + protected final ContractVerifierConfigProperties configProperties + protected final TemplateProcessor templateProcessor + protected final ContractTemplate contractTemplate + protected final Contract contract + + protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties, Contract contract) { this.configProperties = configProperties + this.templateProcessor = processor() + this.contractTemplate = template() + this.contract = contract + } + + private TemplateProcessor processor() { + return new HandlebarsTemplateProcessor() + } + + private ContractTemplate template() { + return new HandlebarsTemplateProcessor() } /** @@ -125,6 +158,11 @@ abstract class MethodBodyBuilder { * Appends to the {@link BlockBuilder} the assertion for the given header path */ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, String value) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given header path + */ + protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) /** * Appends to the {@link BlockBuilder} the code to retrieve a value for a property @@ -301,9 +339,16 @@ abstract class MethodBodyBuilder { Object copiedBody = cloneBody(convertedResponseBody) convertedResponseBody = JsonToJsonPathsConverter.removeMatchingJsonPaths(convertedResponseBody, bodyMatchers) JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(convertedResponseBody) + DocumentContext parsedRequestBody + if (contract.request?.body) { + def requestBody = MapConverter.getTestSideValues(contract.request.body) + parsedRequestBody = JsonPath.parse(requestBody) + } jsonPaths.each { String method = it.method() - String postProcessedMethod = postProcessJsonPathCall(method) + method = processIfTemplateIsPresent(method, parsedRequestBody) + String postProcessedMethod = templateProcessor.containsJsonPathTemplateEntry(method) ? + method : postProcessJsonPathCall(method) bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) addColonIfRequired(bb) } @@ -325,6 +370,21 @@ abstract class MethodBodyBuilder { processBodyElement(bb, "", convertedResponseBody) } + protected String processIfTemplateIsPresent(String method, DocumentContext parsedRequestBody) { + if (templateProcessor.containsTemplateEntry(method) && + templateProcessor.containsJsonPathTemplateEntry(method) && contract.request?.body) { + // Unquoting the values of non strings + String jsonPathEntry = templateProcessor.jsonPathFromTemplateEntry(method) + Object object = parsedRequestBody.read(jsonPathEntry) + if (!(object instanceof String)) { + return method + .replace('"' + contractTemplate.openingTemplate(), contractTemplate.openingTemplate()) + .replace(contractTemplate.closingTemplate() + '"', contractTemplate.closingTemplate()) + } + } + return method + } + protected void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { String path = quotedAndEscaped(bodyMatcher.path()) Object retrievedValue = value(copiedBody, bodyMatcher) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy index 446fa1305e..bbca85cfeb 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy @@ -25,7 +25,6 @@ import org.springframework.cloud.contract.spec.internal.NotToEscapePattern import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import java.util.regex.Pattern - /** * A {@link SpockMethodRequestProcessingBodyBuilder} implementation that uses MockMvc to send requests. * @@ -63,6 +62,7 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP "${patternComparison(value.serverValue.pattern().replace("\\", "\\\\"))}") } } + @Override protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) { blockBuilder.addLine("${exec.insertValue("response.header(\'$property\')")}") @@ -81,6 +81,9 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP // #273 - should escape $ for Groovy since it will try to make it a GString @Override protected String postProcessJsonPathCall(String jsonPath) { + if (templateProcessor.containsTemplateEntry(jsonPath)) { + return jsonPath + } return jsonPath.replace('$', '\\$') } } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index 62a34d200a..0492aa4023 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -21,21 +21,22 @@ import groovy.transform.PackageScope import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.BodyMatchers import org.springframework.cloud.contract.spec.internal.ExecutionProperty -import org.springframework.cloud.contract.spec.internal.Request import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.MatchingStrategy import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.QueryParameter +import org.springframework.cloud.contract.spec.internal.Request import org.springframework.cloud.contract.spec.internal.Response import org.springframework.cloud.contract.spec.internal.Url -import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.ContentUtils +import org.springframework.cloud.contract.verifier.util.MapConverter import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader - /** * An abstraction for creating a test method that includes processing of an HTTP request * @@ -55,7 +56,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { private static final String QUERY_PARAM_METHOD = 'queryParam' RequestProcessingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) { - super(configProperties) + super(configProperties, stubDefinition) this.request = stubDefinition.request this.response = stubDefinition.response } @@ -154,6 +155,19 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { } } + @Override + protected void validateResponseBodyBlock(BlockBuilder bb, BodyMatchers bodyMatchers, Object responseBody) { + super.validateResponseBodyBlock(bb, bodyMatchers, responseBody) + String newBody = this.templateProcessor.transform(request, bb.toString()) + bb.updateContents(newBody) + } + + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) { + String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString() + processHeaderElement(blockBuilder, property, gstringValue) + } + @Override protected ContentType getResponseContentType() { ContentType contentType = recognizeContentTypeFromHeader(response.headers) @@ -214,4 +228,4 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { private boolean hasQueryParams(Url url) { return url.queryParameters } -} +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy index c64726d68d..3f265e9a08 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy @@ -194,6 +194,9 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { // #273 - should escape $ for Groovy since it will try to make it a GString @Override protected String postProcessJsonPathCall(String method) { + if (templateProcessor.containsTemplateEntry(method)) { + return method + } return method.replace('$', '\\$') } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy index ea6a8b7130..07124834f2 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy @@ -25,6 +25,7 @@ import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.Request import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.util.ContentUtils import java.util.regex.Pattern @@ -140,6 +141,12 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing return ".param('$parameter.key', '$parameter.value')" } + @Override + protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) { + String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString() + processHeaderElement(blockBuilder, property, gstringValue) + } + protected String convertHeaderComparison(String headerValue) { return " == '$headerValue'" } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy new file mode 100644 index 0000000000..b66e795591 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy @@ -0,0 +1,83 @@ +package org.springframework.cloud.contract.verifier.builder + +import groovy.json.JsonOutput +import groovy.transform.CompileStatic +import groovy.transform.Immutable +import org.apache.commons.lang3.StringEscapeUtils +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.util.ContentUtils +import org.springframework.cloud.contract.verifier.util.MapConverter + +/** + * Representation of the request side to be used for response templating in + * the generated tests. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@Immutable +@CompileStatic +class TestSideRequestTemplateModel { + /** + * Request URL + */ + final String url + + /** + * Map containing query parameters + */ + final Map> query + + /** + * Map containing request headers + */ + final Map> headers + + /** + * Escaped request body that can be put into test + */ + final String body + + /** + * Request body as it would be sent to the controller + */ + final String rawBody + + static TestSideRequestTemplateModel from(final Request request) { + String url = MapConverter.getTestSideValues(request.url ?: request.urlPath) + Map> query = (Map>) (request.url ?: request.urlPath) + .queryParameters?.parameters?.groupBy { it.name }?.collectEntries { + [(it.key): it.value.collect { MapConverter.getTestSideValues(it) }] + } + Map> headers = (Map>) (request.headers?.entries?.groupBy { + it.name + }?.collectEntries { + [(it.key): it.value.collect { MapConverter.getTestSideValues(it) }] + }) + String body = trimmedAndEscapedBody(request.body) + String rawBody = getBodyAsRawJson(request.body) + return new TestSideRequestTemplateModel(url, query, headers, body, rawBody) + } + + private static String trimmedAndEscapedBody(Object body) { + String rawBody = getBodyAsRawJson(body) + return StringEscapeUtils.escapeJava(rawBody) + } + + private static String getBodyAsRawJson(Object body) { + Object bodyValue = extractServerValueFromBody(body) + return new JsonOutput().toJson(bodyValue) + } + + protected static Object extractServerValueFromBody(bodyValue) { + if (bodyValue instanceof GString) { + bodyValue = ContentUtils.extractValue(bodyValue, { DslProperty dslProperty -> dslProperty.serverValue }) + } else { + bodyValue = MapConverter.transformValues(bodyValue, { + it instanceof DslProperty ? it.serverValue : it + }) + } + return bodyValue + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy new file mode 100644 index 0000000000..b4ca4933b1 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy @@ -0,0 +1,40 @@ +package org.springframework.cloud.contract.verifier.builder.handlebars + +import com.github.jknack.handlebars.Helper +import com.github.jknack.handlebars.Options +import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel +import groovy.transform.CompileStatic +import org.apache.commons.lang3.StringEscapeUtils +import org.springframework.cloud.contract.verifier.builder.TestSideRequestTemplateModel +/** + * A Handlebars helper for the {@code escapejsonbody} helper function. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@CompileStatic +class HandlebarsEscapeHelper implements Helper> { + + public static final String NAME = "escapejsonbody" + public static final String REQUEST_MODEL_NAME = "request" + + @Override + Object apply(Map context, Options options) throws IOException { + Object model = context.get(REQUEST_MODEL_NAME) + if (model instanceof TestSideRequestTemplateModel) { + return StringEscapeUtils.escapeJson(returnObjectForTest(model).toString()) + } else if (model instanceof RequestTemplateModel) { + return StringEscapeUtils.escapeJson(returnObjectForStub(model).toString()) + } + throw new IllegalArgumentException("Unsupported model") + } + + private Object returnObjectForStub(Object model) { + return ((RequestTemplateModel) model).body + } + + private Object returnObjectForTest(Object model) { + return ((TestSideRequestTemplateModel) model).rawBody + } + +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy new file mode 100644 index 0000000000..ac29171b85 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy @@ -0,0 +1,45 @@ +package org.springframework.cloud.contract.verifier.builder.handlebars + +import com.github.jknack.handlebars.Helper +import com.github.jknack.handlebars.Options +import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.verifier.builder.TestSideRequestTemplateModel + +/** + * A Handlebars helper for the {@code jsonpath} helper function. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@CompileStatic +class HandlebarsJsonPathHelper implements Helper> { + + public static final String NAME = "jsonpath" + public static final String REQUEST_MODEL_NAME = "request" + + @Override + Object apply(Map context, Options options) throws IOException { + String jsonPath = options.param(0) + Object model = context.get(REQUEST_MODEL_NAME) + if (model instanceof TestSideRequestTemplateModel) { + return returnObjectForTest(model, jsonPath) + } else if (model instanceof RequestTemplateModel) { + return returnObjectForStub(model, jsonPath) + } + throw new IllegalArgumentException("Unsupported model") + } + + private Object returnObjectForStub(Object model, String jsonPath) { + DocumentContext documentContext = JsonPath.parse(((RequestTemplateModel) model).body) + return documentContext.read(jsonPath) + } + + private Object returnObjectForTest(Object model, String jsonPath) { + DocumentContext documentContext = JsonPath.parse(((TestSideRequestTemplateModel) model).rawBody) + return documentContext.read(jsonPath) + } + +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy index d1356c2b12..c9717eaaf4 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -16,18 +16,23 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath import groovy.json.JsonBuilder import groovy.transform.PackageScope import groovy.transform.TypeChecked -import org.springframework.cloud.contract.spec.internal.Headers +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractTemplate import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.verifier.util.MapConverter +import org.springframework.cloud.contract.spec.internal.Headers +import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor +import org.springframework.cloud.contract.verifier.template.TemplateProcessor import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils +import org.springframework.cloud.contract.verifier.util.MapConverter import static ContentUtils.extractValue -import static MapConverter.transformValues - +import static org.springframework.cloud.contract.verifier.util.MapConverter.transformValues /** * Common abstraction over WireMock Request / Response conversion implementations * @@ -39,6 +44,26 @@ import static MapConverter.transformValues @PackageScope abstract class BaseWireMockStubStrategy { + private static final String WRAPPER = "UNQUOTE_ME" + + protected final TemplateProcessor processor + protected final ContractTemplate template + protected final Contract contract + + protected BaseWireMockStubStrategy(Contract contract) { + this.processor = processor() + this.template = contractTemplate() + this.contract = contract + } + + private TemplateProcessor processor() { + return new HandlebarsTemplateProcessor() + } + + private ContractTemplate contractTemplate() { + return new HandlebarsTemplateProcessor() + } + /** * Returns the stub side values from the object */ @@ -69,7 +94,33 @@ abstract class BaseWireMockStubStrategy { */ String parseBody(Map map, ContentType contentType) { def transformedMap = MapConverter.getStubSideValues(map) - return parseBody(toJson(transformedMap), contentType) + String responseSideBody = toJson(MapConverter.getTestSideValues(contract.request.body)) + DocumentContext context = JsonPath.parse(responseSideBody) + transformedMap = processEntriesForTemplating(transformedMap, context) + String json = toJson(transformedMap) + // the space is important cause at the end of the json body you also have a } + // you can't have 4 } next to each other + String unquotedJson = json.replace('"' + WRAPPER, '').replace(WRAPPER + '"', ' ') + return parseBody(unquotedJson, contentType) + } + + private Object processEntriesForTemplating(transformedMap, DocumentContext context) { + return transformValues(transformedMap, { + if (it instanceof String && processor.containsJsonPathTemplateEntry(it)) { + String jsonPath = processor.jsonPathFromTemplateEntry(it) + if (!jsonPath) { + return it + } + Object value = context.read(jsonPath) + if (value instanceof String) { + return it + } + return "${WRAPPER}${it}${WRAPPER}" + } else if (it instanceof String && processor.containsTemplateEntry(it) && template.body() == it) { + return template.escapedBody() + } + return it + }) } /** 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 2bb9de4c8f..eecac825f5 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 @@ -58,6 +58,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { private final Request request WireMockRequestStubStrategy(Contract groovyDsl) { + super(groovyDsl) this.request = groovyDsl.request } @@ -145,7 +146,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } private UrlPattern urlPattern() { - Object urlPath = request?.urlPath?.clientValue + Object urlPath = urlPathOrUrlIfQueryPresent() if (urlPath) { if(urlPath instanceof Pattern) { return WireMock.urlPathMatching(getStubSideValue(urlPath.toString()) as String) @@ -163,6 +164,18 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { return WireMock.urlEqualTo(url.toString()) } + private Object urlPathOrUrlIfQueryPresent() { + Object urlPath = request?.urlPath?.clientValue + Object queryParamsFromUrl = request?.url?.queryParameters?.parameters + if (urlPath) { + return urlPath + } + if (queryParamsFromUrl) { + return request?.url?.clientValue + } + return null + } + private Object getUrlIfGstring(Object clientSide) { if (clientSide instanceof GString) { if (clientSide.values.any { getStubSideValue(it) instanceof Pattern }) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy index eda44df799..4c0e4d8c7c 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy @@ -28,7 +28,6 @@ import org.springframework.cloud.contract.spec.internal.Response import org.springframework.cloud.contract.verifier.util.ContentType import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent - import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader /** * Converts a {@link Request} into {@link ResponseDefinition} @@ -42,6 +41,7 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { private final Response response WireMockResponseStubStrategy(Contract groovyDsl) { + super(groovyDsl) this.response = groovyDsl.response } @@ -55,6 +55,7 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { appendHeaders(builder) appendBody(builder) appendResponseDelayTime(builder) + builder.withTransformers("response-template") return builder.build() } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy new file mode 100644 index 0000000000..2dcad8681d --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy @@ -0,0 +1,80 @@ +package org.springframework.cloud.contract.verifier.template + +import com.github.jknack.handlebars.Handlebars +import com.github.jknack.handlebars.Template +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.ContractTemplate +import org.springframework.cloud.contract.spec.internal.HandlebarsContractTemplate +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper +import org.springframework.cloud.contract.verifier.builder.TestSideRequestTemplateModel + +import java.util.regex.Matcher +import java.util.regex.Pattern +/** + * Default Handlebars template processor + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@CompileStatic +class HandlebarsTemplateProcessor implements TemplateProcessor, ContractTemplate { + + private static final Pattern JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{\\{jsonpath this '(.*)'}}}.*\$") + + @Delegate + private final ContractTemplate contractTemplate = new HandlebarsContractTemplate() + + @Override + String transform(Request request, String testContents) { + TestSideRequestTemplateModel templateModel = TestSideRequestTemplateModel.from(request) + Map model = [(HandlebarsJsonPathHelper.REQUEST_MODEL_NAME): templateModel] + Template bodyTemplate = uncheckedCompileTemplate(testContents) + return templatedResponseBody(model, bodyTemplate) + } + + @Override + boolean containsTemplateEntry(String line) { + return line.matches('^.*\\{\\{\\{.*}}}.*$') + } + + @Override + boolean containsJsonPathTemplateEntry(String line) { + return line.contains(openingTemplate() + HandlebarsJsonPathHelper.NAME) + } + + @Override + String jsonPathFromTemplateEntry(String line) { + if (!containsJsonPathTemplateEntry(line)) { + return "" + } + Matcher matcher = JSON_PATH_PATTERN.matcher(line) + if (!matcher.matches()) { + return "" + } + return matcher.group(1) + } + + private String templatedResponseBody(Map< String, TestSideRequestTemplateModel> model, Template bodyTemplate) { + return uncheckedApplyTemplate(bodyTemplate, model) + } + + private String uncheckedApplyTemplate(Template template, Object context) { + try { + return template.apply(context) + } catch (IOException e) { + throw new RuntimeException(e) + } + } + + private Template uncheckedCompileTemplate(String content) { + try { + Handlebars handlebars = new Handlebars() + handlebars.registerHelper(HandlebarsJsonPathHelper.NAME, new HandlebarsJsonPathHelper()) + return handlebars.compileInline(content) + } catch (IOException e) { + throw new RuntimeException(e) + } + } +} + diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy new file mode 100644 index 0000000000..6791fea2b6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy @@ -0,0 +1,41 @@ +package org.springframework.cloud.contract.verifier.template + +import org.springframework.cloud.contract.spec.internal.Request +/** + * Contract for conversion of templated responses. + * + * If no implementation is provided then Handlebars will be picked as a default implementation. + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +interface TemplateProcessor { + + /** + * For the given {@link Request} and the test contents should perform a transformation + * and return the converted test + */ + String transform(Request request, String testContents) + + /** + * Returns {@code true} if the current line contains template related entry. E.g. for Handlebars + * if a line contains {{{...}}} then it's considered to contain template related entry + */ + boolean containsTemplateEntry(String line) + + /** + * Returns {@code true} if the current line contains template related entry for json path processing. + * E.g. for Handlebars if a line contains {{{jsonpath ...}}} then + * it's considered to contain template related entry for json path processing + */ + boolean containsJsonPathTemplateEntry(String line) + + /** + * Returns the json path entry from the current line that contains template related entry for json path processing. + * E.g. for Handlebars if a line contains {{{jsonpath this '$.a.b.c'}}} then + * the te method would return {@code $.a.b.c}. Returns empty string if there's no matching + * json path entry + */ + String jsonPathFromTemplateEntry(String line) + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index 553dac2913..ea0aa40b39 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -152,7 +152,7 @@ class ContentUtils { } } - private static GStringImpl extractValueForGString(GString bodyAsValue, Closure valueProvider) { + static GStringImpl extractValueForGString(GString bodyAsValue, Closure valueProvider) { return new GStringImpl( bodyAsValue.values.collect { it instanceof DslProperty ? valueProvider(it) : it } as String[], bodyAsValue.strings.clone() as String[] diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index a61ce93f31..6b220f3b70 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -32,7 +32,6 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig import org.springframework.util.SerializationUtils import java.util.regex.Pattern - /** * I would like to apologize to anyone who is reading this class. Since JSON is a hectic structure * this class is also hectic. The idea is to traverse the JSON structure and build a set of @@ -59,7 +58,7 @@ class JsonToJsonPathsConverter { } JsonToJsonPathsConverter() { - this.configProperties = new ContractVerifierConfigProperties() + this(new ContractVerifierConfigProperties()) if (log.isDebugEnabled()) { log.debug("Creating JsonToJsonPaths converter with default properties") } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy index 49ff66f9b1..6e2529f8b8 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy @@ -18,7 +18,8 @@ package org.springframework.cloud.contract.verifier.util import groovy.json.JsonSlurper import org.springframework.cloud.contract.spec.internal.DslProperty - +import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor +import org.springframework.cloud.contract.verifier.template.TemplateProcessor /** * Converts an object into either client or server side representation. * Iterates over the structure of an object (depending on whether it's an @@ -34,6 +35,16 @@ class MapConverter { public static final boolean STUB_SIDE = true public static final boolean TEST_SIDE = false + private final TemplateProcessor templateProcessor + + MapConverter() { + this.templateProcessor = processor() + } + + private TemplateProcessor processor() { + return new HandlebarsTemplateProcessor() + } + /** * Returns the object with client side values of {@link org.springframework.cloud.contract.spec.internal.DslProperty} */ @@ -107,7 +118,10 @@ class MapConverter { return clientSide ? getClientOrServerSideValues(dslProperty.clientValue, clientSide) : getClientOrServerSideValues(dslProperty.serverValue, clientSide) } else if (it instanceof GString) { - return ContentUtils.extractValue(it , null, { + ContentType type = new MapConverter().templateProcessor.containsJsonPathTemplateEntry( + ContentUtils.extractValueForGString(it, ContentUtils.GET_TEST_SIDE).toString() + ) ? ContentType.TEXT : null + return ContentUtils.extractValue(it , type, { if (it instanceof DslProperty) { return clientSide ? getClientOrServerSideValues((it as DslProperty).clientValue, clientSide) : getClientOrServerSideValues((it as DslProperty).serverValue, clientSide) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index 3af4f13f7a..5f9f8ef4a1 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -29,7 +29,6 @@ import spock.lang.Specification import spock.util.environment.RestoreSystemProperties import java.util.regex.Pattern - /** * @author Jakub Kubrynski, codearte.io */ @@ -2190,4 +2189,69 @@ World.'''""" "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String body -> body.contains(""".method('GET', entity('12000', 'text/plain'))""") } | { String body -> body.contains('responseBody == "12000"') } "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | { String body -> body.contains(""".method("GET", entity("12000", "text/plain"))""") } | { String body -> body.contains('assertThat(responseBody).isEqualTo("12000")') } } + + @Issue("#230") + def "should manage to reference request in response [#methodBuilderName]"() { + given: + //tag::template_contract[] + Contract contractDsl = Contract.make { + request { + method 'GET' + url('/api/v1/xxxx') { + queryParameters { + parameter("foo", "bar") + parameter("foo", "bar2") + } + } + headers { + header(authorization(), "secret") + header(authorization(), "secret2") + } + body(foo: "bar", baz: 5) + } + response { + status 200 + headers { + header(authorization(), "foo ${fromRequest().header(authorization())} bar") + } + body( + url: fromRequest().url(), + param: fromRequest().query("foo"), + paramIndex: fromRequest().query("foo", 1), + authorization: fromRequest().header("Authorization"), + authorization2: fromRequest().header("Authorization", 1), + fullBody: fromRequest().body(), + responseFoo: fromRequest().body('$.foo'), + responseBaz: fromRequest().body('$.baz'), + responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla" + ) + } + } + //end::template_contract[] + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + and: + builder.appendTo(blockBuilder) + String test = blockBuilder.toString() + when: + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + then: + !test.contains('''DslProperty''') + test.contains('''assertThatJson(parsedJson).field("url").isEqualTo("/api/v1/xxxx")''') + test.contains('''assertThatJson(parsedJson).field("fullBody").isEqualTo("{\\"foo\\":\\"bar\\",\\"baz\\":5}")''') + test.contains('''assertThatJson(parsedJson).field("paramIndex").isEqualTo("bar2")''') + test.contains('''assertThatJson(parsedJson).field("responseFoo").isEqualTo("bar")''') + test.contains('''assertThatJson(parsedJson).field("authorization").isEqualTo("secret")''') + test.contains('''assertThatJson(parsedJson).field("authorization2").isEqualTo("secret2")''') + test.contains('''assertThatJson(parsedJson).field("responseBaz").isEqualTo(5)''') + test.contains('''assertThatJson(parsedJson).field("responseBaz2").isEqualTo("Bla bla bar bla bla")''') + test.contains('''assertThatJson(parsedJson).field("param").isEqualTo("bar")''') + responseAssertion(test) + where: + methodBuilderName | methodBuilder | responseAssertion + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String body -> body.contains("response.header('Authorization') == 'foo secret bar'") } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | { String body -> body.contains('assertThat(response.header("Authorization")).isEqualTo("foo secret bar");') } + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String body -> body.contains("response.getHeaderString('Authorization') == 'foo secret bar'") } + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | { String body -> body.contains('assertThat(response.getHeaderString("Authorization")).isEqualTo("foo secret bar");') } + } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy index fd4cf596b1..031d5201f3 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy @@ -16,11 +16,22 @@ package org.springframework.cloud.contract.verifier.dsl +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer import groovy.json.JsonBuilder import groovy.json.JsonSlurper +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubMapping import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy import org.springframework.cloud.contract.verifier.file.ContractMetadata import org.springframework.cloud.contract.verifier.util.AssertionUtil +import org.springframework.http.RequestEntity +import org.springframework.http.ResponseEntity +import org.springframework.util.SocketUtils import spock.lang.Issue import spock.lang.Specification @@ -66,7 +77,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"id\\":\\"123\\",\\"surname\\":\\"Kowalsky\\",\\"name\\":\\"Jan\\",\\"created\\":\\"2014-02-02 12:23:43\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } ''', wireMockStub) @@ -113,7 +125,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie }, "response" : { "status" : 200, - "body" : "{\\"ingredients\\":[{\\"type\\":\\"MALT\\",\\"quantity\\":100},{\\"type\\":\\"WATER\\",\\"quantity\\":200},{\\"type\\":\\"HOP\\",\\"quantity\\":300},{\\"type\\":\\"YIEST\\",\\"quantity\\":400}]}" + "body" : "{\\"ingredients\\":[{\\"type\\":\\"MALT\\",\\"quantity\\":100},{\\"type\\":\\"WATER\\",\\"quantity\\":200},{\\"type\\":\\"HOP\\",\\"quantity\\":300},{\\"type\\":\\"YIEST\\",\\"quantity\\":400}]}", + "transformers" : [ "response-template" ] } } ''', wireMockStub) @@ -166,7 +179,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie }, "response": { "status": 204, - "body": "{\\"paymentId\\":\\"4\\",\\"foundExistingPayment\\":false}" + "body": "{\\"paymentId\\":\\"4\\",\\"foundExistingPayment\\":false}", + "transformers" : [ "response-template" ] } } ''', wireMockStub) @@ -212,7 +226,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"id\\":\\"123\\",\\"surname\\":\\"Kowalsky\\",\\"name\\":\\"Jan\\",\\"created\\":\\"2014-02-02 12:23:43\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } ''') @@ -268,7 +283,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"name\\":\\"Jan\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } ''', wireMockStub) @@ -318,7 +334,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } ] }, "response" : { - "status" : 200 + "status" : 200, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -363,7 +380,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } }, "response" : { - "status" : 200 + "status" : 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -408,7 +426,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -445,7 +464,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -478,7 +498,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie }, "response": { "status": 200, - "body":"Jozo<test>" + "body":"Jozo<test>", + "transformers" : [ "response-template" ] } } '''), json) @@ -513,7 +534,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -550,7 +572,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -600,7 +623,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"name\\":\\"Jan\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -662,7 +686,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"fraudCheckStatus\\":\\"OK\\",\\"rejectionReason\\":null}", "headers" : { "Content-Type" : "application/vnd.fraud.v1+json" - } + }, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -728,7 +753,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -768,7 +794,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -798,7 +825,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "urlPath": "boxes" }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -827,7 +855,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "urlPath": "boxes" }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -969,7 +998,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie { "request": { "method": "GET", - "urlPattern": "users/[0-9]*", + "urlPathPattern": "users/[0-9]*", "queryParameters": { "age": { "doesNotMatch": "^\\\\w*$" @@ -980,7 +1009,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), json) @@ -1053,7 +1083,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"name\\":\\"Jan\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -1113,7 +1144,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"status\\":\\"OK\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] } } '''), json) @@ -1146,7 +1178,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 406 + "status": 406, + "transformers" : [ "response-template" ] } } '''), json) @@ -1175,7 +1208,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "url": "test" }, "response": { - "status": 406 + "status": 406, + "transformers" : [ "response-template" ] } } '''), json) @@ -1208,7 +1242,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } ] }, "response" : { - "status" : 200 + "status" : 200, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -1245,7 +1280,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie ] }, "response": { - "status": 200 + "status": 200, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -1303,7 +1339,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"code\\":4,\\"message\\":\\"User not found by email = [not.existing@user.com]\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] }, "priority" : 1 } @@ -1348,7 +1385,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } }, "response" : { - "status" : 422 + "status" : 422, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -1382,7 +1420,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email = [not.existing@user.com]\\"}", "headers" : { "Content-Type" : "application/json" - } + }, + "transformers" : [ "response-template" ] }, "priority" : 1 } @@ -1456,7 +1495,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie new JsonSlurper().parseText(json) } - String toWireMockClientJsonStub(groovyDsl) { + String toWireMockClientJsonStub(Contract groovyDsl) { new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null, groovyDsl), groovyDsl).toWireMockClientStub() } @@ -1497,7 +1536,8 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } ] }, "response" : { - "status" : 200 + "status" : 200, + "transformers" : [ "response-template" ] } } '''), wireMockStub) @@ -1505,84 +1545,6 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie stubMappingIsValidWireMockStub(wireMockStub) } - @Issue('#219') - def "should generate request with an optional queryParameter for client side"() { - given: - org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { - request { - method 'GET' - urlPath ('/some/api') { - queryParameters { - parameter 'size': value( - consumer(regex('[0-9]+')), - producer(1) - ) - parameter 'page': value( - consumer(regex('[0-9]+')), - producer(0) - ) - parameter sort: value( - consumer(optional(regex('^[a-z]+$'))), - producer('id') - ) - } - } - } - response { - status 200 - body( - content: [[ - id : '00000000-0000-0000-0000-000000000000', - type : 'Extraordinary', - state : 'ACTIVE', - ]], - totalPages: 1, - totalElements: 1, - last: true, - sort: [[ - direction: 'ASC', - property: 'id', - ignoreCase: false, - nullHandling: 'NATIVE', - ascending: true - ]], - first: true, - numberOfElements: 1, - size: 1, - number: 0 - ) - } - } - when: - def json = toWireMockClientJsonStub(groovyDsl) - then: - AssertionUtil.assertThatJsonsAreEqual((''' - { - "request" : { - "urlPath" : "/some/api", - "method" : "GET", - "queryParameters" : { - "size" : { - "matches" : "[0-9]+" - }, - "page" : { - "matches" : "[0-9]+" - }, - "sort" : { - "matches" : "(^[a-z]+$)?" - } - } - }, - "response" : { - "status" : 200, - "body" : "{\\"content\\":[{\\"id\\":\\"00000000-0000-0000-0000-000000000000\\",\\"type\\":\\"Extraordinary\\",\\"state\\":\\"ACTIVE\\"}],\\"totalPages\\":1,\\"totalElements\\":1,\\"last\\":true,\\"sort\\":[{\\"direction\\":\\"ASC\\",\\"property\\":\\"id\\",\\"ignoreCase\\":false,\\"nullHandling\\":\\"NATIVE\\",\\"ascending\\":true}],\\"first\\":true,\\"numberOfElements\\":1,\\"size\\":1,\\"number\\":0}" - } - } - '''), json) - and: - stubMappingIsValidWireMockStub(json) - } - @Issue('#30') def "should not create a stub for a skipped contract"() { given: @@ -1635,4 +1597,199 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie then: json == '' } -} + + @Issue('#219') + def "should generate request with an optional queryParameter for client side"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + urlPath ('/some/api') { + queryParameters { + parameter 'size': value( + consumer(regex('[0-9]+')), + producer(1) + ) + parameter 'page': value( + consumer(regex('[0-9]+')), + producer(0) + ) + parameter sort: value( + consumer(optional(regex('^[a-z]+$'))), + producer('id') + ) + } + } + } + response { + status 200 + body( + content: [[ + id : '00000000-0000-0000-0000-000000000000', + type : 'Extraordinary', + state : 'ACTIVE', + ]], + totalPages: 1, + totalElements: 1, + last: true, + sort: [[ + direction: 'ASC', + property: 'id', + ignoreCase: false, + nullHandling: 'NATIVE', + ascending: true + ]], + first: true, + numberOfElements: 1, + size: 1, + number: 0 + ) + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "urlPath" : "/some/api", + "method" : "GET", + "queryParameters" : { + "size" : { + "matches" : "[0-9]+" + }, + "page" : { + "matches" : "[0-9]+" + }, + "sort" : { + "matches" : "(^[a-z]+$)?" + } + } + }, + "response" : { + "status" : 200, + "body" : "{\\"content\\":[{\\"id\\":\\"00000000-0000-0000-0000-000000000000\\",\\"type\\":\\"Extraordinary\\",\\"state\\":\\"ACTIVE\\"}],\\"totalPages\\":1,\\"totalElements\\":1,\\"last\\":true,\\"sort\\":[{\\"direction\\":\\"ASC\\",\\"property\\":\\"id\\",\\"ignoreCase\\":false,\\"nullHandling\\":\\"NATIVE\\",\\"ascending\\":true}],\\"first\\":true,\\"numberOfElements\\":1,\\"size\\":1,\\"number\\":0}", + "transformers" : [ "response-template" ] + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + } + + @Issue('#237') + def "should generate a stub with response template"() { + given: + org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { + request { + method 'POST' + url('/api/v1/xxxx') { + queryParameters { + parameter("foo", "bar") + parameter("foo", "bar2") + } + } + headers { + header(authorization(), "secret") + header(authorization(), "secret2") + } + body(foo: "bar", baz: 5) + } + response { + status 200 + headers { + header(authorization(), fromRequest().header(authorization())) + } + body( + url: fromRequest().url(), + param: fromRequest().query("foo"), + paramIndex: fromRequest().query("foo", 1), + authorization: fromRequest().header("Authorization"), + authorization2: fromRequest().header("Authorization", 1), + fullBody: fromRequest().body(), + responseFoo: fromRequest().body('$.foo'), + responseBaz: fromRequest().body('$.baz'), + responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla" + ) + } + } + when: + def json = toWireMockClientJsonStub(groovyDsl) + then: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "request" : { + "urlPath" : "/api/v1/xxxx", + "method" : "POST", + "headers" : { + "Authorization" : { + "equalTo" : "secret2" + } + }, + "queryParameters" : { + "foo" : { + "equalTo" : "bar2" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.baz == 5)]" + }, { + "matchesJsonPath" : "$[?(@.foo == 'bar')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"url\\":\\"{{{request.url}}}\\",\\"param\\":\\"{{{request.query.foo.[0]}}}\\",\\"paramIndex\\":\\"{{{request.query.foo.[1]}}}\\",\\"authorization\\":\\"{{{request.headers.Authorization.[0]}}}\\",\\"authorization2\\":\\"{{{request.headers.Authorization.[1]}}}\\",\\"fullBody\\":\\"{{{escapejsonbody}}}\\",\\"responseFoo\\":\\"{{{jsonpath this '$.foo'}}}\\",\\"responseBaz\\":{{{jsonpath this '$.baz'}}} ,\\"responseBaz2\\":\\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\\"}", + "headers" : { + "Authorization" : "{{{request.headers.Authorization.[0]}}}" + }, + "transformers" : [ "response-template" ] + } + } + '''), json) + and: + stubMappingIsValidWireMockStub(json) + and: + int port = SocketUtils.findAvailableTcpPort() + WireMockServer server = new WireMockServer(config().port(port)) + server.start() + server.addStubMapping(WireMockStubMapping.buildFrom(json)) + then: + ResponseEntity entity = call(port) + entity.headers.find { it.key == "Authorization" && it.value.contains("secret") } + and: + AssertionUtil.assertThatJsonsAreEqual((''' + { + "url" : "/api/v1/xxxx?foo=bar&foo=bar2", + "param" : "bar", + "paramIndex" : "bar2", + "authorization" : "secret", + "authorization2" : "secret2", + "fullBody" : "{\\"foo\\":\\"bar\\",\\"baz\\":5}", + "responseFoo" : "bar", + "responseBaz" : 5, + "responseBaz2" : "Bla bla bar bla bla" + } + '''), entity.body) + cleanup: + server?.shutdown() + } + + WireMockConfiguration config() { + return new WireMockConfiguration().extensions(responseTemplateTransformer()) + } + + private ResponseTemplateTransformer responseTemplateTransformer() { + return new ResponseTemplateTransformer(false, + [(HandlebarsJsonPathHelper.NAME): new HandlebarsJsonPathHelper(), + (HandlebarsEscapeHelper.NAME): new HandlebarsEscapeHelper()]) + } + + ResponseEntity call(int port) { + return new TestRestTemplate().exchange( + RequestEntity.post(URI.create("http://localhost:" + port + "/api/v1/xxxx?foo=bar&foo=bar2")) + .header("Authorization", "secret") + .header("Authorization", "secret2") + .body("{\"foo\":\"bar\",\"baz\":5}"), String.class) + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubMappingSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubMappingSpec.groovy index 3aaf03f124..05c762461e 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubMappingSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubMappingSpec.groovy @@ -27,6 +27,19 @@ class WireMockStubMappingSpec extends Specification { }, "uuid" : "77514bd4-a102-4478-a3c0-0fda8b905591" } +""" + private static final String stub_2_5_1_with_transformer = """ +{ + "id" : "77514bd4-a102-4478-a3c0-0fda8b905591", + "request" : { + "method" : "GET" + }, + "response" : { + "status" : 200, + "transformers": ["response-template"] + }, + "uuid" : "77514bd4-a102-4478-a3c0-0fda8b905591" +} """ def "should successfully parse a WireMock 2.1.7 stub"() { @@ -38,4 +51,9 @@ class WireMockStubMappingSpec extends Specification { expect: WireMockStubMapping.buildFrom(stub_2_5_1) } + + def "should successfully parse a WireMock 2.5.1 stub that contains transformers"() { + expect: + WireMockStubMapping.buildFrom(stub_2_5_1_with_transformer) + } }