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:
Marcin Grzejszczak
2017-03-02 10:14:10 +01:00
committed by GitHub
parent 7340f2893d
commit d6f2227f89
41 changed files with 1407 additions and 156 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -55,4 +55,3 @@ class Response {
this.count = count;
}
}

View File

@@ -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(),

View File

@@ -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));
}
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}

View File

@@ -205,4 +205,4 @@ class Common {
void assertThatSidesMatch(Object firstSide, Object secondSide) {
// do nothing
}
}
}

View File

@@ -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))
}
}

View File

@@ -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()
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>

View File

@@ -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()

View File

@@ -160,6 +160,9 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ
@Override
protected String postProcessJsonPathCall(String jsonPath) {
if (templateProcessor.containsTemplateEntry(jsonPath)) {
return jsonPath
}
return jsonPath.replace('$', '\\$')
}
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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('$', '\\$')
}
}

View File

@@ -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
}
}
}

View File

@@ -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('$', '\\$')
}

View File

@@ -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'"
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
})
}
/**

View File

@@ -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 }) {

View File

@@ -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()
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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[]

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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");') }
}
}

View File

@@ -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>&lt;test&gt;</jobId></user>"
"body":"<user><name>Jozo</name><jobId>&lt;test&gt;</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)
}
}

View File

@@ -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)
}
}