Skip to content

Commit

Permalink
fix: handle base64 encoded bodies in pact files #1110
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed Jun 14, 2020
1 parent f67ecbb commit dde272b
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,21 @@ data class ContentType(val contentType: MediaType?) {

fun isBinaryType(): Boolean {
return if (contentType != null) {
val baseType = registry.getSupertype(contentType)
val type = baseType.type
val superType = registry.getSupertype(contentType)
val type = contentType.type
val baseType = superType.type
when {
isOctetStream() -> true
type == "image" -> true
type == "audio" -> true
type == "video" -> true
baseType == MediaType.APPLICATION_ZIP -> true
baseType.subtype == "pdf" -> true
type == "text" || baseType == "text" -> false
type == "image" || baseType == "image" -> true
type == "audio" || baseType == "audio" -> true
type == "video" || baseType == "video" -> true
type == "application" && superType.subtype == "pdf" -> true
type == "application" && superType.subtype == "xml" -> false
type == "application" && superType.subtype == "json" -> false
type == "application" && superType.subtype == "javascript" -> false
type == "application" && contentType.subtype.matches(JSON_TYPE) -> false
superType == MediaType.APPLICATION_ZIP -> true
superType == MediaType.OCTET_STREAM -> true
else -> false
}
} else false
Expand All @@ -79,6 +85,8 @@ data class ContentType(val contentType: MediaType?) {
val JSONREGEXP = """^\s*(true|false|null|[0-9]+|"\w*|\{\s*(}|"\w+)|\[\s*).*""".toRegex()
val XMLREGEXP2 = """^\s*<\w+\s*(:\w+=[\"”][^\"”]+[\"”])?.*""".toRegex()

val JSON_TYPE = ".*json".toRegex(setOf(RegexOption.IGNORE_CASE))

val registry: MediaTypeRegistry = MediaTypeRegistry.getDefaultRegistry()

@JvmStatic
Expand Down
23 changes: 23 additions & 0 deletions core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package au.com.dius.pact.core.model

import au.com.dius.pact.core.model.matchingrules.MatchingRules
import au.com.dius.pact.core.support.isNotEmpty
import au.com.dius.pact.core.support.json.JsonValue
import mu.KLogging
import java.nio.charset.Charset
import java.util.Base64

/**
* Base trait for an object that represents part of an http message
Expand Down Expand Up @@ -58,5 +60,26 @@ abstract class HttpPart {

companion object : KLogging() {
private const val CONTENT_TYPE = "Content-Type"

@JvmStatic
fun extractBody(json: JsonValue.Object, contentType: ContentType): OptionalBody {
return when (val b = json["body"]) {
is JsonValue.Null -> OptionalBody.nullBody()
is JsonValue.StringValue -> decodeBody(b.value, contentType)
else -> decodeBody(b.serialise(), contentType)
}
}

private fun decodeBody(body: String, contentType: ContentType): OptionalBody {
return when {
contentType.isBinaryType() || contentType.isMultipart() -> try {
OptionalBody.body(Base64.getDecoder().decode(body), contentType)
} catch (ex: IllegalArgumentException) {
logger.warn(ex) { "Expected body for content type $contentType to be base64 encoded" }
OptionalBody.body(body.toByteArray(contentType.asCharset()), contentType)
}
else -> OptionalBody.body(body.toByteArray(contentType.asCharset()), contentType)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,7 @@ class Request @JvmOverloads constructor(
}

val body = if (json.has("body")) {
when (val b = json["body"]) {
is JsonValue.Null -> OptionalBody.nullBody()
is JsonValue.StringValue -> OptionalBody.body(b.value.toByteArray(contentType.asCharset()), contentType)
else -> OptionalBody.body(b.serialise().toByteArray(contentType.asCharset()), contentType)
}
extractBody(json, contentType)
} else OptionalBody.missing()
val matchingRules = if (json.has("matchingRules") && json["matchingRules"] is JsonValue.Object)
MatchingRulesImpl.fromJson(json["matchingRules"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,7 @@ class Response @JvmOverloads constructor(
}

val body = if (json.has("body")) {
when (val b = json["body"]) {
is JsonValue.Null -> OptionalBody.nullBody()
is JsonValue.StringValue -> OptionalBody.body(b.value.toByteArray(contentType.asCharset()), contentType)
else -> OptionalBody.body(json["body"].serialise().toByteArray(contentType.asCharset()), contentType)
}
extractBody(json, contentType)
} else OptionalBody.missing()
val matchingRules = if (json.has("matchingRules") && json["matchingRules"] is JsonValue.Object)
MatchingRulesImpl.fromJson(json["matchingRules"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import au.com.dius.pact.core.model.generators.Generators
import au.com.dius.pact.core.model.matchingrules.MatchingRules
import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl
import au.com.dius.pact.core.support.Json
import au.com.dius.pact.core.support.isNotEmpty
import au.com.dius.pact.core.support.json.JsonException
import au.com.dius.pact.core.support.json.JsonParser
import au.com.dius.pact.core.support.json.JsonValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,29 @@ class ContentTypeSpec extends Specification {
contentType = new ContentType(value)
}

@Unroll
def '"#value" is binary -> #result'() {
expect:
contentType.binaryType == result

where:

value || result
'' || false
'text/plain' || false
'application/pdf' || true
'application/zip' || true
'application/json' || false
'application/hal+json' || false
'application/HAL+JSON' || false
'application/xml' || false
'application/atom+xml' || false
'image/jpeg' || true
'video/H264' || true
'audio/aac' || true
'text/csv' || false
'multipart/form-data' || true

contentType = new ContentType(value)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package au.com.dius.pact.core.model

import au.com.dius.pact.core.support.json.JsonValue
import spock.lang.Specification
import spock.lang.Unroll

Expand All @@ -11,7 +12,7 @@ class HttpPartSpec extends Specification {
@Unroll
def 'Pact contentType'() {
expect:
request.contentType() == contentType
request.determineContentType().asString() == contentType

where:
request | contentType
Expand Down Expand Up @@ -42,4 +43,22 @@ class HttpPartSpec extends Specification {
new Request('Get', '', [:], ['Content-Type': ['text/html']]) | Charset.defaultCharset()
new Request('Get', '', [:], ['Content-Type': ['application/json; charset=UTF-16']]) | Charset.forName('UTF-16')
}

def 'handles base64 encoded bodies'() {
given:
def json = new JsonValue.Object([body: new JsonValue.StringValue('aGVsbG8=')])

expect:
HttpPart.extractBody(json, ContentType.fromString('application/zip'))
.valueAsString() == 'hello'
}

def 'returns the raw body if it can not be decoded'() {
given:
def json = new JsonValue.Object([body: new JsonValue.StringValue('hello')])

expect:
HttpPart.extractBody(json, ContentType.fromString('application/zip'))
.valueAsString() == 'hello'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,18 @@ class PactReaderSpec extends Specification {
pact.interactions[0].request.matchingRules == matchingRules
}

@Issue('#1110')
@SuppressWarnings('LineLength')
def 'handle multipart form post bodies'() {
given:
def pactUrl = PactReaderSpec.classLoader.getResource('pact-multipart-form-post.json')

when:
def pact = DefaultPactReader.INSTANCE.loadPact(pactUrl)

then:
pact instanceof RequestResponsePact
pact.interactions[0].request.determineContentType().baseType == 'multipart/form-data'
pact.interactions[0].request.body.valueAsString().startsWith('--lk9eSoRxJdPHMNbDpbvOYepMB0gWDyQPWo\r\nContent-Disposition: form-data; name="photo"; filename="ron.jpg"\r\nContent-Type: image/jpeg')
}
}
85 changes: 85 additions & 0 deletions core/model/src/test/resources/pact-multipart-form-post.json

Large diffs are not rendered by default.

0 comments on commit dde272b

Please sign in to comment.