From ba845e825db7b37dcbee6df5ac2e6733c0fee567 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Mon, 26 Aug 2024 14:41:47 +1000 Subject: [PATCH] fix: Support provider tests with multiple targets using different transports #1819 --- .../messaging/SynchronousMessageBuilder.kt | 2 +- .../com/dius/pact/consumer/dsl/PactBuilder.kt | 4 +- .../au/com/dius/pact/core/model/HttpPart.kt | 7 +++ .../com/dius/pact/core/model/V4HttpParts.kt | 8 ++++ .../au/com/dius/pact/core/model/V4Pact.kt | 2 +- .../junit5/PactVerificationContext.kt | 21 ++++++++- .../pact/provider/junit5/PluginTestTarget.kt | 8 +++- .../junit5/PactVerificationContextSpec.groovy | 43 +++++++++++++++++++ .../junit5/PluginTestTargetSpec.groovy | 13 ++++++ .../junit5/PactVerificationSpringExtension.kt | 5 ++- .../PactVerificationSpring6Extension.kt | 5 ++- 11 files changed, 107 insertions(+), 11 deletions(-) diff --git a/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt b/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt index e03f6475c8..97e892e14a 100644 --- a/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt +++ b/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt @@ -120,7 +120,7 @@ open class SynchronousMessageBuilder( override fun addPluginConfiguration(matcher: ContentMatcher, pactConfiguration: Map) { if (pluginConfiguration.containsKey(matcher.pluginName)) { - pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration) + pluginConfiguration[matcher.pluginName] = pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration) } else { pluginConfiguration[matcher.pluginName] = pactConfiguration.toMutableMap() } diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt index 315878273a..6585b436a6 100644 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt @@ -315,7 +315,7 @@ open class PactBuilder( override fun addPluginConfiguration(matcher: ContentMatcher, pactConfiguration: Map) { if (pluginConfiguration.containsKey(matcher.pluginName)) { - pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration) + pluginConfiguration[matcher.pluginName] = pluginConfiguration[matcher.pluginName].deepMerge(pactConfiguration) } else { pluginConfiguration[matcher.pluginName] = pactConfiguration.toMutableMap() } @@ -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) diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt index 096c5943e3..35226f939a 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt @@ -35,6 +35,13 @@ interface IHttpPart { fun setupGenerators(category: Category, context: Map): Map 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): Map = config } /** 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 51fbef595f..f19ecca89f 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 @@ -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): Map { + return mapOf("request" to JsonValue.Object(config)) + } + companion object { @JvmStatic fun fromJson(json: JsonValue): HttpRequest { @@ -227,6 +231,10 @@ data class HttpResponse @JvmOverloads constructor( override fun copyResponse() = this.copy() + override fun transformConfig(config: MutableMap): Map { + return mapOf("response" to JsonValue.Object(config)) + } + companion object { fun fromJson(json: JsonValue): HttpResponse { val status = when { diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt index 1191eed09e..a85ba3b3d0 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt @@ -180,7 +180,7 @@ sealed class V4Interaction( */ fun addPluginConfiguration(plugin: String, config: Map) { if (pluginConfiguration.containsKey(plugin)) { - pluginConfiguration[plugin]!!.deepMerge(config) + pluginConfiguration[plugin] = pluginConfiguration[plugin]!!.deepMerge(config) } else { pluginConfiguration[plugin] = config.toMutableMap() } diff --git a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationContext.kt b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationContext.kt index 7db310fbf3..2c053d1bb4 100644 --- a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationContext.kt +++ b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationContext.kt @@ -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 { @@ -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( @@ -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, diff --git a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PluginTestTarget.kt b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PluginTestTarget.kt index 942b075d2c..d5acd479f8 100644 --- a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PluginTestTarget.kt +++ b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PluginTestTarget.kt @@ -110,7 +110,13 @@ class PluginTestTarget(private val config: MutableMap = 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() diff --git a/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PactVerificationContextSpec.groovy b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PactVerificationContextSpec.groovy index 553cbc6121..cfd3778510 100644 --- a/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PactVerificationContextSpec.groovy +++ b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PactVerificationContextSpec.groovy @@ -39,6 +39,7 @@ class PactVerificationContextSpec extends Specification { } TestTarget target = Stub { executeInteraction(_, _) >> { throw new IOException('Boom!') } + supportsInteraction(_) >> true } IProviderVerifier verifier = Stub() ValueResolver valueResolver = Stub() @@ -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 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 @@ -80,6 +121,7 @@ class PactVerificationContextSpec extends Specification { } TestTarget target = Stub { executeInteraction(_, _) >> { throw new IOException('Boom!') } + supportsInteraction(_) >> true } IProviderVerifier verifier = Stub() ValueResolver valueResolver = Stub() @@ -125,6 +167,7 @@ class PactVerificationContextSpec extends Specification { } TestTarget target = Stub { executeInteraction(_, _) >> { throw new IOException('Boom!') } + supportsInteraction(_) >> true } IProviderVerifier verifier = Mock() ValueResolver valueResolver = Stub() diff --git a/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PluginTestTargetSpec.groovy b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PluginTestTargetSpec.groovy index 988bc207d0..b605032f13 100644 --- a/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PluginTestTargetSpec.groovy +++ b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/PluginTestTargetSpec.groovy @@ -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 = [ diff --git a/provider/junit5spring/src/main/kotlin/au/com/dius/pact/provider/spring/junit5/PactVerificationSpringExtension.kt b/provider/junit5spring/src/main/kotlin/au/com/dius/pact/provider/spring/junit5/PactVerificationSpringExtension.kt index 2d60c55dcc..e7c508c33f 100644 --- a/provider/junit5spring/src/main/kotlin/au/com/dius/pact/provider/spring/junit5/PactVerificationSpringExtension.kt +++ b/provider/junit5spring/src/main/kotlin/au/com/dius/pact/provider/spring/junit5/PactVerificationSpringExtension.kt @@ -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) } } diff --git a/provider/spring6/src/main/kotlin/au/com/dius/pact/provider/spring/spring6/PactVerificationSpring6Extension.kt b/provider/spring6/src/main/kotlin/au/com/dius/pact/provider/spring/spring6/PactVerificationSpring6Extension.kt index 716994fc05..f204e1f429 100644 --- a/provider/spring6/src/main/kotlin/au/com/dius/pact/provider/spring/spring6/PactVerificationSpring6Extension.kt +++ b/provider/spring6/src/main/kotlin/au/com/dius/pact/provider/spring/spring6/PactVerificationSpring6Extension.kt @@ -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) } }