Skip to content

Commit

Permalink
fix: Support provider tests with multiple targets using different tra…
Browse files Browse the repository at this point in the history
…nsports #1819
  • Loading branch information
rholshausen committed Aug 26, 2024
1 parent ae9b230 commit ba845e8
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ open class SynchronousMessageBuilder(

override fun addPluginConfiguration(matcher: ContentMatcher, pactConfiguration: Map<String, JsonValue>) {
if (pluginConfiguration.containsKey(matcher.pluginName)) {
pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration)
pluginConfiguration[matcher.pluginName] = pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration)
} else {
pluginConfiguration[matcher.pluginName] = pactConfiguration.toMutableMap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ open class PactBuilder(

override fun addPluginConfiguration(matcher: ContentMatcher, pactConfiguration: Map<String, JsonValue>) {
if (pluginConfiguration.containsKey(matcher.pluginName)) {
pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration)
pluginConfiguration[matcher.pluginName] = pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration)
} else {
pluginConfiguration[matcher.pluginName] = pactConfiguration.toMutableMap()
}
Expand Down Expand Up @@ -392,7 +392,7 @@ open class PactBuilder(
logger.debug { "Plugin config: $config" }

if (config.interactionConfiguration.isNotEmpty()) {
interaction.addPluginConfiguration(matcher.pluginName, config.interactionConfiguration)
interaction.addPluginConfiguration(matcher.pluginName, part.transformConfig(config.interactionConfiguration))
}
if (config.pactConfiguration.isNotEmpty()) {
addPluginConfiguration(matcher, config.pactConfiguration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ interface IHttpPart {
fun setupGenerators(category: Category, context: Map<String, Any>): Map<String, Generator>

fun hasHeader(name: String): Boolean

/**
* Allows the part of the interaction to transform the config so that it is keyed correctly. For instance,
* an HTTP interaction may have both a request and response body from a plugin. This allows the request and
* response parts to set the config for the correct part of the interaction.
*/
fun transformConfig(config: MutableMap<String, JsonValue>): Map<String, JsonValue> = config
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ data class HttpRequest @JvmOverloads constructor(

override fun hasHeader(name: String) = headers.any { (key, _) -> key.lowercase() == name }

override fun transformConfig(config: MutableMap<String, JsonValue>): Map<String, JsonValue> {
return mapOf("request" to JsonValue.Object(config))
}

companion object {
@JvmStatic
fun fromJson(json: JsonValue): HttpRequest {
Expand Down Expand Up @@ -227,6 +231,10 @@ data class HttpResponse @JvmOverloads constructor(

override fun copyResponse() = this.copy()

override fun transformConfig(config: MutableMap<String, JsonValue>): Map<String, JsonValue> {
return mapOf("response" to JsonValue.Object(config))
}

companion object {
fun fromJson(json: JsonValue): HttpResponse {
val status = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ sealed class V4Interaction(
*/
fun addPluginConfiguration(plugin: String, config: Map<String, JsonValue>) {
if (pluginConfiguration.containsKey(plugin)) {
pluginConfiguration[plugin]!!.deepMerge(config)
pluginConfiguration[plugin] = pluginConfiguration[plugin]!!.deepMerge(config)
} else {
pluginConfiguration[plugin] = config.toMutableMap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ data class PactVerificationContext @JvmOverloads constructor(
interactionMessage += " [PENDING]"
}

val targetForInteraction = currentTarget()
if (targetForInteraction == null) {
val transport = interaction.asV4Interaction().transport ?: "http"
val message = "Did not find a test target to execute for the interaction transport '" +
transport + "'"
return listOf(
VerificationResult.Failed(
message, interactionMessage,
mapOf(
interaction.interactionId.orEmpty() to
listOf(VerificationFailureType.InvalidInteractionFailure(message))
),
consumer.pending
)
)
}

when (providerInfo.verificationType) {
null, PactVerification.REQUEST_RESPONSE -> {
return try {
Expand All @@ -104,7 +121,7 @@ data class PactVerificationContext @JvmOverloads constructor(
val pactPluginData = pact.asV4Pact().get()?.pluginData() ?: emptyList()
val expectedResponse = DefaultResponseGenerator.generateResponse(reqResInteraction.response, context,
GeneratorTestMode.Provider, pactPluginData, pluginData)
val actualResponse = target.executeInteraction(client, request)
val actualResponse = targetForInteraction.executeInteraction(client, request)

listOf(
verifier!!.verifyRequestResponsePact(
Expand Down Expand Up @@ -148,7 +165,7 @@ data class PactVerificationContext @JvmOverloads constructor(
)
}
return listOf(verifier!!.verifyInteractionViaPlugin(providerInfo, consumer, v4pact, interaction.asV4Interaction(),
client, request, context + ("userConfig" to target.userConfig)))
client, request, context + ("userConfig" to targetForInteraction.userConfig)))
}
else -> {
return listOf(verifier!!.verifyResponseByInvokingProviderMethods(providerInfo, consumer, interaction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ class PluginTestTarget(private val config: MutableMap<String, Any?> = mutableMap
return false
}

override fun supportsInteraction(interaction: Interaction) = interaction.isV4()
override fun supportsInteraction(interaction: Interaction): Boolean {
return interaction.isV4() &&
(
!config.containsKey("transport") ||
config["transport"] == interaction.asV4Interaction().transport
)
}

override fun executeInteraction(client: Any?, request: Any?): ProviderResponse {
return ProviderResponse()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class PactVerificationContextSpec extends Specification {
}
TestTarget target = Stub {
executeInteraction(_, _) >> { throw new IOException('Boom!') }
supportsInteraction(_) >> true
}
IProviderVerifier verifier = Stub()
ValueResolver valueResolver = Stub()
Expand All @@ -65,6 +66,46 @@ class PactVerificationContextSpec extends Specification {
context.testExecutionResult[0].failures['12345'][0] instanceof VerificationFailureType.ExceptionFailure
}

@SuppressWarnings('LineLength')
def 'sets the test result to an error result if no test target is found to execute the interaction'() {
given:
PactVerificationContext context
ExtensionContext.Store store = Stub {
get(_) >> { args ->
if (args[0] == 'interactionContext') {
context
}
}
}
ExtensionContext extContext = Stub {
getStore(_) >> store
}
TestTarget target = Stub {
supportsInteraction(_) >> false
}
IProviderVerifier verifier = Stub()
ValueResolver valueResolver = Stub()
IProviderInfo provider = Stub {
getName() >> 'Stub'
}
IConsumerInfo consumer = new ConsumerInfo('Test')
Interaction interaction = new RequestResponseInteraction('Test Interaction', [], new Request(),
new Response(), '12345')
def pact = new RequestResponsePact(new Provider(), new Consumer(), [interaction ])
List<VerificationResult> testResults = []

context = new PactVerificationContext(store, extContext, target, verifier, valueResolver,
provider, consumer, interaction, pact, testResults)

when:
context.verifyInteraction()

then:
thrown(AssertionError)
context.testExecutionResult[0] instanceof VerificationResult.Failed
context.testExecutionResult[0].description == "Did not find a test target to execute for the interaction transport 'http'"
}

def 'only throw an exception if there are non-pending failures'() {
given:
PactVerificationContext context
Expand All @@ -80,6 +121,7 @@ class PactVerificationContextSpec extends Specification {
}
TestTarget target = Stub {
executeInteraction(_, _) >> { throw new IOException('Boom!') }
supportsInteraction(_) >> true
}
IProviderVerifier verifier = Stub()
ValueResolver valueResolver = Stub()
Expand Down Expand Up @@ -125,6 +167,7 @@ class PactVerificationContextSpec extends Specification {
}
TestTarget target = Stub {
executeInteraction(_, _) >> { throw new IOException('Boom!') }
supportsInteraction(_) >> true
}
IProviderVerifier verifier = Mock()
ValueResolver valueResolver = Stub()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ class PluginTestTargetSpec extends Specification {
new V4Interaction.SynchronousHttp('test') | true
}

def 'only supports interactions that have a matching transport'() {
given:
def interaction1 = new V4Interaction.SynchronousHttp('test')
interaction1.transport = 'http'
def interaction2 = new V4Interaction.SynchronousHttp('test')
interaction2.transport = 'xttp'
def pluginTarget = new PluginTestTarget([transport: 'xttp'])

expect:
!pluginTarget.supportsInteraction(interaction1)
pluginTarget.supportsInteraction(interaction2)
}

def 'when calling a plugin, prepareRequest must merge the provider state test context config'() {
given:
def config = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ open class PactVerificationSpringExtension(
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val testContext = store.get("interactionContext") as PactVerificationContext
val target = testContext.currentTarget()
return when (parameterContext.parameter.type) {
MockHttpServletRequestBuilder::class.java -> testContext.target is MockMvcTestTarget
WebTestClient.RequestHeadersSpec::class.java -> testContext.target is WebFluxTarget
MockHttpServletRequestBuilder::class.java -> target is MockMvcTestTarget
WebTestClient.RequestHeadersSpec::class.java -> target is WebFluxTarget
else -> super.supportsParameter(parameterContext, extensionContext)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ open class PactVerificationSpring6Extension(
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val testContext = store.get("interactionContext") as PactVerificationContext
val target = testContext.currentTarget()
return when (parameterContext.parameter.type) {
MockHttpServletRequestBuilder::class.java -> testContext.target is Spring6MockMvcTestTarget
WebTestClient.RequestHeadersSpec::class.java -> testContext.target is WebFluxSpring6Target
MockHttpServletRequestBuilder::class.java -> target is Spring6MockMvcTestTarget
WebTestClient.RequestHeadersSpec::class.java -> target is WebFluxSpring6Target
else -> super.supportsParameter(parameterContext, extensionContext)
}
}
Expand Down

0 comments on commit ba845e8

Please sign in to comment.