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:
Henry Jonathan
2018-06-08 17:25:37 +07:00
committed by Marcin Grzejszczak
parent 75c082a9c6
commit d72524173d
15 changed files with 261 additions and 9 deletions

1
.gitignore vendored
View File

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

View File

@@ -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\"}");
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
{
"status": "test"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')""",