au.com.dius.pact.provider.junit.loader.PactBrokerLoader.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pact-jvm-provider Show documentation
Show all versions of pact-jvm-provider Show documentation
Pact provider
=============
sub project of https://github.com/DiUS/pact-jvm
The pact provider is responsible for verifying that an API provider adheres to a number of pacts authored by its clients
This library provides the basic tools required to automate the process, and should be usable on its own in many instances.
Framework and build tool specific bindings will be provided in separate libraries that build on top of this core functionality.
### Provider State
Before each interaction is executed, the provider under test will have the opportunity to enter a state.
Generally the state maps to a set of fixture data for mocking out services that the provider is a consumer of (they will have their own pacts)
The pact framework will instruct the test server to enter that state by sending:
POST "${config.stateChangeUrl.url}/setup" { "state" : "${interaction.stateName}" }
### An example of running provider verification with junit
This example uses Groovy, JUnit 4 and Hamcrest matchers to run the provider verification.
As the provider service is a DropWizard application, it uses the DropwizardAppRule to startup the service before running any test.
**Warning:** It only grabs the first interaction from the pact file with the consumer, where there could be many. (This could possibly be solved with a parameterized test)
```groovy
class ReadmeExamplePactJVMProviderJUnitTest {
@ClassRule
public static final TestRule startServiceRule = new DropwizardAppRule<DropwizardConfiguration>(
TestDropwizardApplication, ResourceHelpers.resourceFilePath('dropwizard/test-config.yaml'))
private static ProviderInfo serviceProvider
private static Pact<RequestResponseInteraction> testConsumerPact
private static ConsumerInfo consumer
@BeforeClass
static void setupProvider() {
serviceProvider = new ProviderInfo('Dropwizard App')
serviceProvider.setProtocol('http')
serviceProvider.setHost('localhost')
serviceProvider.setPort(8080)
serviceProvider.setPath('/')
consumer = new ConsumerInfo()
consumer.setName('test_consumer')
consumer.setPactSource(new UrlSource(
ReadmeExamplePactJVMProviderJUnitTest.getResource('/pacts/zoo_app-animal_service.json').toString()))
testConsumerPact = DefaultPactReader.INSTANCE.loadPact(consumer.getPactSource()) as Pact<RequestResponseInteraction>
}
@Test
void runConsumerPacts() {
// grab the first interaction from the pact with consumer
Interaction interaction = testConsumerPact.interactions.get(0)
// setup the verifier
ProviderVerifier verifier = setupVerifier(interaction, serviceProvider, consumer)
// setup any provider state
// setup the client and interaction to fire against the provider
ProviderClient client = new ProviderClient(serviceProvider, new HttpClientFactory())
Map<String, Object> failures = new HashMap<>()
verifier.verifyResponseFromProvider(serviceProvider, interaction, interaction.getDescription(), failures, client)
// normally assert all good, but in this example it will fail
assertThat(failures, is(not(empty())))
verifier.displayFailures(failures)
}
private ProviderVerifier setupVerifier(Interaction interaction, ProviderInfo provider, ConsumerInfo consumer) {
ProviderVerifier verifier = new ProviderVerifier()
verifier.initialiseReporters(provider)
verifier.reportVerificationForConsumer(consumer, provider, new UrlSource('http://example.example'))
if (!interaction.getProviderStates().isEmpty()) {
for (ProviderState providerState: interaction.getProviderStates()) {
verifier.reportStateForInteraction(providerState.getName(), provider, consumer, true)
}
}
verifier.reportInteractionDescription(interaction)
return verifier
}
}
```
### An example of running provider verification with spock
This example uses groovy and spock to run the provider verification.
Again the provider service is a DropWizard application, and is using the DropwizardAppRule to startup the service.
This example runs all interactions using spocks Unroll feature
```groovy
class ReadmeExamplePactJVMProviderSpockSpec extends Specification {
@ClassRule @Shared
TestRule startServiceRule = new DropwizardAppRule<DropwizardConfiguration>(TestDropwizardApplication,
ResourceHelpers.resourceFilePath('dropwizard/test-config.yaml'))
@Shared
ProviderInfo serviceProvider
ProviderVerifier verifier
def setupSpec() {
serviceProvider = new ProviderInfo('Dropwizard App')
serviceProvider.protocol = 'http'
serviceProvider.host = 'localhost'
serviceProvider.port = 8080
serviceProvider.path = '/'
serviceProvider.hasPactWith('zoo_app') { consumer ->
consumer.pactSource = new FileSource(new File(ResourceHelpers.resourceFilePath('pacts/zoo_app-animal_service.json')))
}
}
def setup() {
verifier = new ProviderVerifier()
}
def cleanup() {
// cleanup provider state
// ie. db.truncateAllTables()
}
def cleanupSpec() {
// cleanup provider
}
@Unroll
def "Provider Pact - With Consumer #consumer"() {
expect:
!verifyConsumerPact(consumer).empty
where:
consumer << serviceProvider.consumers
}
private Map verifyConsumerPact(ConsumerInfo consumer) {
Map failures = [:]
verifier.initialiseReporters(serviceProvider)
verifier.runVerificationForConsumer(failures, serviceProvider, consumer)
if (!failures.empty) {
verifier.displayFailures(failures)
}
failures
}
}
```
The newest version!
package au.com.dius.pact.provider.junit.loader
import au.com.dius.pact.core.model.BrokerUrlSource
import au.com.dius.pact.core.model.Consumer
import au.com.dius.pact.core.model.DefaultPactReader
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactBrokerSource
import au.com.dius.pact.core.model.PactReader
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.pactbroker.PactBrokerClient
import au.com.dius.pact.core.support.expressions.ExpressionParser.parseExpression
import au.com.dius.pact.core.support.expressions.ExpressionParser.parseListExpression
import au.com.dius.pact.core.support.expressions.SystemPropertyResolver
import au.com.dius.pact.core.support.expressions.ValueResolver
import au.com.dius.pact.core.support.isNotEmpty
import au.com.dius.pact.provider.ConsumerInfo
import mu.KLogging
import org.apache.http.client.utils.URIBuilder
import java.io.IOException
import java.net.URI
import java.net.URISyntaxException
import kotlin.reflect.KClass
/**
* Out-of-the-box implementation of {@link PactLoader} that downloads pacts from Pact broker
*/
open class PactBrokerLoader(
val pactBrokerHost: String,
val pactBrokerPort: String?,
val pactBrokerScheme: String,
val pactBrokerTags: List? = listOf("latest"),
val pactBrokerConsumers: List = emptyList(),
var failIfNoPactsFound: Boolean = true,
var authentication: PactBrokerAuth?,
var valueResolverClass: KClass?,
valueResolver: ValueResolver? = null
) : OverrideablePactLoader {
private var pacts: MutableMap>> = mutableMapOf()
private var resolver: ValueResolver? = valueResolver
private var overriddenPactUrl: String? = null
private var overriddenConsumer: String? = null
var pactReader: PactReader = DefaultPactReader
constructor(pactBroker: PactBroker) : this(
pactBroker.host,
pactBroker.port,
pactBroker.scheme,
pactBroker.tags.toList(),
pactBroker.consumers.toList(),
true,
pactBroker.authentication,
pactBroker.valueResolver
)
override fun description(): String {
val resolver = setupValueResolver()
val tags = pactBrokerTags?.flatMap { parseListExpression(it, resolver) }?.filter { it.isNotEmpty() }
val consumers = pactBrokerConsumers.flatMap { parseListExpression(it, resolver) }.filter { it.isNotEmpty() }
var source = getPactBrokerSource(resolver).description()
if (tags != null && tags.isNotEmpty()) {
source += " tags=$tags"
}
if (consumers.isNotEmpty()) {
source += " consumers=$consumers"
}
return source
}
override fun overridePactUrl(pactUrl: String, consumer: String) {
overriddenPactUrl = pactUrl
overriddenConsumer = consumer
}
override fun load(providerName: String): List> {
val resolver = setupValueResolver()
val pacts = when {
overriddenPactUrl.isNotEmpty() -> {
val brokerUri = brokerUrl(resolver).build()
val pactBrokerClient = newPactBrokerClient(brokerUri, resolver)
val pactSource = BrokerUrlSource(overriddenPactUrl!!, brokerUri.toString())
pactSource.encodePath = false
listOf(loadPact(ConsumerInfo(name = overriddenConsumer!!, pactSource = pactSource),
pactBrokerClient.options))
}
pactBrokerTags.isNullOrEmpty() -> loadPactsForProvider(providerName, null, resolver)
else -> {
pactBrokerTags.flatMap { parseListExpression(it, resolver) }.flatMap {
try {
loadPactsForProvider(providerName, it, resolver)
} catch (e: NoPactsFoundException) {
// Ignoring exception at this point, it will be handled at a higher level
emptyList>()
}
}
}
}
return pacts
}
private fun setupValueResolver(): ValueResolver {
var valueResolver: ValueResolver = SystemPropertyResolver()
if (resolver != null) {
valueResolver = resolver!!
} else if (valueResolverClass != null) {
try {
valueResolver = valueResolverClass!!.java.newInstance()
} catch (e: InstantiationException) {
logger.warn(e) { "Failed to instantiate the value resolver, using the default" }
} catch (e: IllegalAccessException) {
logger.warn(e) { "Failed to instantiate the value resolver, using the default" }
}
}
return valueResolver
}
override fun getPactSource(): PactSource? {
val resolver = setupValueResolver()
return getPactBrokerSource(resolver)
}
override fun setValueResolver(valueResolver: ValueResolver) {
this.resolver = valueResolver
}
@Throws(IOException::class, IllegalArgumentException::class)
private fun loadPactsForProvider(
providerName: String,
tag: String?,
resolver: ValueResolver
): List> {
logger.debug { "Loading pacts from pact broker for provider $providerName and tag $tag" }
val uriBuilder = brokerUrl(resolver)
try {
var consumers: List
val pactBrokerClient = newPactBrokerClient(uriBuilder.build(), resolver)
consumers = if (tag.isNullOrEmpty() || tag == "latest") {
pactBrokerClient.fetchConsumers(providerName).map { ConsumerInfo.from(it) }
} else {
pactBrokerClient.fetchConsumersWithTag(providerName, tag).map { ConsumerInfo.from(it) }
}
if (failIfNoPactsFound && consumers.isEmpty()) {
throw NoPactsFoundException("No consumer pacts were found for provider '" + providerName + "' and tag '" +
tag + "'. (URL " + getUrlForProvider(providerName, tag.orEmpty(), pactBrokerClient) + ")")
}
if (pactBrokerConsumers.isNotEmpty()) {
val consumerInclusions = pactBrokerConsumers.flatMap { parseListExpression(it, resolver) }
consumers = consumers.filter { consumerInclusions.isEmpty() || consumerInclusions.contains(it.name) }
}
return consumers.map { loadPact(it, pactBrokerClient.options) }
} catch (e: URISyntaxException) {
throw IOException("Was not able load pacts from broker as the broker URL was invalid", e)
}
}
private fun brokerUrl(resolver: ValueResolver): URIBuilder {
val (host, port, scheme) = getPactBrokerSource(resolver)
val uriBuilder = URIBuilder().setScheme(scheme).setHost(host)
if (port.isNotEmpty()) {
uriBuilder.port = Integer.parseInt(port)
}
return uriBuilder
}
private fun getPactBrokerSource(resolver: ValueResolver): PactBrokerSource {
val scheme = parseExpression(pactBrokerScheme, resolver)
val host = parseExpression(pactBrokerHost, resolver)
val port = parseExpression(pactBrokerPort, resolver)
if (host.isNullOrEmpty()) {
throw IllegalArgumentException(String.format("Invalid pact broker host specified ('%s'). " +
"Please provide a valid host or specify the system property 'pactbroker.host'.", pactBrokerHost))
}
if (port.isNotEmpty() && !port!!.matches(Regex("^[0-9]+"))) {
throw IllegalArgumentException(String.format("Invalid pact broker port specified ('%s'). " +
"Please provide a valid port number or specify the system property 'pactbroker.port'.", pactBrokerPort))
}
return if (scheme == null) {
PactBrokerSource(host, port, pacts = pacts)
} else {
PactBrokerSource(host, port, scheme, pacts)
}
}
private fun getUrlForProvider(providerName: String, tag: String, pactBrokerClient: PactBrokerClient): String {
return try {
pactBrokerClient.getUrlForProvider(providerName, tag) ?: "Unknown"
} catch (e: Exception) {
logger.debug(e) { "Failed to get provider URL from the pact broker" }
"Unknown"
}
}
open fun loadPact(consumer: ConsumerInfo, options: Map): Pact {
val pact = pactReader.loadPact(consumer.pactSource!!, options) as Pact
val pactConsumer = consumer.toPactConsumer()
val pactList = pacts.getOrDefault(pactConsumer, mutableListOf())
pactList.add(pact)
pacts[pactConsumer] = pactList
return pact
}
open fun newPactBrokerClient(url: URI, resolver: ValueResolver): PactBrokerClient {
if (authentication == null || authentication!!.scheme.equals("none", ignoreCase = true)) {
logger.debug { "Authentication: None" }
return PactBrokerClient(url.toString(), emptyMap())
}
val scheme = parseExpression(authentication!!.scheme, resolver)
if (scheme.isNotEmpty()) {
// Legacy behavior (before support for bearer token was added):
// If scheme was not explicitly set, basic was always used.
// If it was explicitly set, the given value was used together with username and password
val schemeToUse = if (scheme.equals("legacy")) "basic" else scheme
logger.debug { "Authentication: $schemeToUse" }
val options = mapOf("authentication" to listOf(schemeToUse,
parseExpression(authentication!!.username, resolver),
parseExpression(authentication!!.password, resolver)))
return PactBrokerClient(url.toString(), options)
}
// Check if username is set. If yes, use basic auth.
val username = parseExpression(authentication!!.username, resolver)
if (username.isNotEmpty()) {
logger.debug { "Authentication: Basic" }
val options = mapOf("authentication" to listOf("basic", username,
parseExpression(authentication!!.password, resolver)))
return PactBrokerClient(url.toString(), options)
}
// Check if token is set. If yes, use bearer auth.
val token = parseExpression(authentication!!.token, resolver)
if (token.isNotEmpty()) {
logger.debug { "Authentication: Bearer" }
val options = mapOf("authentication" to listOf("bearer", token))
return PactBrokerClient(url.toString(), options)
}
throw IllegalArgumentException("Invalid pact authentication specified. Either username or token must be set.")
}
companion object : KLogging()
}