Added support for multipart with content type, fixes gh-599
This commit is contained in:
@@ -7,6 +7,9 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
|
||||
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
@@ -14,6 +17,9 @@ import com.example.loan.model.Client;
|
||||
import com.example.loan.model.LoanApplication;
|
||||
import com.example.loan.model.LoanApplicationResult;
|
||||
import com.example.loan.model.LoanApplicationStatus;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -81,4 +87,36 @@ public class LoanApplicationServiceTests {
|
||||
assertThat(cookies).isEqualTo("foo bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyWorkWithMultipart() {
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.add("file1", new ByteArrayResource(("content").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "file1";
|
||||
}
|
||||
});
|
||||
parameters.add("file2", new ByteArrayResource(("content").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "file2";
|
||||
}
|
||||
});
|
||||
parameters.add("test", new ByteArrayResource(("{\n \"status\": \"test\"\n}").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "test";
|
||||
}
|
||||
});
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "multipart/form-data");
|
||||
headers.set("Accept", "text/plain");
|
||||
String result = new RestTemplate().postForObject(
|
||||
"http://localhost:6565/tests",
|
||||
new HttpEntity<MultiValueMap<String, Object>>(parameters, headers),
|
||||
String.class);
|
||||
|
||||
assertThat(result).isEqualTo("{\"status\":\"ok\"}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.fraud;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
public class TestController {
|
||||
|
||||
@PostMapping("/tests")
|
||||
public Test createNew(@RequestPart MultipartFile file1,
|
||||
@RequestPart MultipartFile file2,
|
||||
@RequestPart Test test) {
|
||||
return new Test("ok");
|
||||
}
|
||||
}
|
||||
|
||||
class Test {
|
||||
private String status;
|
||||
|
||||
public Test(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Test() {
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.fraud;
|
||||
|
||||
import com.example.fraud.TestController;
|
||||
import io.restassured.module.mockmvc.RestAssuredMockMvc;
|
||||
import org.junit.Before;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
public class MultipartBase {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
RestAssuredMockMvc.standaloneSetup(new TestController());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
Contract.make {
|
||||
request {
|
||||
method 'POST'
|
||||
url '/tests'
|
||||
multipart(
|
||||
[
|
||||
file1: named(
|
||||
name: value(consumer(regex(nonEmpty())), producer('filename1')),
|
||||
content: value(consumer(regex(nonEmpty())), producer('content1'))),
|
||||
file2: named(
|
||||
name: value(consumer(regex(nonEmpty())), producer('filename1')),
|
||||
content: value(c(regex(nonEmpty())), producer('content2'))),
|
||||
test : named(
|
||||
name: value(consumer(regex(nonEmpty())), producer('filename1')),
|
||||
content: value(c(regex(nonEmpty())), producer(file("test.json"))),
|
||||
contentType: "application/json")
|
||||
]
|
||||
)
|
||||
|
||||
headers {
|
||||
contentType('multipart/form-data')
|
||||
}
|
||||
}
|
||||
response {
|
||||
status 200
|
||||
body([
|
||||
status: 'ok'
|
||||
])
|
||||
headers {
|
||||
contentType('application/json')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"status": "test"
|
||||
}
|
||||
@@ -71,6 +71,10 @@ class Common {
|
||||
return new NamedProperty(name, value)
|
||||
}
|
||||
|
||||
NamedProperty named(DslProperty name, DslProperty value, DslProperty contentType) {
|
||||
return new NamedProperty(name, value, contentType)
|
||||
}
|
||||
|
||||
NamedProperty named(Map<String, DslProperty> namedMap){
|
||||
return new NamedProperty(namedMap)
|
||||
}
|
||||
|
||||
@@ -33,16 +33,37 @@ class NamedProperty {
|
||||
|
||||
private static final String NAME = 'name'
|
||||
private static final String CONTENT = 'content'
|
||||
private static final String CONTENT_TYPE = 'contentType'
|
||||
|
||||
DslProperty name
|
||||
DslProperty value
|
||||
DslProperty contentType
|
||||
|
||||
NamedProperty(DslProperty name, DslProperty value) {
|
||||
this.name = name
|
||||
this.value = value
|
||||
this.contentType = null
|
||||
}
|
||||
|
||||
NamedProperty(DslProperty name, DslProperty value, DslProperty contentType) {
|
||||
this.name = name
|
||||
this.value = value
|
||||
this.contentType = contentType
|
||||
}
|
||||
|
||||
NamedProperty(Map<String, DslProperty> namedMap) {
|
||||
this(namedMap?.get(NAME), namedMap?.get(CONTENT))
|
||||
this(asDslProperty(namedMap?.get(NAME)),
|
||||
asDslProperty(namedMap?.get(CONTENT)),
|
||||
asDslProperty(namedMap?.get(CONTENT_TYPE)))
|
||||
}
|
||||
|
||||
static DslProperty asDslProperty(Object o) {
|
||||
if (o == null) {
|
||||
return null
|
||||
}
|
||||
if (o instanceof DslProperty) {
|
||||
return o
|
||||
}
|
||||
return new DslProperty(o)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +142,18 @@ class RegexPatterns {
|
||||
return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"\r\n(Content-Type: .*\r\n)?(Content-Transfer-Encoding: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$value\r\n--\\1.*"
|
||||
}
|
||||
|
||||
static String multipartFile(Object name, Object filename, Object content) {
|
||||
return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n(Content-Type: .*\r\n)?(Content-Transfer-Encoding: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$content\r\n--\\1.*";
|
||||
static String multipartFile(Object name, Object filename, Object content, Object contentType) {
|
||||
return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n(Content-Type: ${toContentType(contentType)}\r\n)?(Content-Transfer-Encoding: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$content\r\n--\\1.*";
|
||||
}
|
||||
|
||||
private static String toContentType(Object contentType) {
|
||||
if (contentType == null) {
|
||||
return '.*'
|
||||
}
|
||||
if (contentType instanceof Pattern) {
|
||||
return contentType.pattern()
|
||||
}
|
||||
return contentType.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -125,10 +125,10 @@ class DslToWireMockClientConverterSpec extends Specification {
|
||||
wireMockRule.addStubMapping(mapping)
|
||||
and:
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>()
|
||||
parameters.add("file", new ByteArrayResource([100, 117, 100, 97] as byte[]) {
|
||||
parameters.add("test", new ByteArrayResource([100, 117, 100, 97] as byte[]) {
|
||||
@Override
|
||||
public String getFilename(){
|
||||
return "file"
|
||||
String getFilename(){
|
||||
return "test"
|
||||
}
|
||||
})
|
||||
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders()
|
||||
|
||||
@@ -136,7 +136,9 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
|
||||
if (request.multipart.clientValue instanceof Map) {
|
||||
List<StringValuePattern> multipartPatterns = (request.multipart.clientValue as Map).collect {
|
||||
(it.value instanceof NamedProperty
|
||||
? WireMock.matching(RegexPatterns.multipartFile(it.key, (it.value as NamedProperty).name.clientValue, (it.value as NamedProperty).value.clientValue))
|
||||
? WireMock.matching(RegexPatterns.multipartFile(it.key, (it.value as NamedProperty).name.clientValue,
|
||||
(it.value as NamedProperty).value.clientValue,
|
||||
(it.value as NamedProperty).contentType?.clientValue))
|
||||
: WireMock.matching(RegexPatterns.multipartParam(it.key, it.value)) )
|
||||
}
|
||||
multipartPatterns.each {
|
||||
|
||||
@@ -399,11 +399,13 @@ class ContentUtils {
|
||||
}
|
||||
|
||||
static String getGroovyMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) {
|
||||
return "'$propertyName', ${namedPropertyName(propertyValue, "'")}, ${groovyNamedPropertyValue(propertyValue, "'")}"
|
||||
return "'$propertyName', ${namedPropertyName(propertyValue, "'")}, " +
|
||||
"${groovyNamedPropertyValue(propertyValue, "'")}" + namedContentTypeNameIfPresent(propertyValue, "'")
|
||||
}
|
||||
|
||||
static String getJavaMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) {
|
||||
return """"${escapeJava(propertyName)}", ${namedPropertyName(propertyValue, '"')}, ${javaNamedPropertyValue(propertyValue, '"')}"""
|
||||
return """"${escapeJava(propertyName)}", ${namedPropertyName(propertyValue, '"')}, """ +
|
||||
"""${javaNamedPropertyValue(propertyValue, '"')}${namedContentTypeNameIfPresent(propertyValue, '"')}"""
|
||||
}
|
||||
|
||||
static String namedPropertyName(NamedProperty property, String quote) {
|
||||
@@ -411,6 +413,15 @@ class ContentUtils {
|
||||
property.name.serverValue.toString() : quote + escapeJava(property.name.serverValue.toString()) + quote
|
||||
}
|
||||
|
||||
static String namedContentTypeNameIfPresent(NamedProperty property, String quote) {
|
||||
if (!property.contentType) {
|
||||
return ""
|
||||
}
|
||||
String contentType = property.contentType.serverValue instanceof ExecutionProperty ?
|
||||
property.contentType.serverValue.toString() : quote + escapeJava(property.contentType.serverValue.toString()) + quote
|
||||
return ", " + contentType
|
||||
}
|
||||
|
||||
static String groovyNamedPropertyValue(NamedProperty property, String quote) {
|
||||
if (property.value.serverValue instanceof ExecutionProperty) {
|
||||
return property.value.serverValue.toString()
|
||||
|
||||
@@ -1295,7 +1295,9 @@ World.'''"""
|
||||
// name of the file
|
||||
name: $(c(regex(nonEmpty())), p('filename.csv')),
|
||||
// content of the file
|
||||
content: $(c(regex(nonEmpty())), p('file content')))
|
||||
content: $(c(regex(nonEmpty())), p('file content')),
|
||||
// content type for the part
|
||||
contentType: $(c(regex(nonEmpty())), p('application/json')))
|
||||
)
|
||||
}
|
||||
response {
|
||||
@@ -1316,6 +1318,56 @@ World.'''"""
|
||||
SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString())
|
||||
where:
|
||||
methodBuilderName | methodBuilder | requestStrings
|
||||
"MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ['"Content-Type", "multipart/form-data;boundary=AaB03x"',
|
||||
""".param('formParameter', '"formParameterValue"'""",
|
||||
""".param('someBooleanParameter', 'true')""",
|
||||
""".multiPart('file', 'filename.csv', 'file content'.bytes, 'application/json')"""]
|
||||
"MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | ['"Content-Type", "multipart/form-data;boundary=AaB03x"',
|
||||
'.param("formParameter", "\\"formParameterValue\\"")',
|
||||
'.param("someBooleanParameter", "true")',
|
||||
'.multiPart("file", "filename.csv", "file content".getBytes(), "application/json");']
|
||||
}
|
||||
|
||||
@Issue('180')
|
||||
def "should generate proper test code when having multipart parameters without content type with #methodBuilderName"() {
|
||||
given:
|
||||
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
|
||||
request {
|
||||
method "PUT"
|
||||
url "/multipart"
|
||||
headers {
|
||||
contentType('multipart/form-data;boundary=AaB03x')
|
||||
}
|
||||
multipart(
|
||||
// key (parameter name), value (parameter value) pair
|
||||
formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
|
||||
someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
|
||||
// a named parameter (e.g. with `file` name) that represents file with
|
||||
// `name` and `content`. You can also call `named("fileName", "fileContent")`
|
||||
file: named(
|
||||
// name of the file
|
||||
name: $(c(regex(nonEmpty())), p('filename.csv')),
|
||||
// content of the file
|
||||
content: $(c(regex(nonEmpty())), p('file content')))
|
||||
)
|
||||
}
|
||||
response {
|
||||
status OK()
|
||||
}
|
||||
}
|
||||
MethodBodyBuilder builder = methodBuilder(contractDsl)
|
||||
BlockBuilder blockBuilder = new BlockBuilder(" ")
|
||||
when:
|
||||
builder.appendTo(blockBuilder)
|
||||
def test = blockBuilder.toString()
|
||||
then:
|
||||
for (String requestString : requestStrings) {
|
||||
assert test.contains(requestString)
|
||||
}
|
||||
and:
|
||||
SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString())
|
||||
where:
|
||||
methodBuilderName | methodBuilder | requestStrings
|
||||
"MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | ['"Content-Type", "multipart/form-data;boundary=AaB03x"',
|
||||
""".param('formParameter', '"formParameterValue"'""",
|
||||
""".param('someBooleanParameter', 'true')""",
|
||||
|
||||
Reference in New Issue
Block a user