Contract for multipart/mixed endpoint backport to 1.2.x (#662)
* apply cherry pick based on b965c49a7e and fix conflict
This commit is contained in:
committed by
Marcin Grzejszczak
parent
75c082a9c6
commit
d72524173d
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ bin/
|
||||
# Package Files #
|
||||
*.war
|
||||
*.ear
|
||||
*.versionsBackup
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
@@ -6,6 +6,10 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
|
||||
@@ -13,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;
|
||||
|
||||
@@ -79,4 +86,42 @@ public class LoanApplicationServiceTests {
|
||||
assertThat(cookies).isEqualTo("foo bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyWorkWithMultipart() {
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
|
||||
parameters.add("file1", new ByteArrayResource(("content1").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "filename1";
|
||||
}
|
||||
});
|
||||
parameters.add("file2", new ByteArrayResource(("content2").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "filename2";
|
||||
}
|
||||
});
|
||||
|
||||
HttpHeaders jsonHeader = new HttpHeaders();
|
||||
jsonHeader.set("Content-Type", "application/json");
|
||||
parameters.add("test", new HttpEntity<>(new ByteArrayResource(("{\n \"status\": \"test\"\n}").getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "filename3";
|
||||
}
|
||||
}, jsonHeader));
|
||||
|
||||
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,33 @@
|
||||
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('filename2')),
|
||||
content: value(c(regex(nonEmpty())), producer('content2'))),
|
||||
test : named(
|
||||
name: value(consumer(regex(nonEmpty())), producer('filename3')),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import java.util.regex.Pattern
|
||||
/**
|
||||
* Contains most common regular expression patterns
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@@ -114,8 +116,18 @@ class RegexPatterns {
|
||||
return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"\r\n(Content-Type: .*\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-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-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>
|
||||
|
||||
@@ -28,7 +28,11 @@ import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubMapp
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.verifier.file.ContractMetadata
|
||||
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.RequestEntity
|
||||
import org.springframework.util.LinkedMultiValueMap
|
||||
import org.springframework.util.MultiValueMap
|
||||
import org.springframework.util.SocketUtils
|
||||
import spock.lang.Issue
|
||||
import spock.lang.Specification
|
||||
|
||||
@@ -208,10 +208,10 @@
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
<sourceDirectories>
|
||||
${project.basedir}/src/main/java,${project.basedir}/src/main/groovy
|
||||
<sourceDirectory>${project.basedir}/src/main/java,${project.basedir}/src/main/groovy</sourceDirectory>
|
||||
</sourceDirectories>
|
||||
<testSourceDirectories>
|
||||
${project.basedir}/src/test/java,${project.basedir}/src/test/groovy
|
||||
<sourceDirectory>${project.basedir}/src/test/java,${project.basedir}/src/test/groovy</sourceDirectory>
|
||||
</testSourceDirectories>
|
||||
</configuration>
|
||||
<goals>
|
||||
|
||||
@@ -132,7 +132,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 {
|
||||
|
||||
@@ -388,11 +388,13 @@ class ContentUtils {
|
||||
}
|
||||
|
||||
static String getGroovyMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) {
|
||||
return "'$propertyName', ${namedPropertyName(propertyValue, "'")}, ${namedPropertyValue(propertyValue, "'")}.bytes"
|
||||
return "'$propertyName', ${namedPropertyName(propertyValue, "'")}, " +
|
||||
"${namedPropertyValue(propertyValue, "'")}.bytes" + namedContentTypeNameIfPresent(propertyValue, "'")
|
||||
}
|
||||
|
||||
static String getJavaMultipartFileParameterContent(String propertyName, NamedProperty propertyValue) {
|
||||
return """"${escapeJava(propertyName)}", ${namedPropertyName(propertyValue, '"')}, ${namedPropertyValue(propertyValue, '"')}.getBytes()"""
|
||||
return """"${escapeJava(propertyName)}", ${namedPropertyName(propertyValue, '"')}, """ +
|
||||
"""${javaNamedPropertyValue(propertyValue, '"')}${namedContentTypeNameIfPresent(propertyValue, '"')}"""
|
||||
}
|
||||
|
||||
static String namedPropertyName(NamedProperty property, String quote) {
|
||||
@@ -400,9 +402,27 @@ 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 namedPropertyValue(NamedProperty property, String quote) {
|
||||
return property.value.serverValue instanceof ExecutionProperty ?
|
||||
property.value.serverValue.toString() : quote + escapeJava(property.value.serverValue.toString()) + quote
|
||||
}
|
||||
|
||||
static String javaNamedPropertyValue(NamedProperty property, String quote) {
|
||||
if (property.value.serverValue instanceof ExecutionProperty) {
|
||||
return property.value.serverValue.toString()
|
||||
} else if (property.value.serverValue instanceof byte[]) {
|
||||
byte[] bytes = (byte[]) property.value.serverValue
|
||||
return "new byte[] {" + bytes.collect { it }.join(", ") + "}"
|
||||
}
|
||||
return quote + escapeJava(property.value.serverValue.toString()) + quote + ".getBytes()"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,7 +1249,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 {
|
||||
@@ -1270,6 +1272,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 200
|
||||
}
|
||||
}
|
||||
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