Skip to content

Commit

Permalink
feat(compatibility-suite): Implemented scenarios related to non-JSON …
Browse files Browse the repository at this point in the history
…bodies
  • Loading branch information
rholshausen committed Jun 30, 2023
1 parent ae0a7a4 commit af661f3
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
implementation 'io.netty:netty-handler:4.1.84.Final'
implementation 'org.apache.groovy:groovy:4.0.11'
implementation 'org.apache.groovy:groovy-json:4.0.11'
implementation 'org.apache.groovy:groovy-xml:4.0.11'
implementation 'io.pact.plugin.driver:core:0.3.2'

testImplementation 'org.apache.groovy:groovy:4.0.11'
Expand Down
1 change: 1 addition & 0 deletions compatibility-suite/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
testImplementation 'io.cucumber:cucumber-picocontainer:7.12.0'
testImplementation 'org.apache.groovy:groovy'
testImplementation 'org.apache.groovy:groovy-json'
testImplementation 'org.apache.groovy:groovy-xml'
testImplementation project(':core:model')
testImplementation project(':core:matchers')
testImplementation project(':consumer')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import au.com.dius.pact.consumer.BaseMockServer
import au.com.dius.pact.consumer.PactVerificationResult
import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.core.model.Consumer
import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.HeaderParser
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.Provider
import au.com.dius.pact.core.model.RequestResponsePact
import au.com.dius.pact.provider.HttpClientFactory
Expand All @@ -24,6 +22,7 @@ import org.apache.hc.core5.http.HttpRequest
import static au.com.dius.pact.consumer.MockHttpServerKt.mockServer
import static au.com.dius.pact.core.model.PactReaderKt.queryStringToMap
import static io.ktor.http.HttpHeaderValueParserKt.parseHeaderValue
import static steps.shared.SharedSteps.configureBody

