diff --git a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java index 518150bc93..e9e83b9ad8 100644 --- a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java +++ b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java @@ -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 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>(parameters, headers), + String.class); + + assertThat(result).isEqualTo("{\"status\":\"ok\"}"); + } + } diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java new file mode 100644 index 0000000000..4ca3826ef2 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/TestController.java @@ -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; + } +} \ No newline at end of file diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MultipartBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MultipartBase.java new file mode 100644 index 0000000000..90a87c0636 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MultipartBase.java @@ -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()); + } +} \ No newline at end of file diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/shouldWorkForMultipartWithContentType.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/shouldWorkForMultipartWithContentType.groovy new file mode 100644 index 0000000000..37d9271fd7 --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/shouldWorkForMultipartWithContentType.groovy @@ -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') + } + } +} diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/test.json b/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/test.json new file mode 100644 index 0000000000..21a721f25a --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/multipart/test.json @@ -0,0 +1,3 @@ +{ + "status": "test" +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy index 416fbfa367..7484ec2837 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy @@ -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 namedMap){ return new NamedProperty(namedMap) } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy index 2907acf2f5..6ffea29205 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy @@ -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 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) } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy index f0292ceafe..8d5fd078f1 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy @@ -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() } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml index d46fbd95d0..fc30b3a88e 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml @@ -66,6 +66,11 @@ spring-boot-starter-web test + + io.rest-assured + rest-assured + test + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy index 3345834f38..47d94a4fb5 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -125,10 +125,10 @@ class DslToWireMockClientConverterSpec extends Specification { wireMockRule.addStubMapping(mapping) and: MultiValueMap parameters = new LinkedMultiValueMap() - 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() diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index c5a15dfa2c..8c2130678b 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -136,7 +136,9 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { if (request.multipart.clientValue instanceof Map) { List 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 { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index 3f28c382db..d1665a0582 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -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() diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index 2523390afc..214d544f0b 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -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')""",