From e2905aeb89ccab511bea866c8e58d7cca9c7f29e Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Thu, 16 Mar 2023 16:12:54 +1100 Subject: [PATCH] fix(JUnit5): Initialise any plugins before running the provider verification --- config/detekt-config.yml | 2 +- .../com/dius/pact/consumer/MockHttpServer.kt | 13 +-- .../pact/core/matchers/CatalogueEntries.kt | 22 +++++ .../au/com/dius/pact/core/model/V4Pact.kt | 7 +- .../junit5/PactVerificationContext.kt | 27 ++++-- .../junit5/PactVerificationExtension.kt | 1 + .../dius/pact/provider/ProviderVerifier.kt | 85 +++++++++++++++++-- .../pact/provider/ProviderVerifierSpec.groovy | 33 +++++++ 8 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/CatalogueEntries.kt diff --git a/config/detekt-config.yml b/config/detekt-config.yml index b82de4b80..73e6c6974 100755 --- a/config/detekt-config.yml +++ b/config/detekt-config.yml @@ -517,7 +517,7 @@ style: active: false includeLineWrapping: false ForbiddenComment: - active: true + active: false values: - 'FIXME:' - 'STOPSHIP:' diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/MockHttpServer.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/MockHttpServer.kt index bff4606f7..a5dc55cb7 100755 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/MockHttpServer.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/MockHttpServer.kt @@ -402,15 +402,4 @@ fun calculateCharset(headers: Map>): Charset { return default } -fun interactionCatalogueEntries(): List { - return listOf( - CatalogueEntry(CatalogueEntryType.TRANSPORT, CatalogueEntryProviderType.CORE, "core", - "http", mapOf()), - CatalogueEntry(CatalogueEntryType.TRANSPORT, CatalogueEntryProviderType.CORE, "core", - "https", mapOf()), - CatalogueEntry(CatalogueEntryType.INTERACTION, CatalogueEntryProviderType.CORE, "core", - "message", mapOf()), - CatalogueEntry(CatalogueEntryType.INTERACTION, CatalogueEntryProviderType.CORE, "core", - "synchronous-message", mapOf()) - ) -} +fun interactionCatalogueEntries() = au.com.dius.pact.core.matchers.interactionCatalogueEntries() diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/CatalogueEntries.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/CatalogueEntries.kt new file mode 100644 index 000000000..d7dd86ac7 --- /dev/null +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/CatalogueEntries.kt @@ -0,0 +1,22 @@ +package au.com.dius.pact.core.matchers + +import io.pact.plugins.jvm.core.CatalogueEntry +import io.pact.plugins.jvm.core.CatalogueEntryProviderType +import io.pact.plugins.jvm.core.CatalogueEntryType + +/** + * Configures all the core transport catalogue entries + */ +fun interactionCatalogueEntries(): List { + return listOf( + CatalogueEntry( + CatalogueEntryType.TRANSPORT, CatalogueEntryProviderType.CORE, "core", + "http", mapOf()), + CatalogueEntry(CatalogueEntryType.TRANSPORT, CatalogueEntryProviderType.CORE, "core", + "https", mapOf()), + CatalogueEntry(CatalogueEntryType.INTERACTION, CatalogueEntryProviderType.CORE, "core", + "message", mapOf()), + CatalogueEntry(CatalogueEntryType.INTERACTION, CatalogueEntryProviderType.CORE, "core", + "synchronous-message", mapOf()) + ) +} 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 7e2e0b2c3..831c2dc27 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 @@ -664,7 +664,7 @@ open class V4Pact @JvmOverloads constructor( } } - fun pluginData(): List { + open fun pluginData(): List { return when (val plugins = metadata["plugins"]) { is List<*> -> plugins.mapNotNull { when (it) { @@ -678,4 +678,9 @@ open class V4Pact @JvmOverloads constructor( else -> emptyList() } } + + open fun requiresPlugins(): Boolean { + val pluginData = metadata["plugins"] + return pluginData is List<*> && pluginData.isNotEmpty() + } } 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 e8c6504a0..29091edae 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 @@ -8,6 +8,7 @@ import au.com.dius.pact.core.model.RequestResponseInteraction import au.com.dius.pact.core.model.UnknownPactSource import au.com.dius.pact.core.model.V4Interaction import au.com.dius.pact.core.model.generators.GeneratorTestMode +import au.com.dius.pact.core.support.Json import au.com.dius.pact.core.support.MetricEvent import au.com.dius.pact.core.support.Metrics import au.com.dius.pact.core.support.Result @@ -21,6 +22,7 @@ import au.com.dius.pact.provider.ProviderVerifier import au.com.dius.pact.provider.VerificationFailureType import au.com.dius.pact.provider.VerificationResult import au.com.dius.pact.provider.junitsupport.TestDescription +import io.pact.plugins.jvm.core.PluginConfiguration import org.junit.jupiter.api.extension.ExtensionContext /** @@ -55,7 +57,7 @@ data class PactVerificationContext @JvmOverloads constructor( try { Metrics.sendMetrics(MetricEvent.ProviderVerificationRan(1, "junit5")) - val result = validateTestExecution(client, request, testContext.executionContext ?: mutableMapOf()) + val result = validateTestExecution(client, request, testContext.executionContext ?: mutableMapOf(), pact) verifier!!.displayOutput(result.flatMap { it.getResultOutput() }) this.testExecutionResult.addAll(result.filterIsInstance()) @@ -81,7 +83,8 @@ data class PactVerificationContext @JvmOverloads constructor( private fun validateTestExecution( client: Any?, request: Any?, - context: MutableMap + context: MutableMap, + pact: Pact ): List { var interactionMessage = "Verifying a pact between ${consumer.name} and ${providerInfo.name}" + " - ${interaction.description}" @@ -92,19 +95,27 @@ data class PactVerificationContext @JvmOverloads constructor( when (providerInfo.verificationType) { null, PactVerification.REQUEST_RESPONSE -> { return try { - val reqResInteraction = if (interaction is V4Interaction.SynchronousHttp) { - interaction.asV3Interaction() + val (reqResInteraction, pluginData) = if (interaction is V4Interaction.SynchronousHttp) { + interaction.asV3Interaction() to interaction.pluginConfiguration.toMap() } else { - interaction as RequestResponseInteraction + interaction as RequestResponseInteraction to emptyMap() } + val pactPluginData = pact.asV4Pact().get()?.pluginData() ?: emptyList() val expectedResponse = DefaultResponseGenerator.generateResponse(reqResInteraction.response, context, - GeneratorTestMode.Provider, emptyList(), emptyMap()) // TODO: need to pass any plugin config here + GeneratorTestMode.Provider, pactPluginData, pluginData) val actualResponse = target.executeInteraction(client, request) + val pluginContext = pactPluginData.associate { + it.name to PluginConfiguration( + pluginData[it.name].orEmpty().toMutableMap(), + it.configuration.mapValues { (_, v) -> Json.toJson(v) }.toMutableMap() + ) + } listOf( verifier!!.verifyRequestResponsePact( expectedResponse, actualResponse, interactionMessage, mutableMapOf(), - reqResInteraction.interactionId.orEmpty(), consumer.pending + reqResInteraction.interactionId.orEmpty(), consumer.pending, + pluginContext ) ) } catch (e: Exception) { @@ -127,7 +138,7 @@ data class PactVerificationContext @JvmOverloads constructor( } } PactVerification.PLUGIN -> { - val v4pact = when(val p = pact.asV4Pact()) { + val v4pact = when(val p = this.pact.asV4Pact()) { is Result.Ok -> p.value is Result.Err -> return listOf( VerificationResult.Failed( diff --git a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationExtension.kt b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationExtension.kt index 77582b3a3..ab38bee52 100644 --- a/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationExtension.kt +++ b/provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationExtension.kt @@ -157,6 +157,7 @@ open class PactVerificationExtension( setupReporters(verifier, serviceName, interaction.description, extContext, testContext.valueResolver) + verifier.initialisePlugins(pact) verifier.initialiseReporters(testContext.providerInfo) verifier.reportVerificationForConsumer(consumer, testContext.providerInfo, pactSource) diff --git a/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt b/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt index a2d0c927d..1684e3511 100644 --- a/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt +++ b/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt @@ -3,9 +3,12 @@ package au.com.dius.pact.provider import au.com.dius.pact.core.matchers.BodyMismatch import au.com.dius.pact.core.matchers.BodyTypeMismatch import au.com.dius.pact.core.matchers.HeaderMismatch +import au.com.dius.pact.core.matchers.MatchingConfig import au.com.dius.pact.core.matchers.MetadataMismatch import au.com.dius.pact.core.matchers.StatusMismatch import au.com.dius.pact.core.matchers.generators.ArrayContainsJsonGenerator +import au.com.dius.pact.core.matchers.interactionCatalogueEntries +import au.com.dius.pact.core.matchers.matcherCatalogueEntries import au.com.dius.pact.core.model.BrokerUrlSource import au.com.dius.pact.core.model.ContentType import au.com.dius.pact.core.model.DefaultPactReader @@ -30,7 +33,8 @@ import au.com.dius.pact.core.support.Auth import au.com.dius.pact.core.support.MetricEvent import au.com.dius.pact.core.support.Metrics import au.com.dius.pact.core.support.Result -import au.com.dius.pact.core.support.Result.* +import au.com.dius.pact.core.support.Result.Err +import au.com.dius.pact.core.support.Result.Ok import au.com.dius.pact.core.support.expressions.SystemPropertyResolver import au.com.dius.pact.core.support.hasProperty import au.com.dius.pact.core.support.ifNullOrEmpty @@ -41,8 +45,11 @@ import au.com.dius.pact.provider.reporters.VerifierReporter import groovy.lang.Closure import io.github.classgraph.ClassGraph import io.pact.plugins.jvm.core.CatalogueEntry +import io.pact.plugins.jvm.core.CatalogueManager import io.pact.plugins.jvm.core.DefaultPluginManager import io.pact.plugins.jvm.core.InteractionVerificationDetails +import io.pact.plugins.jvm.core.PluginConfiguration +import io.pact.plugins.jvm.core.PluginManager import mu.KLogging import java.io.File import java.lang.reflect.Method @@ -270,9 +277,25 @@ interface IProviderVerifier { interactionMessage: String, failures: MutableMap, interactionId: String, - pending: Boolean + pending: Boolean, + pluginConfiguration: Map ): VerificationResult + /** + * Compares the expected and actual responses + */ + @Suppress("LongParameterList") + @Deprecated("Use the version that passes in any plugin configuration") + fun verifyRequestResponsePact( + expectedResponse: IResponse, + actualResponse: ProviderResponse, + interactionMessage: String, + failures: MutableMap, + interactionId: String, + pending: Boolean + ): VerificationResult = verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, failures, + interactionId, pending, emptyMap()) + /** * If publishing of verification results has been disabled */ @@ -353,6 +376,7 @@ open class ProviderVerifier @JvmOverloads constructor ( var stateChangeHandler: StateChange = DefaultStateChange var pactReader: PactReader = DefaultPactReader override var verificationSource: String? = null + var pluginManager: PluginManager = DefaultPluginManager /** * This will return true unless the pact.verifier.publishResults property has the value of "true" @@ -412,8 +436,15 @@ open class ProviderVerifier @JvmOverloads constructor ( val actualResponse = ProviderResponse(response["statusCode"] as Int, response["headers"] as Map>, ContentType.UNKNOWN, body ) - result = result.merge(this.verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, - failures, interactionId.orEmpty(), pending)) + result = result.merge(this.verifyRequestResponsePact( + expectedResponse, + actualResponse, + interactionMessage, + failures, + interactionId.orEmpty(), + pending, + emptyMap() // TODO: pass any plugin config here + )) } result } @@ -468,7 +499,8 @@ open class ProviderVerifier @JvmOverloads constructor ( interactionMessage, failures, interactionId, - pending + pending, + emptyMap() // TODO: Pass in any plugin config here ) } } catch (e: Exception) { @@ -786,9 +818,10 @@ open class ProviderVerifier @JvmOverloads constructor ( interactionMessage: String, failures: MutableMap, interactionId: String, - pending: Boolean + pending: Boolean, + pluginConfiguration: Map ): VerificationResult { - val comparison = ResponseComparison.compareResponse(expectedResponse, actualResponse) + val comparison = ResponseComparison.compareResponse(expectedResponse, actualResponse, pluginConfiguration) reporters.forEach { it.returnsAResponseWhich() } @@ -874,8 +907,15 @@ open class ProviderVerifier @JvmOverloads constructor ( val expectedResponse = interaction.response.generatedResponse(context, GeneratorTestMode.Provider) val actualResponse = client.makeRequest(interaction.request.generatedRequest(context, GeneratorTestMode.Provider)) - verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, failures, - interaction.interactionId.orEmpty(), pending) + verifyRequestResponsePact( + expectedResponse, + actualResponse, + interactionMessage, + failures, + interaction.interactionId.orEmpty(), + pending, + emptyMap() // TODO: Pass any plugin config in here + ) } catch (e: Exception) { failures[interactionMessage] = e reporters.forEach { @@ -920,7 +960,10 @@ open class ProviderVerifier @JvmOverloads constructor ( client: IPactBrokerClient? = null ): VerificationResult { val pact = FilteredPact(loadPactFileForConsumer(consumer)) { filterInteractions(it) } + reportVerificationForConsumer(consumer, provider, pact.source) + initialisePlugins(pact) + return if (pact.interactions.isEmpty()) { reporters.forEach { it.warnPactFileHasNoInteractions(pact as Pact) } VerificationResult.Ok() @@ -956,6 +999,30 @@ open class ProviderVerifier @JvmOverloads constructor ( } } + /** + * Initialise any required plugins and plugin entries required for the verification + */ + fun initialisePlugins(pact: Pact) { + CatalogueManager.registerCoreEntries( + MatchingConfig.contentMatcherCatalogueEntries() + + matcherCatalogueEntries() + + interactionCatalogueEntries() + + MatchingConfig.contentHandlerCatalogueEntries() + ) + val v4pact = pact.asV4Pact().get() + if (v4pact != null && v4pact.requiresPlugins()) { + logger.info { "Pact file requires plugins, will load those now" } + for (pluginDetails in v4pact.pluginData()) { + val result = pluginManager.loadPlugin(pluginDetails.name, pluginDetails.version) + if (result is Err) { + throw RuntimeException( + "Failed to load plugin ${pluginDetails.name}/${pluginDetails.version} - ${result.error}" + ) + } + } + } + } + override fun reportVerificationForConsumer( consumer: IConsumerInfo, provider: IProviderInfo, diff --git a/provider/src/test/groovy/au/com/dius/pact/provider/ProviderVerifierSpec.groovy b/provider/src/test/groovy/au/com/dius/pact/provider/ProviderVerifierSpec.groovy index 94602763e..3a4566159 100644 --- a/provider/src/test/groovy/au/com/dius/pact/provider/ProviderVerifierSpec.groovy +++ b/provider/src/test/groovy/au/com/dius/pact/provider/ProviderVerifierSpec.groovy @@ -11,6 +11,7 @@ import au.com.dius.pact.core.model.InvalidPathExpression import au.com.dius.pact.core.model.OptionalBody import au.com.dius.pact.core.model.Pact import au.com.dius.pact.core.model.PactReader +import au.com.dius.pact.core.model.PluginData import au.com.dius.pact.core.model.Provider import au.com.dius.pact.core.model.ProviderState import au.com.dius.pact.core.model.Request @@ -20,6 +21,7 @@ import au.com.dius.pact.core.model.Response import au.com.dius.pact.core.model.UnknownPactSource import au.com.dius.pact.core.model.UrlSource import au.com.dius.pact.core.model.V4Interaction +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.MatchingRules import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl @@ -34,6 +36,7 @@ import au.com.dius.pact.core.support.expressions.SystemPropertyResolver import au.com.dius.pact.provider.reporters.Event import au.com.dius.pact.provider.reporters.VerifierReporter import groovy.json.JsonOutput +import io.pact.plugins.jvm.core.PluginManager import spock.lang.Specification import spock.lang.Unroll import spock.util.environment.RestoreSystemProperties @@ -426,6 +429,7 @@ class ProviderVerifierSpec extends Specification { def interaction2 = Stub(RequestResponseInteraction) def mockPact = Stub(Pact) { getSource() >> new BrokerUrlSource('http://localhost', 'http://pact-broker') + asV4Pact() >> new Result.Err('Not V4') } verifier.projectHasProperty = { it == ProviderVerifier.PACT_VERIFIER_PUBLISH_RESULTS } @@ -475,6 +479,7 @@ class ProviderVerifierSpec extends Specification { def interaction2 = Stub(RequestResponseInteraction) def mockPact = Stub(Pact) { getSource() >> new BrokerUrlSource('http://localhost', 'http://pact-broker') + asV4Pact() >> new Result.Err('Not V4') } verifier.projectHasProperty = { it == ProviderVerifier.PACT_VERIFIER_PUBLISH_RESULTS } @@ -520,6 +525,7 @@ class ProviderVerifierSpec extends Specification { def interaction2 = Stub(RequestResponseInteraction) def mockPact = Stub(Pact) { getSource() >> new BrokerUrlSource('http://localhost', 'http://pact-broker') + asV4Pact() >> new Result.Err('Not V4') } verifier.projectHasProperty = { it == ProviderVerifier.PACT_VERIFIER_PUBLISH_RESULTS } @@ -565,6 +571,7 @@ class ProviderVerifierSpec extends Specification { interaction2.asSynchronousRequestResponse() >> { interaction2 } def mockPact = Mock(Pact) { getSource() >> UnknownPactSource.INSTANCE + asV4Pact() >> new Result.Err('Not V4') } verifier.pactReader.loadPact(_) >> mockPact @@ -885,4 +892,30 @@ class ProviderVerifierSpec extends Specification { result.failures['abc123'][0].description == 'Verification factory method failed with an exception' result.failures['abc123'][0].e instanceof RuntimeException } + + def 'when verifying a V4 Pact, it should load any required plugins'() { + given: + ProviderInfo provider = new ProviderInfo('Test Provider') + ConsumerInfo consumer = new ConsumerInfo(name: 'Test Consumer', pactSource: UnknownPactSource.INSTANCE) + PactBrokerClient pactBrokerClient = Mock(PactBrokerClient, constructorArgs: ['']) + verifier.pactReader = Stub(PactReader) + def v4pact = Mock(V4Pact) { + requiresPlugins() >> true + pluginData() >> { [new PluginData('a', '1.0', [:]), new PluginData('b', '2.0', [:])] } + } + def mockPact = Stub(Pact) { + getSource() >> new BrokerUrlSource('http://localhost', 'http://pact-broker') + asV4Pact() >> new Result.Ok(v4pact) + } + + verifier.pactReader.loadPact(_) >> mockPact + verifier.pluginManager = Mock(PluginManager) + + when: + verifier.runVerificationForConsumer([:], provider, consumer, pactBrokerClient) + + then: + 1 * verifier.pluginManager.loadPlugin('a', '1.0') >> new Result.Ok(null) + 1 * verifier.pluginManager.loadPlugin('b', '2.0') >> new Result.Ok(null) + } }