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
This commit is contained in:
committed by
GitHub
parent
7340f2893d
commit
d6f2227f89
37
README.adoc
37
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.
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.4.2.BUILD-SNAPSHOT</version>
|
||||
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<spring-cloud-contract.version>1.0.4.BUILD-SNAPSHOT</spring-cloud-contract.version>
|
||||
<spring-cloud-dependencies.version>Camden.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
|
||||
<spring-cloud-contract.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
|
||||
<spring-cloud-dependencies.version>Dalston.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
|
||||
<excludeBuildFolders>true</excludeBuildFolders>
|
||||
</properties>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
- url: https://webhooks.gitter.im/e/ece0ece9a76a5af2aa91
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
5
pom.xml
5
pom.xml
@@ -137,6 +137,11 @@
|
||||
<artifactId>pact-jvm-model</artifactId>
|
||||
<version>2.4.18</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jknack</groupId>
|
||||
<artifactId>handlebars</artifactId>
|
||||
<version>4.0.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-dependencies</artifactId>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,3 @@ class Response {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -205,4 +205,4 @@ class Common {
|
||||
void assertThatSidesMatch(Object firstSide, Object secondSide) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String, Helper> helpers() {
|
||||
Map<String, Helper> helpers = new HashMap<>();
|
||||
helpers.put(HandlebarsJsonPathHelper.NAME, new HandlebarsJsonPathHelper());
|
||||
helpers.put(HandlebarsEscapeHelper.NAME, new HandlebarsEscapeHelper());
|
||||
return helpers;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
<groupId>org.skyscreamer</groupId>
|
||||
<artifactId>jsonassert</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jknack</groupId>
|
||||
<artifactId>handlebars</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cglib</groupId>
|
||||
<artifactId>cglib</artifactId>
|
||||
@@ -122,6 +126,21 @@
|
||||
<version>1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -160,6 +160,9 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ
|
||||
|
||||
@Override
|
||||
protected String postProcessJsonPathCall(String jsonPath) {
|
||||
if (templateProcessor.containsTemplateEntry(jsonPath)) {
|
||||
return jsonPath
|
||||
}
|
||||
return jsonPath.replace('$', '\\$')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('$', '\\$')
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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('$', '\\$')
|
||||
}
|
||||
|
||||
|
||||
@@ -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'"
|
||||
}
|
||||
|
||||
@@ -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<String, List<String>> query
|
||||
|
||||
/**
|
||||
* Map containing request headers
|
||||
*/
|
||||
final Map<String, List<String>> 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<String, List<String>> query = (Map<String, List<String>>) (request.url ?: request.urlPath)
|
||||
.queryParameters?.parameters?.groupBy { it.name }?.collectEntries {
|
||||
[(it.key): it.value.collect { MapConverter.getTestSideValues(it) }]
|
||||
}
|
||||
Map<String, List<String>> headers = (Map<String, List<String>>) (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
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> {
|
||||
|
||||
public static final String NAME = "escapejsonbody"
|
||||
public static final String REQUEST_MODEL_NAME = "request"
|
||||
|
||||
@Override
|
||||
Object apply(Map<String, Object> 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Map<String, Object>> {
|
||||
|
||||
public static final String NAME = "jsonpath"
|
||||
public static final String REQUEST_MODEL_NAME = "request"
|
||||
|
||||
@Override
|
||||
Object apply(Map<String, Object> 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, TestSideRequestTemplateModel> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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[]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");') }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":"<user><name>Jozo</name><jobId><test></jobId></user>"
|
||||
"body":"<user><name>Jozo</name><jobId><test></jobId></user>",
|
||||
"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<String> 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<String> 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user