class MockServerData {
RequestResponsePact pact
Expand Down Expand Up @@ -97,26 +96,7 @@ class MockServerSharedSteps {
}

if (entry['body']) {
println(entry['body'].inspect())
if (entry['body'].startsWith('JSON:')) {
request.headers['content-type'] = ['application/json']
request.body = OptionalBody.body(entry['body'][5..-1].bytes, new ContentType('application/json'))
} else if (entry['body'].startsWith('XML:')) {
request.headers['content-type'] = ['application/xml']
request.body = OptionalBody.body(entry['body'][4..-1].bytes, new ContentType('application/xml'))
} else {
String contentType = 'text/plain'
if (entry['body'].endsWith('.json')) {
contentType = 'application/json'
} else if (entry['body'].endsWith('.xml')) {
contentType = 'application/xml'
}
request.headers['content-type'] = [contentType]
File contents = new File("pact-compatibility-suite/fixtures/${entry['body']}")
contents.withInputStream {
request.body = OptionalBody.body(it.readAllBytes(), new ContentType(contentType))
}
}
configureBody(entry['body'], request)
}

IProviderInfo providerInfo = new ProviderInfo()
Expand Down
76 changes: 47 additions & 29 deletions compatibility-suite/src/test/groovy/steps/shared/SharedSteps.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package steps.shared

import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.HeaderParser
import au.com.dius.pact.core.model.HttpPart
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.Request
Expand All @@ -11,6 +12,7 @@ import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl
import au.com.dius.pact.core.support.json.JsonParser
import au.com.dius.pact.core.support.json.JsonValue
import groovy.transform.Canonical
import groovy.xml.XmlSlurper
import io.cucumber.datatable.DataTable
import io.cucumber.java.en.Given

Expand Down Expand Up @@ -65,25 +67,7 @@ class SharedSteps {
}

if (entry['body']) {
if (entry['body'].startsWith('JSON:')) {
interaction.request.headers['content-type'] = ['application/json']
interaction.request.body = OptionalBody.body(entry['body'][5..-1].bytes, new ContentType('application/json'))
} else if (entry['body'].startsWith('XML:')) {
interaction.request.headers['content-type'] = ['application/xml']
interaction.request.body = OptionalBody.body(entry['body'][4..-1].bytes, new ContentType('application/xml'))
} else {
String contentType = 'text/plain'
if (entry['body'].endsWith('.json')) {
contentType = 'application/json'
} else if (entry['body'].endsWith('.xml')) {
contentType = 'application/xml'
}
interaction.request.headers['content-type'] = [contentType]
File contents = new File("pact-compatibility-suite/fixtures/${entry['body']}")
contents.withInputStream {
interaction.request.body = OptionalBody.body(it.readAllBytes(), new ContentType(contentType))
}
}
configureBody(entry['body'], interaction.request)
}

if (entry['matching rules']) {
Expand Down Expand Up @@ -119,21 +103,13 @@ class SharedSteps {
}

if (entry['response body']) {
String contentType = 'text/plain'
if (entry['response content']) {
contentType = entry['response content']
}
interaction.response.headers['content-type'] = [ contentType ]
File contents = new File("pact-compatibility-suite/fixtures/${entry['response body']}")
contents.withInputStream {
interaction.response.body = OptionalBody.body(it.readAllBytes(), new ContentType(contentType))
}
configureBody(entry['response body'], interaction.response)
}

if (entry['response matching rules']) {
JsonValue json
if (entry['response matching rules'].startsWith('JSON:')) {
json = JsonParser.INSTANCE.parseString(entry['body'][5..-1])
json = JsonParser.INSTANCE.parseString(entry['response matching rules'][5..-1])
} else {
File contents = new File("pact-compatibility-suite/fixtures/${entry['response matching rules']}")
contents.withInputStream {
Expand All @@ -146,4 +122,46 @@ class SharedSteps {
world.interactions << interaction
}
}

static void configureBody(String entry, HttpPart part) {
if (entry.startsWith('JSON:')) {
part.headers['content-type'] = ['application/json']
part.body = OptionalBody.body(entry[5..-1].bytes, new ContentType('application/json'))
} else if (entry.startsWith('XML:')) {
part.headers['content-type'] = ['application/xml']
part.body = OptionalBody.body(entry[4..-1].trim().bytes, new ContentType('application/xml'))
} else if (entry.startsWith('file:')) {
if (entry.endsWith('-body.xml')) {
File contents = new File("pact-compatibility-suite/fixtures/${entry[5..-1].trim()}")
def fixture = new XmlSlurper().parse(contents)
def contentType = fixture.contentType.toString()
part.headers['content-type'] = [contentType]
part.body = OptionalBody.body(fixture.contents.text(), new ContentType(contentType))
} else {
String contentType = determineContentType(entry, part)
part.headers['content-type'] = [contentType]
File contents = new File("pact-compatibility-suite/fixtures/${entry[5..-1].trim()}")
contents.withInputStream {
part.body = OptionalBody.body(it.readAllBytes(), new ContentType(contentType))
}
}
} else {
part.headers['content-type'] = [determineContentType(entry, part)]
part.body = OptionalBody.body(entry)
}
}

private static String determineContentType(String entry, HttpPart part) {
String contentType = part.contentTypeHeader()
if (entry.endsWith('.json')) {
contentType = 'application/json'
} else if (entry.endsWith('.xml')) {
contentType = 'application/xml'
} else if (entry.endsWith('.jpg')) {
contentType = 'image/jpeg'
} else if (entry.endsWith('.pdf')) {
contentType = 'application/pdf'
}
contentType ?: 'text/plain'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package au.com.dius.pact.core.matchers
import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.support.Result
import au.com.dius.pact.core.support.padTo
import io.pact.plugins.jvm.core.InteractionContents
import mu.KLogging
import org.apache.hc.core5.http.NameValuePair
Expand Down Expand Up @@ -39,6 +40,7 @@ class FormPostContentMatcher : ContentMatcher {
))))
}

@Suppress("LongMethod")
private fun compareParameters(
expectedParameters: List<NameValuePair>,
actualParameters: List<NameValuePair>,
Expand All @@ -47,34 +49,74 @@ class FormPostContentMatcher : ContentMatcher {
val expectedMap = expectedParameters.groupBy { it.name }
val actualMap = actualParameters.groupBy { it.name }
val result = mutableListOf<BodyItemMatchResult>()
expectedMap.forEach {
if (actualMap.containsKey(it.key)) {
it.value.forEachIndexed { index, valuePair ->
val path = listOf("$", it.key, index.toString())
if (context.matcherDefined(path)) {
logger.debug { "Matcher defined for form post parameter '${it.key}'[$index]" }
expectedMap.forEach { entry ->
if (actualMap.containsKey(entry.key)) {
val actualParameterValues = actualMap[entry.key]!!
val path = listOf("$", entry.key)
if (context.matcherDefined(path)) {
logger.debug { "Matcher defined for form post parameter '${entry.key}'" }
entry.value.padTo(actualParameterValues.size).forEachIndexed { index, valuePair ->
val childPath = path + index.toString()
result.add(
BodyItemMatchResult(path.joinToString("."),
Matchers.domatch(context, path, valuePair.value,
actualMap[it.key]!![index].value, BodyMismatchFactory)))
} else {
logger.debug { "No matcher defined for form post parameter '${it.key}'[$index], using equality" }
val actualValues = actualMap[it.key]!!
if (actualValues.size <= index) {
result.add(BodyItemMatchResult(path.joinToString("."), listOf(
BodyMismatch("${it.key}=${valuePair.value}", null,
"Expected form post parameter '${it.key}'='${valuePair.value}' but was missing"))))
} else if (valuePair.value != actualValues[index].value) {
result.add(BodyItemMatchResult(path.joinToString("."), listOf(
BodyMismatch("${it.key}=${valuePair.value}",
"${it.key}=${actualValues[index].value}", "Expected form post parameter " +
"'${it.key}'[$index] with value '${valuePair.value}' but was '${actualValues[index].value}'"))))
BodyItemMatchResult(
childPath.joinToString("."),
Matchers.domatch(context, childPath, valuePair.value,
actualParameterValues[index].value, BodyMismatchFactory)
)
)
}
} else {
if (actualParameterValues.size > entry.value.size) {
result.add(
BodyItemMatchResult(
path.joinToString("."), listOf(
BodyMismatch(
"${entry.key}=${entry.value.map { it.value }}",
"${entry.key}=${actualParameterValues.map { it.value }}",
"Expected form post parameter '${entry.key}' with ${entry.value.size} value(s) " +
"but received ${actualParameterValues.size} value(s)"
)
)
)
)
}
entry.value.forEachIndexed { index, valuePair ->
logger.debug { "No matcher defined for form post parameter '${entry.key}'[$index], using equality" }
if (actualParameterValues.size <= index) {
result.add(
BodyItemMatchResult(
path.joinToString("."), listOf(
BodyMismatch(
"${entry.key}=${valuePair.value}", null,
"Expected form post parameter '${entry.key}'='${valuePair.value}' but was missing"
)
)
)
)
} else if (valuePair.value != actualParameterValues[index].value) {
val mismatch = if (entry.value.size == 1 && actualParameterValues.size == 1)
"Expected form post parameter '${entry.key}' with value '${valuePair.value}'" +
" but was '${actualParameterValues[index].value}'"
else
"Expected form post parameter '${entry.key}'[$index] with value '${valuePair.value}'" +
" but was '${actualParameterValues[index].value}'"
result.add(
BodyItemMatchResult(
path.joinToString("."), listOf(
BodyMismatch(
"${entry.key}=${valuePair.value}",
"${entry.key}=${actualParameterValues[index].value}",
mismatch
)
)
)
)
}
}
}
} else {
result.add(BodyItemMatchResult(it.key, listOf(BodyMismatch(it.key, null,
"Expected form post parameter '${it.key}' but was missing"))))
result.add(BodyItemMatchResult(entry.key, listOf(BodyMismatch(entry.key, null,
"Expected form post parameter '${entry.key}' but was missing"))))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import au.com.dius.pact.core.model.parsePath
import au.com.dius.pact.core.support.padTo
import io.pact.plugins.jvm.core.PluginConfiguration
import mu.KLogging
import org.apache.commons.codec.binary.Hex

data class MatchingContext @JvmOverloads constructor(
val matchers: MatchingRuleCategory,
Expand Down Expand Up @@ -256,15 +257,31 @@ object Matching : KLogging() {

fun matchBodyContents(expected: HttpPart, actual: HttpPart): BodyMatchResult {
val matcher = expected.matchingRules.rulesForCategory("body").matchingRules["$"]
val contentType = expected.determineContentType()
return when {
matcher != null && matcher.canMatch(expected.determineContentType()) ->
matcher != null && matcher.canMatch(contentType) ->
BodyMatchResult(null, listOf(BodyItemMatchResult("$",
domatch(matcher, listOf("$"), expected.body.unwrap(), actual.body.unwrap(), BodyMismatchFactory))))
expected.body.unwrap().contentEquals(actual.body.unwrap()) -> BodyMatchResult(null, emptyList())
else -> BodyMatchResult(null, listOf(BodyItemMatchResult("$",
listOf(BodyMismatch(expected.body.unwrap(), actual.body.unwrap(),
"Actual body '${actual.body.valueAsString()}' is not equal to the expected body " +
"'${expected.body.valueAsString()}'")))))
else -> {
val actualContentType = actual.determineContentType()
val actualBody = actual.body.unwrap()
val actualDisplay = if (actualContentType.isBinaryType()) {
"$actualContentType, ${actualBody.size} bytes, starting with ${Hex.encodeHexString(actual.body.slice(32))}"
} else {
"$actualContentType, ${actualBody.size} bytes, starting with ${actual.body.slice(32).toString(actual.body.contentType.asCharset())}"
}
val expectedBody = expected.body.unwrap()
val expectedDisplay = if (contentType.isBinaryType()) {
"$contentType, ${expectedBody.size} bytes, starting with ${Hex.encodeHexString(expected.body.slice(32))}"
} else {
"$contentType, ${expectedBody.size} bytes, starting with ${expected.body.slice(32).toString(expected.body.contentType.asCharset())}"
}
BodyMatchResult(null, listOf(BodyItemMatchResult("$",
listOf(BodyMismatch(
expectedBody, actualBody,
"Actual body [$actualDisplay] is not equal to the expected body [$expectedDisplay]")))))
}
}
}

Expand Down
Loading

0 comments on commit af661f3

Please sign in to comment.