Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented matching of multipart uploads #123
- Loading branch information
Ronald Holshausen
committed
Nov 29, 2017
1 parent
1fe85e4
commit 5180041
Showing
7 changed files
with
279 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
...umer-groovy/src/test/groovy/au/com/dius/pact/consumer/groovy/ExampleFileUploadSpec.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package au.com.dius.pact.consumer.groovy | ||
|
||
import au.com.dius.pact.consumer.MockServer | ||
import au.com.dius.pact.consumer.PactVerificationResult | ||
import org.apache.http.client.methods.RequestBuilder | ||
import org.apache.http.entity.ContentType | ||
import org.apache.http.entity.mime.HttpMultipartMode | ||
import org.apache.http.entity.mime.MultipartEntityBuilder | ||
import org.apache.http.impl.client.CloseableHttpClient | ||
import org.apache.http.impl.client.HttpClients | ||
import spock.lang.Specification | ||
|
||
class ExampleFileUploadSpec extends Specification { | ||
|
||
def 'handles bodies from form posts'() { | ||
given: | ||
def service = new PactBuilder() | ||
service { | ||
serviceConsumer 'Consumer' | ||
hasPactWith 'File Service' | ||
uponReceiving('a multipart file POST') | ||
withAttributes(path: '/upload', method: 'post') | ||
withFileUpload('file', 'data.csv', 'text/csv', '1,2,3,4\n5,6,7,8'.bytes) | ||
willRespondWith(status: 201, body: 'file uploaded ok', headers: ['Content-Type': 'text/plain']) | ||
} | ||
|
||
when: | ||
def result = service.runTest { MockServer mockServer -> | ||
CloseableHttpClient httpclient = HttpClients.createDefault() | ||
httpclient.withCloseable { | ||
def data = MultipartEntityBuilder.create() | ||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE) | ||
.addBinaryBody('file', '1,2,3,4\n5,6,7,8'.bytes, ContentType.create('text/csv'), 'data.csv') | ||
.build() | ||
def request = RequestBuilder | ||
.post(mockServer.url + '/upload') | ||
.setEntity(data) | ||
.build() | ||
println('Executing request ' + request.requestLine) | ||
httpclient.execute(request) | ||
} | ||
} | ||
|
||
then: | ||
result == PactVerificationResult.Ok.INSTANCE | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
pact-jvm-matchers/src/main/kotlin/au/com/dius/pact/matchers/MultipartFormBodyMatcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package au.com.dius.pact.matchers | ||
|
||
import au.com.dius.pact.model.HttpPart | ||
import au.com.dius.pact.model.isEmpty | ||
import au.com.dius.pact.model.isMissing | ||
import au.com.dius.pact.model.isNotPresent | ||
import au.com.dius.pact.model.isPresent | ||
import au.com.dius.pact.model.orElse | ||
import java.util.Enumeration | ||
import javax.mail.BodyPart | ||
import javax.mail.Header | ||
import javax.mail.internet.MimeMultipart | ||
import javax.mail.util.ByteArrayDataSource | ||
|
||
class MultipartFormBodyMatcher : BodyMatcher { | ||
|
||
override fun matchBody(expected: HttpPart, actual: HttpPart, allowUnexpectedKeys: Boolean): List<BodyMismatch> { | ||
val expectedBody = expected.body | ||
val actualBody = actual.body | ||
return when { | ||
expectedBody.isMissing() -> emptyList() | ||
expectedBody.isPresent() && actualBody.isNotPresent() -> listOf(BodyMismatch(expectedBody.orElse(""), | ||
null, "Expected a multipart body but was missing")) | ||
expectedBody.isEmpty() && actualBody.isEmpty() -> emptyList() | ||
else -> { | ||
val expectedMultipart = parseMultipart(expectedBody.orElse(""), expected.contentTypeHeader().orEmpty()) | ||
val actualMultipart = parseMultipart(actualBody.orElse(""), actual.contentTypeHeader().orEmpty()) | ||
compareHeaders(expectedMultipart, actualMultipart) + compareContents(expectedMultipart, actualMultipart) | ||
} | ||
} | ||
} | ||
|
||
private fun compareContents(expectedMultipart: BodyPart, actualMultipart: BodyPart): List<BodyMismatch> { | ||
val expectedContents = expectedMultipart.content.toString().trim() | ||
val actualContents = actualMultipart.content.toString().trim() | ||
return when { | ||
expectedContents.isEmpty() && actualContents.isEmpty() -> emptyList() | ||
expectedContents.isNotEmpty() && actualContents.isNotEmpty() -> emptyList() | ||
expectedContents.isEmpty() && actualContents.isNotEmpty() -> listOf(BodyMismatch(expectedContents, | ||
actualContents, "Expected no contents, but received ${actualContents.toByteArray().size} bytes of content")) | ||
else -> listOf(BodyMismatch(expectedContents, | ||
actualContents, "Expected content with the multipart, but received no bytes of content")) | ||
} | ||
} | ||
|
||
private fun compareHeaders(expectedMultipart: BodyPart, actualMultipart: BodyPart): List<BodyMismatch> { | ||
val mismatches = mutableListOf<BodyMismatch>() | ||
(expectedMultipart.allHeaders as Enumeration<Header>).asSequence().forEach { | ||
val header = actualMultipart.getHeader(it.name) | ||
if (header != null) { | ||
val actualValue = header.joinToString(separator = ", ") | ||
if (actualValue != it.value) { | ||
mismatches.add(BodyMismatch(it.toString(), null, | ||
"Expected a multipart header '${it.name}' with value '${it.value}', but was '$actualValue'")) | ||
} | ||
} else { | ||
mismatches.add(BodyMismatch(it.toString(), null, "Expected a multipart header '${it.name}', but was missing")) | ||
} | ||
} | ||
|
||
return mismatches | ||
} | ||
|
||
private fun parseMultipart(body: String, contentType: String): BodyPart { | ||
val multipart = MimeMultipart(ByteArrayDataSource(body, contentType)) | ||
return multipart.getBodyPart(0) | ||
} | ||
|
||
} |
103 changes: 103 additions & 0 deletions
103
...vm-matchers/src/test/groovy/au/com/dius/pact/matchers/MultipartFormBodyMatcherSpec.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package au.com.dius.pact.matchers | ||
|
||
import au.com.dius.pact.model.OptionalBody | ||
import au.com.dius.pact.model.Request | ||
import spock.lang.Specification | ||
|
||
class MultipartFormBodyMatcherSpec extends Specification { | ||
|
||
private MultipartFormBodyMatcher matcher | ||
private expected, actual | ||
|
||
def setup() { | ||
matcher = new MultipartFormBodyMatcher() | ||
expected = { body -> new Request('', '', null, ['Content-Type': 'multipart/form-data; boundary=XXX'], body) } | ||
actual = { body -> new Request('', '', null, ['Content-Type': 'multipart/form-data; boundary=XXX'], body) } | ||
} | ||
|
||
def 'return no mismatches - when comparing empty bodies'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true).empty | ||
|
||
where: | ||
|
||
actualBody = OptionalBody.empty() | ||
expectedBody = OptionalBody.empty() | ||
} | ||
|
||
def 'return no mismatches - when comparing a missing body to anything'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true).empty | ||
|
||
where: | ||
|
||
actualBody = OptionalBody.body('"Blah"') | ||
expectedBody = OptionalBody.missing() | ||
} | ||
|
||
def 'returns a mismatch - when comparing anything to an empty body'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true)*.mismatch == [ | ||
'Expected a multipart body but was missing' | ||
] | ||
|
||
where: | ||
|
||
actualBody = OptionalBody.body('') | ||
expectedBody = OptionalBody.body('"Blah"') | ||
} | ||
|
||
def 'returns a mismatch - when the actual body is missing a header'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true)*.mismatch == [ | ||
'Expected a multipart header \'Test\', but was missing' | ||
] | ||
|
||
where: | ||
|
||
actualBody = multipart('form-data', 'file', '476.csv', 'text/plain', '', '1234') | ||
expectedBody = multipart('form-data', 'file', '476.csv', 'text/plain', 'Test: true\n', '1234') | ||
} | ||
|
||
def 'returns a mismatch - when the headers do not match'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true)*.mismatch == [ | ||
'Expected a multipart header \'Content-Type\' with value \'text/html\', but was \'text/plain\'' | ||
] | ||
|
||
where: | ||
|
||
actualBody = multipart('form-data', 'file', '476.csv', 'text/plain', 'Test: true\n', '1234') | ||
expectedBody = multipart('form-data', 'file', '476.csv', 'text/html', 'Test: true\n', '1234') | ||
} | ||
|
||
def 'returns a mismatch - when the actual body is empty'() { | ||
expect: | ||
matcher.matchBody(expected(expectedBody), actual(actualBody), true)*.mismatch == [ | ||
'Expected content with the multipart, but received no bytes of content' | ||
] | ||
|
||
where: | ||
|
||
actualBody = multipart('form-data', 'file', '476.csv', 'text/plain', '', | ||
'') | ||
expectedBody = multipart('form-data', 'file', '476.csv', 'text/plain', '', | ||
'1234') | ||
} | ||
|
||
@SuppressWarnings('ParameterCount') | ||
OptionalBody multipart(disposition, name, filename, contentType, headers, body) { | ||
OptionalBody.body( | ||
|
||
"""--XXX | ||
|Content-Disposition: $disposition; name=\"$name\"; filename=\"$filename\" | ||
|Content-Type: $contentType | ||
|$headers | ||
| | ||
|$body | ||
|--XXX | ||
""".stripMargin() | ||
) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters