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> headers /** - * Escaped request body that can be put into test + * Request body as it would be sent to the controller */ final String body /** - * Request body as it would be sent to the controller + * Escaped request body that can be put into test */ - final String rawBody + final String escapedBody static TestSideRequestTemplateModel from(final Request request) { String url = MapConverter.getTestSideValues(request.url ?: request.urlPath) @@ -65,9 +65,9 @@ class TestSideRequestTemplateModel { }?.collectEntries { [(it.key): it.value.collect { MapConverter.getTestSideValues(it) }] }) - String body = trimmedAndEscapedBody(request.body) - String rawBody = getBodyAsRawJson(request.body) - return new TestSideRequestTemplateModel(fullUrl, query, paths, headers, body, rawBody) + String escapedBody = trimmedAndEscapedBody(request.body) + String body = getBodyAsRawJson(request.body) + return new TestSideRequestTemplateModel(fullUrl, query, paths, headers, body, escapedBody) } private static List buildPathsFromUrl(String url) { @@ -86,6 +86,9 @@ class TestSideRequestTemplateModel { private static String getBodyAsRawJson(Object body) { Object bodyValue = extractServerValueFromBody(body) + if (bodyValue instanceof GString || bodyValue instanceof String) { + return bodyValue.toString() + } return bodyValue != null ? new JsonOutput().toJson(bodyValue) : bodyValue } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy index 72c6007c23..b2bec99730 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy @@ -34,7 +34,7 @@ class HandlebarsEscapeHelper implements Helper> { } private Object returnObjectForTest(Object model) { - return ((TestSideRequestTemplateModel) model).rawBody + return ((TestSideRequestTemplateModel) model).escapedBody } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy index 35ac89d92e..351a5c73e3 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsJsonPathHelper.groovy @@ -1,5 +1,7 @@ package org.springframework.cloud.contract.verifier.builder.handlebars +import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.WireMockHelpers +import org.apache.commons.text.StringEscapeUtils import wiremock.com.github.jknack.handlebars.Helper import wiremock.com.github.jknack.handlebars.Options import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel @@ -15,21 +17,41 @@ import org.springframework.cloud.contract.verifier.builder.TestSideRequestTempla * @since 1.1.0 */ @CompileStatic -class HandlebarsJsonPathHelper implements Helper> { +class HandlebarsJsonPathHelper implements Helper { public static final String NAME = "jsonpath" public static final String REQUEST_MODEL_NAME = "request" @Override - Object apply(Map context, Options options) throws IOException { - String jsonPath = options.param(0) - Object model = context.get(REQUEST_MODEL_NAME) - if (model instanceof TestSideRequestTemplateModel) { - return returnObjectForTest(model, jsonPath) - } else if (model instanceof RequestTemplateModel) { - return returnObjectForStub(model, jsonPath) + Object apply(Object context, Options options) throws IOException { + if (context instanceof Map) { + // legacy + Map oldContext = (Map) context + String jsonPath = options.param(0) + Object model = oldContext.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") + } else if (context instanceof String) { + Object value = WireMockHelpers.jsonPath.apply(context, options) + if (testSideModel(options)) { + return processTestResponseValue(value) + } + return value } - throw new IllegalArgumentException("Unsupported model") + throw new IllegalArgumentException("Unsupported context") + } + + private boolean testSideModel(Options options) { + Object model = options.context.model() + if (!(model instanceof Map)) { + return false + } + Map map = (Map) model + return map.values().any { it instanceof TestSideRequestTemplateModel } } private Object returnObjectForStub(Object model, String jsonPath) { @@ -37,10 +59,14 @@ class HandlebarsJsonPathHelper implements Helper> { return documentContext.read(jsonPath) } - private Object returnObjectForTest(Object model, String jsonPath) { - String body = removeSurroundingQuotes(((TestSideRequestTemplateModel) model).rawBody).replace('\\"', '"') + private Object returnObjectForTest(TestSideRequestTemplateModel model, String jsonPath) { + String body = removeSurroundingQuotes(model.escapedBody).replace('\\"', '"') DocumentContext documentContext = JsonPath.parse(body) Object value = documentContext.read(jsonPath) + return processTestResponseValue(value) + } + + private Object processTestResponseValue(Object value) { if (value instanceof Long) { return String.valueOf(value) + "L" } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy index 0ab11f66db..6f9422bc19 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -117,7 +117,7 @@ abstract class BaseWireMockStubStrategy { return processEntriesForTemplating(transformedMap, context) } - private Object processEntriesForTemplating(transformedMap, DocumentContext context) { + private Object processEntriesForTemplating(Object transformedMap, DocumentContext context) { return transformValues(transformedMap, { if (it instanceof String && processor.containsJsonPathTemplateEntry(it)) { String jsonPath = processor.jsonPathFromTemplateEntry(it) @@ -129,7 +129,8 @@ abstract class BaseWireMockStubStrategy { return it } return "${WRAPPER}${it}${WRAPPER}" - } else if (it instanceof String && processor.containsTemplateEntry(it) && template.body() == it) { + } else if (it instanceof String && processor.containsTemplateEntry(it) && + template.escapedBody() == it) { return template.escapedBody() } return it diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy index fa9c5d575a..f7c0192526 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/HandlebarsTemplateProcessor.groovy @@ -1,16 +1,19 @@ package org.springframework.cloud.contract.verifier.template -import wiremock.com.github.jknack.handlebars.Handlebars -import wiremock.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 + +import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.WireMockHelpers +import groovy.transform.CompileStatic +import wiremock.com.github.jknack.handlebars.Handlebars +import wiremock.com.github.jknack.handlebars.Template + +import org.springframework.cloud.contract.spec.ContractTemplate +import org.springframework.cloud.contract.spec.internal.CompositeContractTemplate +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.builder.TestSideRequestTemplateModel +import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper + /** * Default Handlebars template processor * @@ -20,10 +23,17 @@ import java.util.regex.Pattern @CompileStatic class HandlebarsTemplateProcessor implements TemplateProcessor, ContractTemplate { - private static final Pattern JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{\\{jsonpath this '(.*)'}}}.*\$") + private static final Pattern ESCAPED_LEGACY_JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{\\{jsonpath this '(.*)'}}}.*\$") + private static final Pattern ESCAPED_JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{\\{jsonPath request.body '(.*)'}}}.*\$") + private static final Pattern LEGACY_JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{jsonpath this '(.*)'}}.*\$") + private static final Pattern JSON_PATH_PATTERN = Pattern.compile("^.*\\{\\{jsonPath request.body '(.*)'}}.*\$") + private static final List PATTERNS = [ESCAPED_LEGACY_JSON_PATH_PATTERN, + ESCAPED_JSON_PATH_PATTERN, LEGACY_JSON_PATH_PATTERN, JSON_PATH_PATTERN] + private static final String LEGACY_JSON_PATH_TEMPLATE_NAME = HandlebarsJsonPathHelper.NAME + private static final String JSON_PATH_TEMPLATE_NAME = WireMockHelpers.jsonPath.name() @Delegate - private final ContractTemplate contractTemplate = new HandlebarsContractTemplate() + private final ContractTemplate contractTemplate = new CompositeContractTemplate() @Override String transform(Request request, String testContents) { @@ -35,12 +45,16 @@ class HandlebarsTemplateProcessor implements TemplateProcessor, ContractTemplate @Override boolean containsTemplateEntry(String line) { - return line.matches('^.*\\{\\{\\{.*}}}.*$') + return (line.contains(contractTemplate.openingTemplate()) && line.contains(contractTemplate.closingTemplate())) || + (line.contains(contractTemplate.escapedOpeningTemplate()) && line.contains(contractTemplate.escapedClosingTemplate())) } @Override boolean containsJsonPathTemplateEntry(String line) { - return line.contains(openingTemplate() + HandlebarsJsonPathHelper.NAME) + return line.contains(openingTemplate() + LEGACY_JSON_PATH_TEMPLATE_NAME) || + line.contains(openingTemplate() + JSON_PATH_TEMPLATE_NAME) || + line.contains(escapedOpeningTemplate() + LEGACY_JSON_PATH_TEMPLATE_NAME) || + line.contains(escapedOpeningTemplate() + JSON_PATH_TEMPLATE_NAME) } @Override @@ -48,11 +62,13 @@ class HandlebarsTemplateProcessor implements TemplateProcessor, ContractTemplate if (!containsJsonPathTemplateEntry(line)) { return "" } - Matcher matcher = JSON_PATH_PATTERN.matcher(line) - if (!matcher.matches()) { - return "" + for (Pattern pattern : PATTERNS) { + Matcher matcher = pattern.matcher(line) + if (matcher.matches()) { + return matcher.group(1) + } } - return matcher.group(1) + return "" } private String templatedResponseBody(Map< String, TestSideRequestTemplateModel> model, Template bodyTemplate) { @@ -71,6 +87,12 @@ class HandlebarsTemplateProcessor implements TemplateProcessor, ContractTemplate try { Handlebars handlebars = new Handlebars() handlebars.registerHelper(HandlebarsJsonPathHelper.NAME, new HandlebarsJsonPathHelper()) + handlebars.registerHelper(WireMockHelpers.jsonPath.name(), new HandlebarsJsonPathHelper()) + WireMockHelpers.values() + .findAll { it != WireMockHelpers.jsonPath} + .each { WireMockHelpers helper -> + handlebars.registerHelper(helper.name(), helper) + } return handlebars.compileInline(content) } catch (IOException e) { throw new RuntimeException(e) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy index 0d768437fc..94c38f24c9 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/template/TemplateProcessor.groovy @@ -26,14 +26,14 @@ interface TemplateProcessor { /** * 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 + * E.g. for Handlebars if a line contains {{{jsonpath ...}}} or {{{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 + * E.g. for Handlebars if a line contains {{{jsonpath this '$.a.b.c'}}} or {{{jsonPath request.body ...}}} then * the te method would return {@code $.a.b.c}. Returns empty string if there's no matching * json path entry */ diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index 1b96f457be..c9348237c8 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -1515,8 +1515,7 @@ World.'''""" method "GET" urlPath('/auth/oauth/check_token') { queryParameters { - parameter 'token': - value( + parameter 'token': value( consumer(regex('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')), producer('6973b31d-7140-402a-bca6-1cdb954e03a7') ) @@ -1551,8 +1550,7 @@ World.'''""" method "GET" urlPath('/auth/oauth/check_token') { queryParameters { - parameter 'token': - value( + parameter 'token': value( consumer(regex('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')), producer('6973b31d-7140-402a-bca6-1cdb954e03a7') ) @@ -2594,7 +2592,17 @@ DocumentContext parsedJson = JsonPath.parse(json); fullBody: fromRequest().body(), responseFoo: fromRequest().body('$.foo'), responseBaz: fromRequest().body('$.baz'), - responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla" + responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla", + rawUrl: fromRequest().rawUrl(), + rawPath: fromRequest().rawPath(), + rawPathIndex: fromRequest().rawPath(1), + rawParam: fromRequest().rawQuery("foo"), + rawParamIndex: fromRequest().rawQuery("foo", 1), + rawAuthorization: fromRequest().rawHeader("Authorization"), + rawAuthorization2: fromRequest().rawHeader("Authorization", 1), + rawResponseFoo: fromRequest().rawBody('$.foo'), + rawResponseBaz: fromRequest().rawBody('$.baz'), + rawResponseBaz2: "Bla bla ${fromRequest().rawBody('$.foo')} bla bla" ) } } @@ -2608,6 +2616,7 @@ DocumentContext parsedJson = JsonPath.parse(json); SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) then: !test.contains('''DslProperty''') + !test.contains('''ERROR: ''') test.contains('''assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2")''') test.contains('''assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx")''') test.contains('''assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1")''') @@ -2619,6 +2628,16 @@ DocumentContext parsedJson = JsonPath.parse(json); 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")''') + test.contains('''assertThatJson(parsedJson).field("['rawUrl']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2")''') + test.contains('''assertThatJson(parsedJson).field("['rawPath']").isEqualTo("/api/v1/xxxx")''') + test.contains('''assertThatJson(parsedJson).field("['rawPathIndex']").isEqualTo("v1")''') + test.contains('''assertThatJson(parsedJson).field("['rawParamIndex']").isEqualTo("bar2")''') + test.contains('''assertThatJson(parsedJson).field("['rawResponseFoo']").isEqualTo("bar")''') + test.contains('''assertThatJson(parsedJson).field("['rawAuthorization']").isEqualTo("secret")''') + test.contains('''assertThatJson(parsedJson).field("['rawAuthorization2']").isEqualTo("secret2")''') + test.contains('''assertThatJson(parsedJson).field("['rawResponseBaz']").isEqualTo(5)''') + test.contains('''assertThatJson(parsedJson).field("['rawResponseBaz2']").isEqualTo("Bla bla bar bla bla")''') + test.contains('''assertThatJson(parsedJson).field("['rawParam']").isEqualTo("bar")''') responseAssertion(test) where: methodBuilderName | methodBuilder | responseAssertion diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy index 96b20b540b..565336dedd 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy @@ -1736,7 +1736,73 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie } @Issue('#237') - def "should generate a stub with response template"() { + def "should work with legacy mappings"() { + given: + def oldJsonMapping = ''' + { + "request" : { + "urlPath" : "/api/v1/xxxx", + "method" : "POST", + "headers" : { + "Authorization" : { + "equalTo" : "secret2" + } + }, + "cookies" : { + "foo" : { + "equalTo" : "bar" + } + }, + "queryParameters" : { + "foo" : { + "equalTo" : "bar2" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.['baz'] == 5)]" + }, { + "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"authorization\\":\\"{{{request.headers.Authorization.[0]}}}\\",\\"path\\":\\"{{{request.path}}}\\",\\"responseBaz\\":{{{jsonpath this '$.baz'}}} ,\\"param\\":\\"{{{request.query.foo.[0]}}}\\",\\"pathIndex\\":\\"{{{request.path.[1]}}}\\",\\"responseBaz2\\":\\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\\",\\"responseFoo\\":\\"{{{jsonpath this '$.foo'}}}\\",\\"authorization2\\":\\"{{{request.headers.Authorization.[1]}}}\\",\\"fullBody\\":\\"{{{escapejsonbody}}}\\",\\"url\\":\\"{{{request.url}}}\\",\\"paramIndex\\":\\"{{{request.query.foo.[1]}}}\\"}", + "headers" : { + "Authorization" : "{{{request.headers.Authorization.[0]}}};foo" + }, + "transformers" : [ "response-template", "foo-transformer" ] + } + } + ''' + and: + int port = SocketUtils.findAvailableTcpPort() + WireMockServer server = new WireMockServer(config().port(port)) + server.start() + and: + server.addStubMapping(WireMockStubMapping.buildFrom(oldJsonMapping)) + when: + ResponseEntity entity = call(port) + then: + entity.headers.find { it.key == "Authorization" && it.value.contains("secret;foo") } + 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() + } + + @Issue('#540') + def "should generate a stub with standard WireMock request template"() { given: org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { request { @@ -1772,7 +1838,17 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie fullBody: fromRequest().body(), responseFoo: fromRequest().body('$.foo'), responseBaz: fromRequest().body('$.baz'), - responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla" + responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla", + rawUrl: fromRequest().rawUrl(), + rawPath: fromRequest().rawPath(), + rawPathIndex: fromRequest().rawPath(1), + rawParam: fromRequest().rawQuery("foo"), + rawParamIndex: fromRequest().rawQuery("foo", 1), + rawAuthorization: fromRequest().rawHeader("Authorization"), + rawAuthorization2: fromRequest().rawHeader("Authorization", 1), + rawResponseFoo: fromRequest().rawBody('$.foo'), + rawResponseBaz: fromRequest().rawBody('$.baz'), + rawResponseBaz2: "Bla bla ${fromRequest().rawBody('$.foo')} bla bla" ) } } @@ -1807,7 +1883,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie }, "response" : { "status" : 200, - "body" : "{\\"authorization\\":\\"{{{request.headers.Authorization.[0]}}}\\",\\"path\\":\\"{{{request.path}}}\\",\\"responseBaz\\":{{{jsonpath this '$.baz'}}} ,\\"param\\":\\"{{{request.query.foo.[0]}}}\\",\\"pathIndex\\":\\"{{{request.path.[1]}}}\\",\\"responseBaz2\\":\\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\\",\\"responseFoo\\":\\"{{{jsonpath this '$.foo'}}}\\",\\"authorization2\\":\\"{{{request.headers.Authorization.[1]}}}\\",\\"fullBody\\":\\"{{{escapejsonbody}}}\\",\\"url\\":\\"{{{request.url}}}\\",\\"paramIndex\\":\\"{{{request.query.foo.[1]}}}\\"}", + "body" : "{\\"rawAuthorization2\\":\\"{{request.headers.Authorization.[1]}}\\",\\"responseBaz\\":{{{jsonPath request.body '$.baz'}}} ,\\"pathIndex\\":\\"{{{request.path.[1]}}}\\",\\"rawAuthorization\\":\\"{{request.headers.Authorization.[0]}}\\",\\"authorization2\\":\\"{{{request.headers.Authorization.[1]}}}\\",\\"rawParam\\":\\"{{request.query.foo.[0]}}\\",\\"url\\":\\"{{{request.url}}}\\",\\"paramIndex\\":\\"{{{request.query.foo.[1]}}}\\",\\"authorization\\":\\"{{{request.headers.Authorization.[0]}}}\\",\\"path\\":\\"{{{request.path}}}\\",\\"rawUrl\\":\\"{{request.url}}\\",\\"rawPath\\":\\"{{request.path}}\\",\\"rawResponseBaz2\\":\\"Bla bla {{jsonPath request.body '$.foo'}} bla bla\\",\\"param\\":\\"{{{request.query.foo.[0]}}}\\",\\"rawResponseBaz\\":{{jsonPath request.body '$.baz'}} ,\\"responseBaz2\\":\\"Bla bla {{{jsonPath request.body '$.foo'}}} bla bla\\",\\"rawResponseFoo\\":\\"{{jsonPath request.body '$.foo'}}\\",\\"responseFoo\\":\\"{{{jsonPath request.body '$.foo'}}}\\",\\"rawPathIndex\\":\\"{{request.path.[1]}}\\",\\"fullBody\\":\\"{{{escapejsonbody}}}\\",\\"rawParamIndex\\":\\"{{request.query.foo.[1]}}\\"}", "headers" : { "Authorization" : "{{{request.headers.Authorization.[0]}}};foo" }, @@ -1827,16 +1903,28 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie entity.headers.find { it.key == "Authorization" && it.value.contains("secret;foo") } 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" + { + "rawAuthorization2":"secret2", + "responseBaz":5, + "pathIndex":"v1", + "rawAuthorization":"secret", + "authorization2":"secret2", + "rawParam":"bar", + "url":"/api/v1/xxxx?foo=bar&foo=bar2", + "paramIndex":"bar2", + "authorization":"secret", + "path":"/api/v1/xxxx", + "rawUrl":"/api/v1/xxxx?foo=bar&foo=bar2", + "rawPath":"/api/v1/xxxx", + "rawResponseBaz2":"Bla bla bar bla bla", + "param":"bar", + "rawResponseBaz":5, + "responseBaz2":"Bla bla bar bla bla", + "rawResponseFoo":"bar", + "responseFoo":"bar", + "rawPathIndex":"v1", + "fullBody":"{\\"foo\\":\\"bar\\",\\"baz\\":5}", + "rawParamIndex":"bar2" } '''), entity.body) cleanup: