Added support for form param

without this change we supported only text and json content type body.
with this change we also support url encoded form parameters

fixes gh-578
This commit is contained in:
Marcin Grzejszczak
2018-03-16 12:05:53 +01:00
parent 068206f02a
commit e222142321
7 changed files with 203 additions and 41 deletions

View File

@@ -43,6 +43,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig
import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor
import org.springframework.cloud.contract.verifier.template.TemplateProcessor
import org.springframework.cloud.contract.verifier.util.ContentType
import org.springframework.cloud.contract.verifier.util.ContentUtils
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
import org.springframework.cloud.contract.verifier.util.MapConverter
@@ -314,7 +315,7 @@ abstract class MethodBodyBuilder {
if (convertedResponseBody instanceof GString) {
convertedResponseBody = extractValue(convertedResponseBody as GString, contentType, { Object o -> o instanceof DslProperty ? o.serverValue : o })
}
if (contentType != ContentType.TEXT) {
if (contentType != ContentType.TEXT && contentType != ContentType.FORM) {
convertedResponseBody = MapConverter.getTestSideValues(convertedResponseBody)
} else {
convertedResponseBody = StringEscapeUtils.escapeJava(convertedResponseBody.toString())
@@ -632,13 +633,13 @@ abstract class MethodBodyBuilder {
*/
protected Object extractServerValueFromBody(bodyValue) {
if (bodyValue instanceof GString) {
bodyValue = extractValue(bodyValue, ContentType.from(MapConverter.getTestSideValues(this.contract.request?.headers?.entries?.find {
it.name.toLowerCase() == "Content-Type".toLowerCase()
}).toString()), GET_SERVER_VALUE)
} else {
bodyValue = MapConverter.transformValues(bodyValue, GET_SERVER_VALUE)
return extractValue(bodyValue, contentType(), GET_SERVER_VALUE)
}
return bodyValue
return MapConverter.transformValues(bodyValue, GET_SERVER_VALUE)
}
protected ContentType contentType() {
return ContentUtils.recognizeContentTypeFromTestHeader(this.contract.request?.headers)
}
/**

View File

@@ -200,9 +200,23 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder {
@Override
protected String getBodyAsString() {
Object bodyValue = extractServerValueFromBody(request.body.serverValue)
String json = new JsonOutput().toJson(bodyValue)
json = convertUnicodeEscapesIfRequired(json)
return trimRepeatedQuotes(json)
if (contentType() == ContentType.FORM) {
if (bodyValue instanceof Map) {
// [a:3, b:4] == "a=3&b=4"
return ((Map) bodyValue).collect {
convertUnicodeEscapesIfRequired(it.key.toString() + "=" + it.value)
}.join("&")
} else if (bodyValue instanceof List) {
// ["a=3", "b=4"] == "a=3&b=4"
return ((List) bodyValue).collect {
convertUnicodeEscapesIfRequired(it.toString())
}.join("&")
}
} else {
String json = new JsonOutput().toJson(bodyValue)
json = convertUnicodeEscapesIfRequired(json)
return trimRepeatedQuotes(json)
}
}
/**

View File

@@ -23,6 +23,7 @@ import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder
import com.github.tomakehurst.wiremock.matching.StringValuePattern
import com.github.tomakehurst.wiremock.matching.UrlPattern
import groovy.json.JsonOutput
import groovy.json.StringEscapeUtils
import groovy.transform.PackageScope
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
@@ -71,9 +72,10 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
return null
}
RequestPatternBuilder requestPatternBuilder = appendMethodAndUrl()
appendHeaders(requestPatternBuilder)
appendQueryParameters(requestPatternBuilder)
appendBody(requestPatternBuilder)
ContentType contentType = tryToGetContentType(request?.body?.clientValue, request?.headers)
appendHeaders(requestPatternBuilder, contentType)
appendQueryParameters(requestPatternBuilder, contentType)
appendBody(requestPatternBuilder, contentType)
appendMultipart(requestPatternBuilder)
return requestPatternBuilder.build()
}
@@ -87,11 +89,10 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
return RequestPatternBuilder.newRequestPattern(requestMethod, urlPattern)
}
private void appendBody(RequestPatternBuilder requestPattern) {
private void appendBody(RequestPatternBuilder requestPattern, ContentType contentType) {
if (!request.body) {
return
}
ContentType contentType = tryToGetContentType(request.body.clientValue, request.headers)
if (contentType == ContentType.JSON) {
def originalBody = getMatchingStrategyFromBody(request.body)?.clientValue
def body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.matchers)
@@ -113,9 +114,10 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
requestPattern.withRequestBody(WireMock.equalToXml(getMatchingStrategy(request.body.clientValue).clientValue.toString()))
} else if (containsPattern(request?.body)) {
MatchingStrategy matchingStrategy = appendBodyRegexpMatchPattern(request.body)
requestPattern.withRequestBody(convertToValuePattern(matchingStrategy))
requestPattern.withRequestBody(convertToValuePattern(matchingStrategy, contentType))
} else {
requestPattern.withRequestBody(convertToValuePattern(getMatchingStrategy(request.body.clientValue)))
requestPattern.withRequestBody(convertToValuePattern(
getMatchingStrategy(request.body.clientValue), contentType))
}
}
@@ -139,12 +141,12 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
}
private void appendHeaders(RequestPatternBuilder requestPattern) {
private void appendHeaders(RequestPatternBuilder requestPattern, ContentType contentType) {
if(!request.headers) {
return
}
request.headers.entries.each {
requestPattern.withHeader(it.name, convertToValuePattern(it.clientValue))
requestPattern.withHeader(it.name, convertToValuePattern(it.clientValue, contentType))
}
}
@@ -190,15 +192,15 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
return clientSide
}
private void appendQueryParameters(RequestPatternBuilder requestPattern) {
private void appendQueryParameters(RequestPatternBuilder requestPattern, ContentType contentType) {
QueryParameters queryParameters = request?.urlPath?.queryParameters ?: request?.url?.queryParameters
queryParameters?.parameters?.each {
requestPattern.withQueryParam(it.name, convertToValuePattern(it.clientValue))
requestPattern.withQueryParam(it.name, convertToValuePattern(it.clientValue, contentType))
}
}
@TypeChecked(TypeCheckingMode.SKIP)
private static StringValuePattern convertToValuePattern(Object object) {
private static StringValuePattern convertToValuePattern(Object object, ContentType contentType) {
switch (object) {
case Pattern:
Pattern value = object as Pattern
@@ -215,7 +217,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
return WireMock.absent()
default:
try {
return WireMock."${value.type.name}"(value.clientValue)
return WireMock."${value.type.name}"(clientBody(value.clientValue, contentType))
} catch (Throwable t) {
log.error("Exception occurred while trying to call WireMock.${value.type.name}(${value.clientValue})", t)
throw t
@@ -226,6 +228,23 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
}
protected static Object clientBody(Object bodyValue, ContentType contentType) {
if (contentType == ContentType.FORM) {
if (bodyValue instanceof Map) {
// [a:3, b:4] == "a=3&b=4"
return ((Map) bodyValue).collect {
StringEscapeUtils.unescapeJavaScript(it.key.toString() + "=" + it.value)
}.join("&")
} else if (bodyValue instanceof List) {
// ["a=3", "b=4"] == "a=3&b=4"
return ((List) bodyValue).collect {
StringEscapeUtils.unescapeJavaScript(it.toString())
}.join("&")
}
}
return bodyValue
}
private MatchingStrategy getMatchingStrategyFromBody(Body body) {
if(!body) {
return null

View File

@@ -26,6 +26,7 @@ enum ContentType {
JSON("application/json"),
XML("application/xml"),
TEXT("text/plain"),
FORM("application/x-www-form-urlencoded"),
UNKNOWN("application/octet-stream")
final String mimeType
@@ -34,19 +35,4 @@ enum ContentType {
this.mimeType = mimeType
}
static ContentType from(String header) {
try {
if (header.contains("json")) {
return JSON
} else if (header.contains("xml")) {
return XML
} else if (header.contains("text") ||
header.contains("application/x-www-form-urlencoded")) {
// we want both to be treated as text
return TEXT
}
} catch(e) {}
return UNKNOWN
}
}

View File

@@ -24,6 +24,7 @@ import groovy.util.logging.Slf4j
import org.codehaus.groovy.runtime.GStringImpl
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.Header
import org.springframework.cloud.contract.spec.internal.Headers
import org.springframework.cloud.contract.spec.internal.MatchingStrategy
import org.springframework.cloud.contract.spec.internal.NamedProperty
@@ -76,7 +77,7 @@ class ContentUtils {
if (bodyAsValue.isEmpty()){
return bodyAsValue
}
if (contentType == ContentType.TEXT) {
if (contentType == ContentType.TEXT || contentType == ContentType.FORM) {
return extractValueForText(bodyAsValue, valueProvider)
}
if (contentType == ContentType.JSON) {
@@ -281,8 +282,9 @@ class ContentUtils {
return val[1]
}
static ContentType recognizeContentTypeFromHeader(Headers headers) {
String content = headers?.entries.find { it.name == "Content-Type" } ?.clientValue?.toString()
static ContentType recognizeContentTypeFromHeader(Headers headers, Closure<Object> closure) {
Header header = headers?.entries?.find { it.name == "Content-Type" }
String content = closure(header)?.toString()
if (content?.contains("json")) {
return ContentType.JSON
}
@@ -292,9 +294,20 @@ class ContentUtils {
if (content?.contains("text")) {
return ContentType.TEXT
}
if (content?.contains("form-urlencoded")) {
return ContentType.FORM
}
return ContentType.UNKNOWN
}
static ContentType recognizeContentTypeFromHeader(Headers headers) {
return recognizeContentTypeFromHeader(headers, { Header header -> header?.clientValue })
}
static ContentType recognizeContentTypeFromTestHeader(Headers headers) {
return recognizeContentTypeFromHeader(headers, { Header header -> header?.serverValue })
}
static MatchingStrategy.Type getEqualsTypeFromContentType(ContentType contentType) {
switch (contentType) {
case ContentType.JSON:

View File

@@ -528,6 +528,61 @@ DocumentContext parsedJson = JsonPath.parse(json);
"JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) }
}
@Issue("#578")
def "should work for form parameters [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method POST()
urlPath('/oauth/token')
headers {
header(authorization(), anyNonBlankString())
header(contentType(), applicationFormUrlencoded())
header(accept(), applicationJson())
}
body([
username : 'user',
password : 'password',
grant_type: 'password'
])
}
response {
status 200
headers {
header(contentType(), applicationJson())
}
body([
refresh_token: 'RANDOM_REFRESH_TOKEN',
access_token : 'RANDOM_ACCESS_TOKEN',
token_type : 'bearer',
expires_in : 3600,
scope : ['task'],
user : [
id : 1,
username: 'user',
name : 'User'
]
])
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
then:
String test = blockBuilder.toString()
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test)
test.contains("username=user&password=password&grant_type=password")
and:
stubMappingIsValidWireMockStub(contractDsl)
where:
methodBuilderName | methodBuilder
"MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
"MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) }
"JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
"JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) }
}
@Issue("#509")
def "classToCheck() should return class of object"() {
given:

View File

@@ -1834,6 +1834,80 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
server?.shutdown()
}
@Issue('#578')
def "should generate a stub for a request with form parameters"() {
given:
Contract groovyDsl = Contract.make {
request {
method POST()
urlPath('/oauth/token')
headers {
header(authorization(), anyNonBlankString())
header(contentType(), applicationFormUrlencoded())
header(accept(), applicationJson())
}
body([
username : 'user',
password : 'password',
grant_type: 'password'
])
}
response {
status 200
headers {
header(contentType(), applicationJson())
}
body([
refresh_token: 'RANDOM_REFRESH_TOKEN',
access_token : 'RANDOM_ACCESS_TOKEN',
token_type : 'bearer',
expires_in : 3600,
scope : ['task'],
user : [
id : 1,
username: 'user',
name : 'User'
]
])
}
}
when:
def json = toWireMockClientJsonStub(groovyDsl)
then:
AssertionUtil.assertThatJsonsAreEqual(('''
{
"request" : {
"urlPath" : "/oauth/token",
"method" : "POST",
"headers" : {
"Authorization" : {
"matches" : "^\\\\s*\\\\S[\\\\S\\\\s]*"
},
"Content-Type" : {
"equalTo" : "application/x-www-form-urlencoded"
},
"Accept" : {
"equalTo" : "application/json"
}
},
"bodyPatterns" : [ {
"equalTo" : "username=user&password=password&grant_type=password"
} ]
},
"response" : {
"status" : 200,
"body" : "{\\"access_token\\":\\"RANDOM_ACCESS_TOKEN\\",\\"refresh_token\\":\\"RANDOM_REFRESH_TOKEN\\",\\"scope\\":[\\"task\\"],\\"token_type\\":\\"bearer\\",\\"expires_in\\":3600,\\"user\\":{\\"name\\":\\"User\\",\\"id\\":1,\\"username\\":\\"user\\"}}",
"headers" : {
"Content-Type" : "application/json"
},
"transformers" : [ "response-template", "foo-transformer" ]
}
}
'''), json)
and:
stubMappingIsValidWireMockStub(json)
}
@Issue('#269')
def "should create a stub for dot separated keys"() {
given: