From fc7e13e87373d4b9865e32378c8c5be70cb21922 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 25 Oct 2022 18:21:14 +1100 Subject: [PATCH] fix: write empty bodies to the Pact file #1611 --- .../consumer/dsl/PactDslResponseSpec.groovy | 25 ++++++++++ .../com/dius/pact/core/model/OptionalBody.kt | 47 ++++++++++--------- .../core/model/RequestResponseInteraction.kt | 10 ++++ .../com/dius/pact/core/model/V4HttpParts.kt | 4 +- .../pact/core/model/HttpRequestSpec.groovy | 13 +++++ .../pact/core/model/HttpResponseSpec.groovy | 12 +++++ .../pact/core/model/OptionalBodySpec.groovy | 2 +- .../RequestResponseInteractionSpec.groovy | 9 ++++ .../dius/pact/core/model/V4PactKtSpec.groovy | 21 +++++++++ 9 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 core/model/src/test/groovy/au/com/dius/pact/core/model/HttpRequestSpec.groovy create mode 100644 core/model/src/test/groovy/au/com/dius/pact/core/model/HttpResponseSpec.groovy create mode 100644 core/model/src/test/groovy/au/com/dius/pact/core/model/V4PactKtSpec.groovy diff --git a/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslResponseSpec.groovy b/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslResponseSpec.groovy index 06d8681072..23d5e9f118 100644 --- a/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslResponseSpec.groovy +++ b/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslResponseSpec.groovy @@ -2,6 +2,8 @@ package au.com.dius.pact.consumer.dsl import au.com.dius.pact.consumer.ConsumerPactBuilder import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.PactSpecVersion +import au.com.dius.pact.core.model.V4Pact import au.com.dius.pact.core.model.generators.Generators import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl @@ -162,4 +164,27 @@ class PactDslResponseSpec extends Specification { !['pactSpecification', 'pact-jvm', 'plugins'].contains(it.key) } == [test: new JsonValue.StringValue('value')] } + + @Issue('#1611') + def 'supports empty bodies'() { + given: + def builder = ConsumerPactBuilder.consumer('empty-body-consumer') + .hasPactWith('empty-body-service') + .uponReceiving('a request for an empty body') + .path('/path') + .willRespondWith() + .body("") + + when: + def pact = builder.toPact() + def interaction = pact.interactions.first() + def pactV4 = builder.toPact(V4Pact) + def v4Interaction = pactV4.interactions.first() + + then: + interaction.response.body.state == OptionalBody.State.EMPTY + interaction.toMap(PactSpecVersion.V3).response == [status: 200, body: ''] + v4Interaction.response.body.state == OptionalBody.State.EMPTY + v4Interaction.toMap(PactSpecVersion.V4).response == [status: 200, body: [content: '']] + } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt index 8fdc4bd005..eca9ca8931 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt @@ -179,31 +179,34 @@ data class OptionalBody @JvmOverloads constructor( fun toV4Format(): Map { return when (state) { - State.PRESENT -> if (value!!.isNotEmpty()) { - if (contentTypeHint == ContentTypeHint.BINARY || contentType.isBinaryType()) { - mapOf( - "content" to valueAsBase64(), - "contentType" to contentType.toString(), - "encoded" to "base64", - "contentTypeHint" to contentTypeHint.name - ) - } else if (contentType.isJson()) { - mapOf( - "content" to JsonParser.parseString(valueAsString()), - "contentType" to contentType.toString(), - "encoded" to false - ) + State.PRESENT -> { + if (value!!.isNotEmpty()) { + if (contentTypeHint == ContentTypeHint.BINARY || contentType.isBinaryType()) { + mapOf( + "content" to valueAsBase64(), + "contentType" to contentType.toString(), + "encoded" to "base64", + "contentTypeHint" to contentTypeHint.name + ) + } else if (contentType.isJson()) { + mapOf( + "content" to JsonParser.parseString(valueAsString()), + "contentType" to contentType.toString(), + "encoded" to false + ) + } else { + mapOf( + "content" to valueAsString(), + "contentType" to contentType.toString(), + "encoded" to false, + "contentTypeHint" to contentTypeHint.name + ) + } } else { - mapOf( - "content" to valueAsString(), - "contentType" to contentType.toString(), - "encoded" to false, - "contentTypeHint" to contentTypeHint.name - ) + mapOf("content" to "") } - } else { - mapOf("content" to "") } + State.EMPTY -> mapOf("content" to "") else -> mapOf() } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt index 682adeab26..d25fde4c7d 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt @@ -78,6 +78,7 @@ open class RequestResponseInteraction @JvmOverloads constructor( companion object : KLogging() { const val COMMA = ", " + @JvmStatic fun requestToMap(request: Request, pactSpecVersion: PactSpecVersion): Map { val map = mutableMapOf( "method" to request.method.toUpperCase(), @@ -89,9 +90,13 @@ open class RequestResponseInteraction @JvmOverloads constructor( if (request.query.isNotEmpty()) { map["query"] = if (pactSpecVersion >= PactSpecVersion.V3) request.query else mapToQueryStr(request.query) } + if (request.body.isPresent()) { map["body"] = setupBodyForJson(request) + } else if (request.body.isEmpty()) { + map["body"] = "" } + if (request.matchingRules.isNotEmpty()) { map["matchingRules"] = request.matchingRules.toMap(pactSpecVersion) } @@ -102,14 +107,19 @@ open class RequestResponseInteraction @JvmOverloads constructor( return map } + @JvmStatic fun responseToMap(response: Response, pactSpecVersion: PactSpecVersion): Map { val map = mutableMapOf("status" to response.status) if (response.headers.isNotEmpty()) { map["headers"] = response.headers.entries.associate { (key, value) -> key to value.joinToString(COMMA) } } + if (response.body.isPresent()) { map["body"] = setupBodyForJson(response) + } else if (response.body.isEmpty()) { + map["body"] = "" } + if (response.matchingRules.isNotEmpty()) { map["matchingRules"] = response.matchingRules.toMap(pactSpecVersion) } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4HttpParts.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4HttpParts.kt index c05ddcb9e8..edee1597e1 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4HttpParts.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4HttpParts.kt @@ -56,7 +56,7 @@ data class HttpRequest @JvmOverloads constructor( if (query.isNotEmpty()) { map["query"] = query } - if (body.isPresent()) { + if (body.isPresent() || body.isEmpty()) { map["body"] = body.toV4Format() } if (matchingRules.isNotEmpty()) { @@ -152,7 +152,7 @@ data class HttpResponse @JvmOverloads constructor( if (headers.isNotEmpty()) { map["headers"] = headers } - if (body.isPresent()) { + if (body.isPresent() || body.isEmpty()) { map["body"] = body.toV4Format() } if (matchingRules.isNotEmpty()) { diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpRequestSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpRequestSpec.groovy new file mode 100644 index 0000000000..92e32b02cc --- /dev/null +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpRequestSpec.groovy @@ -0,0 +1,13 @@ +package au.com.dius.pact.core.model + +import spock.lang.Issue +import spock.lang.Specification + +class HttpRequestSpec extends Specification { + @Issue('#1611') + def 'supports empty bodies'() { + expect: + new HttpRequest('GET', '/', [:], [:], OptionalBody.empty()).toMap() == + [method: 'GET', path: '/', body: [content: '']] + } +} diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpResponseSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpResponseSpec.groovy new file mode 100644 index 0000000000..812a18efc8 --- /dev/null +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpResponseSpec.groovy @@ -0,0 +1,12 @@ +package au.com.dius.pact.core.model + +import spock.lang.Issue +import spock.lang.Specification + +class HttpResponseSpec extends Specification { + @Issue('#1611') + def 'supports empty bodies'() { + expect: + new HttpResponse(200, [:], OptionalBody.empty()).toMap() == [status: 200, body: [content: '']] + } +} diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy index 3508e04368..f9a9164d25 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy @@ -162,7 +162,7 @@ class OptionalBodySpec extends Specification { where: body | v4Format OptionalBody.missing() | [:] - OptionalBody.body(''.bytes, ContentType.UNKNOWN) | [:] + OptionalBody.body(''.bytes, ContentType.UNKNOWN) | [content: ''] OptionalBody.body('{}'.bytes, ContentType.UNKNOWN) | [content: new JsonValue.Object(), contentType: 'application/json', encoded: false] OptionalBody.body('{}'.bytes, ContentType.JSON) | [content: new JsonValue.Object(), contentType: 'application/json', encoded: false] OptionalBody.body([0xff, 0xd8, 0xff, 0xe0] as byte[], new ContentType('image/jpeg')) | [content: '/9j/4A==', contentType: 'image/jpeg', encoded: 'base64', contentTypeHint: 'DEFAULT'] diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/RequestResponseInteractionSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/RequestResponseInteractionSpec.groovy index 693d05e400..7db0f1128f 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/RequestResponseInteractionSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/RequestResponseInteractionSpec.groovy @@ -115,4 +115,13 @@ class RequestResponseInteractionSpec extends Specification { 'include[]=needs_grading_count&include[]=permissions&include[]=current_grading_period_scores&' + 'include[]=course_image&include[]=favorites' } + + @Issue('#1611') + def 'supports empty bodies'() { + expect: + RequestResponseInteraction.requestToMap(new Request(body: OptionalBody.empty()), PactSpecVersion.V3) == + [method: 'GET', path: '/', body: ''] + RequestResponseInteraction.responseToMap(new Response(body: OptionalBody.empty()), PactSpecVersion.V3) == + [status: 200, body: ''] + } } diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/V4PactKtSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/V4PactKtSpec.groovy new file mode 100644 index 0000000000..1c0273b1e1 --- /dev/null +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/V4PactKtSpec.groovy @@ -0,0 +1,21 @@ +package au.com.dius.pact.core.model + +import au.com.dius.pact.core.support.json.JsonValue +import spock.lang.Specification + +import static au.com.dius.pact.core.model.V4PactKt.bodyFromJson + +class V4PactKtSpec extends Specification { + def 'bodyFromJson - when body is empty in the Pact file'() { + expect: + bodyFromJson('body', new JsonValue.Object(json), [:]) == body + + where: + + json | body + [:] | OptionalBody.missing() + [body: JsonValue.Null.INSTANCE] | OptionalBody.nullBody() + [body: new JsonValue.StringValue('')] | OptionalBody.empty() + [body: new JsonValue.Object([content: new JsonValue.StringValue('')])] | OptionalBody.empty() + } +}