Migrated to default WireMock helper methods; fixes gh-540

This commit is contained in:
Marcin Grzejszczak
2018-08-24 15:10:55 +02:00
parent a0bc2eb1c4
commit debea6edfd
15 changed files with 827 additions and 162 deletions

View File

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

View File

@@ -0,0 +1,204 @@
package org.springframework.cloud.contract.spec;
/**
* Contract for defining templated responses.
* <p>
* 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,14 +41,14 @@ class TestSideRequestTemplateModel {
final Map<String, List<String>> 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<String> 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
}

View File

@@ -34,7 +34,7 @@ class HandlebarsEscapeHelper implements Helper<Map<String, Object>> {
}
private Object returnObjectForTest(Object model) {
return ((TestSideRequestTemplateModel) model).rawBody
return ((TestSideRequestTemplateModel) model).escapedBody
}
}

View File

@@ -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<Map<String, Object>> {
class HandlebarsJsonPathHelper implements Helper<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)
Object apply(Object context, Options options) throws IOException {
if (context instanceof Map<String, Object>) {
// legacy
Map<String, Object> oldContext = (Map<String, Object>) 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<Map<String, Object>> {
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"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String> 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&#x3D;bar&amp;foo&#x3D;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: