diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy deleted file mode 100644 index 47aa1c269c..0000000000 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy +++ /dev/null @@ -1,89 +0,0 @@ -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 path and query 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 a URL path - */ - String path() - - /** - * Returns the template for retrieving nth value of a URL path (zero indexed) e.g. {{{ request.path.[2] }}} - * @param index - */ - String path(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) - - /** - * Retruns the tempalte for retrieving the first value of a cookie with certain key - * @param key - */ - String cookie(String key) - - /** - * Request body text (avoid for non-text bodies) e.g. {{{ request.body }}} . The body will not be escaped - * so you won't be able to directly embed it in a JSON for example. - */ - String body() - - /** - * Request body text (avoid for non-text bodies) e.g. {{{ escapejsonbody }}} . The body will not be escaped - * so you will be able to embed it - */ - String escapedBody() - - /** - * Request body text for the given JsonPath. e.g. {{{ jsonpath this '$.a.b.c' }}} - */ - String body(String jsonPath) -} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.java b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.java new file mode 100644 index 0000000000..bafcec195b --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.java @@ -0,0 +1,204 @@ +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
+ */
+public interface ContractTemplate {
+ /**
+ * Asserts whether the given text starts with proper opening template
+ */
+ default boolean startsWithTemplate(String text) {
+ return text.startsWith(openingTemplate());
+ }
+
+ /**
+ * Asserts whether the given text starts with proper opening template for escaped value
+ */
+ default boolean startsWithEscapedTemplate(String text) {
+ return text.startsWith(escapedOpeningTemplate());
+ }
+
+ /**
+ * 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();
+
+ /**
+ * Handlebars is using the Mustache template thus it looks like this
+ * {{{ Mustache }}}. In this case the opening template would return {{{
+ */
+ default String escapedOpeningTemplate() {
+ return openingTemplate();
+ }
+
+ /**
+ * Handlebars is using the Mustache template thus it looks like this
+ * {{{ Mustache }}}. In this case the closing template would return }}}
+ */
+ default String escapedClosingTemplate() {
+ return closingTemplate();
+ }
+
+ /**
+ * Returns the template for retrieving a URL path and query 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 a URL path
+ */
+ String path();
+
+ /**
+ * Returns the template for retrieving nth value of a URL path (zero indexed) e.g. {{ request.path.[2] }}
+ *
+ * @param index
+ */
+ String path(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);
+
+ /**
+ * Retruns the template for retrieving the first value of a cookie with certain key
+ *
+ * @param key
+ */
+ String cookie(String key);
+
+ /**
+ * 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 for the given JsonPath. e.g. {{ jsonPath request.body '$.a.b.c' }}
+ */
+ String body(String jsonPath);
+
+ /**
+ * Returns the template for retrieving a escaped URL path and query from request
+ */
+ default String escapedUrl() {
+ return url();
+ }
+
+ /**
+ * Returns the template for retrieving escaped first value of a query parameter e.g. {{{ request.query.search }}}
+ *
+ * @param key
+ */
+ default String escapedQuery(String key) {
+ return query(key);
+ }
+
+ /**
+ * Returns the template for retrieving esacped nth value of a query parameter (zero indexed) e.g. {{{ request.query.search.[5] }}}
+ *
+ * @param key
+ * @param index
+ */
+ default String escapedQuery(String key, int index) {
+ return query(key, index);
+ }
+
+ /**
+ * Returns the template for retrieving a escaped URL path
+ */
+ default String escapedPath() {
+ return path();
+ }
+
+ /**
+ * Returns the template for retrieving escaped nth value of a URL path (zero indexed) e.g. {{{ request.path.[2] }}}
+ *
+ * @param index
+ */
+ default String escapedPath(int index) {
+ return path(index);
+ }
+
+ /**
+ * Returns the template for retrieving the escaped first value of a request header e.g. {{{ request.headers.X-Request-Id }}}
+ *
+ * @param key
+ */
+ default String escapedHeader(String key) {
+ return header(key);
+ }
+
+ /**
+ * Returns the template for retrieving the esacaped nth value of a request header (zero indexed) e.g. {{{ request.headers.X-Request-Id.[5] }}}
+ *
+ * @param key
+ * @param index
+ */
+ default String escapedHeader(String key, int index) {
+ return header(key, index);
+ }
+
+ /**
+ * Retruns the template for retrieving the escaped first value of a cookie with certain key
+ *
+ * @param key
+ */
+ default String escapedCookie(String key) {
+ return cookie(key);
+ }
+
+ /**
+ * 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.
+ */
+ default String escapedBody() {
+ return body();
+ }
+
+ /**
+ * Request body text for the given JsonPath. e.g. {{{ jsonPath request.body '$.a.b.c' }}}
+ */
+ default String escapedBody(String jsonPath) {
+ return body(jsonPath);
+ }
+
+}
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CompositeContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CompositeContractTemplate.groovy
new file mode 100644
index 0000000000..08397e4438
--- /dev/null
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CompositeContractTemplate.groovy
@@ -0,0 +1,155 @@
+package org.springframework.cloud.contract.spec.internal
+
+import groovy.transform.CompileStatic
+
+import org.springframework.cloud.contract.spec.ContractTemplate
+
+/**
+ * For backward compatibility when assertions take place,
+ * first checks the custom setup. Writes in a new format, can read
+ * the old format.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.1.0
+ */
+@CompileStatic
+class CompositeContractTemplate implements ContractTemplate {
+
+ private final CustomHandlebarsContractTemplate custom = new CustomHandlebarsContractTemplate()
+ private final HandlebarsContractTemplate template = new HandlebarsContractTemplate()
+
+ @Override
+ boolean startsWithTemplate(String text) {
+ if (this.custom.startsWithTemplate(text)) {
+ return true
+ }
+ return template.startsWithTemplate(text)
+ }
+
+ @Override
+ boolean startsWithEscapedTemplate(String text) {
+ return this.template.startsWithEscapedTemplate(text)
+ }
+
+ @Override
+ String openingTemplate() {
+ return this.template.openingTemplate()
+ }
+
+ @Override
+ String closingTemplate() {
+ return this.template.closingTemplate()
+ }
+
+ @Override
+ String escapedOpeningTemplate() {
+ return this.template.escapedOpeningTemplate()
+ }
+
+ @Override
+ String escapedClosingTemplate() {
+ return this.template.escapedClosingTemplate()
+ }
+
+ @Override
+ String url() {
+ return this.template.url()
+ }
+
+ @Override
+ String query(String key) {
+ return this.template.query(key)
+ }
+
+ @Override
+ String query(String key, int index) {
+ return this.template.query(key, index)
+ }
+
+ @Override
+ String path() {
+ return this.template.path()
+ }
+
+ @Override
+ String path(int index) {
+ return this.template.path(index)
+ }
+
+ @Override
+ String header(String key) {
+ return this.template.header(key)
+ }
+
+ @Override
+ String header(String key, int index) {
+ return this.template.header(key, index)
+ }
+
+ @Override
+ String cookie(String key) {
+ return this.template.cookie(key)
+ }
+
+ @Override
+ String body() {
+ return this.template.body()
+ }
+
+ @Override
+ String escapedBody() {
+ // WireMock doesn't support proper escaping of JSON body
+ // that's why we need to use our custom handlebars extension
+ return this.custom.escapedBody()
+ }
+
+ @Override
+ String escapedBody(String jsonPath) {
+ return this.template.escapedBody(jsonPath)
+ }
+
+ @Override
+ String body(String jsonPath) {
+ return this.template.body(jsonPath)
+ }
+
+ @Override
+ String escapedUrl() {
+ return this.template.escapedUrl()
+ }
+
+ @Override
+ String escapedQuery(String key) {
+ return this.template.escapedQuery(key)
+ }
+
+ @Override
+ String escapedQuery(String key, int index) {
+ return this.template.escapedQuery(key, index)
+ }
+
+ @Override
+ String escapedPath() {
+ return this.template.escapedPath()
+ }
+
+ @Override
+ String escapedPath(int index) {
+ return this.template.escapedPath(index)
+ }
+
+ @Override
+ String escapedHeader(String key) {
+ return this.template.escapedHeader(key)
+ }
+
+ @Override
+ String escapedHeader(String key, int index) {
+ return this.template.escapedHeader(key, index)
+ }
+
+ @Override
+ String escapedCookie(String key) {
+ return this.template.escapedCookie(key)
+ }
+}
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CustomHandlebarsContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CustomHandlebarsContractTemplate.groovy
new file mode 100644
index 0000000000..2598aace44
--- /dev/null
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/CustomHandlebarsContractTemplate.groovy
@@ -0,0 +1,94 @@
+package org.springframework.cloud.contract.spec.internal
+
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+
+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
+ *
+ * @deprecated use {@link HandlebarsContractTemplate}
+ */
+@CompileStatic
+@PackageScope
+class CustomHandlebarsContractTemplate 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 path() {
+ return wrapped("request.path")
+ }
+
+ @Override
+ String path(int index) {
+ return wrapped("request.path.[${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 cookie(String key) {
+ return wrapped("request.cookies.${key}")
+ }
+
+ @Override
+ String body() {
+ return wrapped("request.body")
+ }
+
+ @Override
+ String escapedBody() {
+ return wrapped("escapejsonbody")
+ }
+
+ @Override
+ String escapedBody(String jsonPath) {
+ return body(jsonPath)
+ }
+
+ @Override
+ String body(String jsonPath) {
+ return wrapped("jsonpath this '${jsonPath}'")
+ }
+
+ private String wrapped(String text) {
+ return openingTemplate() + text + closingTemplate()
+ }
+}
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy
index b56add4ee6..289e1f8d1c 100644
--- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy
@@ -19,14 +19,14 @@ class FromRequest {
}
private ContractTemplate template() {
- return new HandlebarsContractTemplate()
+ return new CompositeContractTemplate()
}
/**
* URL path and query
*/
DslProperty url() {
- return new DslProperty(template.url())
+ return new DslProperty(template.escapedUrl())
}
/**
@@ -34,7 +34,7 @@ class FromRequest {
* @param key
*/
DslProperty query(String key) {
- return new DslProperty(template.query(key))
+ return new DslProperty(template.escapedQuery(key))
}
/**
@@ -43,14 +43,14 @@ class FromRequest {
* @param index
*/
DslProperty query(String key, int index) {
- return new DslProperty(template.query(key, index))
+ return new DslProperty(template.escapedQuery(key, index))
}
/**
* URL path
*/
DslProperty path() {
- return new DslProperty(template.path())
+ return new DslProperty(template.escapedPath())
}
/**
@@ -58,7 +58,7 @@ class FromRequest {
* @param index
*/
DslProperty path(int index) {
- return new DslProperty(template.path(index))
+ return new DslProperty(template.escapedPath(index))
}
/**
@@ -66,7 +66,7 @@ class FromRequest {
* @param key
*/
DslProperty header(String key) {
- return new DslProperty(template.header(key))
+ return new DslProperty(template.escapedHeader(key))
}
/**
@@ -74,7 +74,7 @@ class FromRequest {
* @param key
*/
DslProperty header(String key, int index) {
- return new DslProperty(template.header(key, index))
+ return new DslProperty(template.escapedHeader(key, index))
}
/**
@@ -82,20 +82,97 @@ class FromRequest {
* @param key
*/
DslProperty cookie(String key) {
- return new DslProperty(template.cookie(key))
+ return new DslProperty(template.escapedCookie(key))
}
/**
* Request body text (avoid for non-text bodies)
*/
DslProperty body() {
- return new DslProperty(template.body())
+ return new DslProperty(template.escapedBody())
}
/**
* Request body text for the given JsonPath
*/
DslProperty body(String jsonPath) {
+ return new DslProperty(template.escapedBody(jsonPath))
+ }
+
+ /**
+ * Unescaped URL path and query
+ */
+ DslProperty rawUrl() {
+ return new DslProperty(template.url())
+ }
+
+ /**
+ * Unescaped First value of a query parameter e.g. request.query.search
+ * @param key
+ */
+ DslProperty rawQuery(String key) {
+ return new DslProperty(template.query(key))
+ }
+
+ /**
+ * Unescaped nth value of a query parameter (zero indexed) e.g. request.query.search.[5]
+ * @param key
+ * @param index
+ */
+ DslProperty rawQuery(String key, int index) {
+ return new DslProperty(template.query(key, index))
+ }
+
+ /**
+ * Unescaped URL path
+ */
+ DslProperty rawPath() {
+ return new DslProperty(template.path())
+ }
+
+ /**
+ * Unescaped nth value of a URL path (zero indexed) e.g. {{{ request.path.[2] }}}
+ * @param index
+ */
+ DslProperty rawPath(int index) {
+ return new DslProperty(template.path(index))
+ }
+
+ /**
+ * Unescaped First value of a request header e.g. request.headers.X-Request-Id
+ * @param key
+ */
+ DslProperty rawHeader(String key) {
+ return new DslProperty(template.header(key))
+ }
+
+ /**
+ * Unescaped nth value of a request header (zero indexed) e.g. request.headers.X-Request-Id
+ * @param key
+ */
+ DslProperty rawHeader(String key, int index) {
+ return new DslProperty(template.header(key, index))
+ }
+
+ /**
+ * Unescaped Retruns the tempalte for retrieving the first value of a cookie with certain key
+ * @param key
+ */
+ DslProperty rawCookie(String key) {
+ return new DslProperty(template.cookie(key))
+ }
+
+ /**
+ * Unescaped Request body text (avoid for non-text bodies)
+ */
+ DslProperty rawBody() {
+ return new DslProperty(template.body())
+ }
+
+ /**
+ * Unescaped Request body text for the given JsonPath
+ */
+ DslProperty rawBody(String jsonPath) {
return new DslProperty(template.body(jsonPath))
}
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy
index 9a5acb6614..73b5270e2a 100644
--- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy
@@ -15,11 +15,21 @@ class HandlebarsContractTemplate implements ContractTemplate {
@Override
String openingTemplate() {
- return "{{{"
+ return "{{"
}
@Override
String closingTemplate() {
+ return "}}"
+ }
+
+ @Override
+ String escapedOpeningTemplate() {
+ return "{{{"
+ }
+
+ @Override
+ String escapedClosingTemplate() {
return "}}}"
}
@@ -70,15 +80,64 @@ class HandlebarsContractTemplate implements ContractTemplate {
@Override
String escapedBody() {
- return wrapped("escapejsonbody")
+ return escapedWrapped("request.body")
+ }
+
+ @Override
+ String escapedBody(String jsonPath) {
+ return escapedWrapped("jsonPath request.body '${jsonPath}'")
}
@Override
String body(String jsonPath) {
- return wrapped("jsonpath this '${jsonPath}'")
+ return wrapped("jsonPath request.body '${jsonPath}'")
+ }
+
+ @Override
+ String escapedUrl() {
+ return escapedWrapped("request.url")
+ }
+
+ @Override
+ String escapedQuery(String key) {
+ return escapedQuery(key, 0)
+ }
+
+ @Override
+ String escapedQuery(String key, int index) {
+ return escapedWrapped("request.query.${key}.[${index}]")
+ }
+
+ @Override
+ String escapedPath() {
+ return escapedWrapped("request.path")
+ }
+
+ @Override
+ String escapedPath(int index) {
+ return escapedWrapped("request.path.[${index}]")
+ }
+
+ @Override
+ String escapedHeader(String key) {
+ return escapedHeader(key, 0)
+ }
+
+ @Override
+ String escapedHeader(String key, int index) {
+ return escapedWrapped("request.headers.${key}.[${index}]")
+ }
+
+ @Override
+ String escapedCookie(String key) {
+ return escapedWrapped("request.cookies.${key}")
}
private String wrapped(String text) {
return openingTemplate() + text + closingTemplate()
}
+
+ private String escapedWrapped(String text) {
+ return escapedOpeningTemplate() + text + escapedClosingTemplate()
+ }
}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
index 439fda17e0..3539b23cb3 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
@@ -69,7 +69,7 @@ abstract class MethodBodyBuilder {
private static final Closure GET_SERVER_VALUE = { it instanceof DslProperty ? it.serverValue : it }
private static final String FROM_REQUEST_PREFIX = "request."
- private static final String FROM_REQUEST_BODY = "body"
+ private static final String FROM_REQUEST_BODY = "escapejsonbody"
private static final String FROM_REQUEST_PATH = "path"
protected final ContractVerifierConfigProperties configProperties
@@ -422,11 +422,15 @@ abstract class MethodBodyBuilder {
String entryAsString = (String) entry
if (templateProcessor.containsTemplateEntry(entryAsString) &&
!templateProcessor.containsJsonPathTemplateEntry(entryAsString)) {
- String justEntry = entryAsString - contractTemplate.openingTemplate() -
+ // TODO: HANDLEBARS LEAKING VIA request.
+ String justEntry = entryAsString - contractTemplate.escapedOpeningTemplate() -
+ contractTemplate.openingTemplate() -
+ contractTemplate.escapedClosingTemplate() -
contractTemplate.closingTemplate() - FROM_REQUEST_PREFIX
if (justEntry == FROM_REQUEST_BODY) {
// the body should be transformed by standard mechanism
- return entry
+ return contractTemplate.escapedOpeningTemplate() + FROM_REQUEST_PREFIX +
+ "escapedBody" + contractTemplate.escapedClosingTemplate()
}
try {
Object result = new PropertyUtilsBean().getProperty(templateModel, justEntry)
@@ -451,6 +455,8 @@ abstract class MethodBodyBuilder {
Object object = parsedRequestBody.read(jsonPathEntry)
if (!(object instanceof String)) {
return method
+ .replace('"' + contractTemplate.escapedOpeningTemplate(), contractTemplate.escapedOpeningTemplate())
+ .replace(contractTemplate.escapedClosingTemplate() + '"', contractTemplate.escapedClosingTemplate())
.replace('"' + contractTemplate.openingTemplate(), contractTemplate.openingTemplate())
.replace(contractTemplate.closingTemplate() + '"', contractTemplate.closingTemplate())
}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy
index fcc7b5c37a..841d97d243 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy
@@ -41,14 +41,14 @@ class TestSideRequestTemplateModel {
final Map