au.com.dius.pact.provider.ProviderClient.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
}
}
```
package au.com.dius.pact.provider
import au.com.dius.pact.core.model.BrokerUrlSource
import au.com.dius.pact.core.model.ClosurePactSource
import au.com.dius.pact.core.model.FileSource
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.model.ProviderState
import au.com.dius.pact.core.model.Request
import au.com.dius.pact.core.model.UrlSource
import au.com.dius.pact.core.pactbroker.PactBrokerConsumer
import au.com.dius.pact.core.support.Json
import groovy.lang.Binding
import groovy.lang.Closure
import groovy.lang.GroovyShell
import mu.KLogging
import org.apache.http.HttpEntityEnclosingRequest
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpHead
import org.apache.http.client.methods.HttpOptions
import org.apache.http.client.methods.HttpPatch
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.client.methods.HttpTrace
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.client.utils.URIBuilder
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.util.EntityUtils
import scala.Function1
import java.io.File
import java.lang.Boolean.getBoolean
import java.net.URI
import java.net.URL
import java.net.URLDecoder
import java.util.concurrent.Callable
import java.util.function.Consumer
import java.util.function.Function
import java.util.function.Supplier
interface IHttpClientFactory {
fun newClient(provider: IProviderInfo): CloseableHttpClient
}
interface IProviderInfo {
var protocol: String
var host: Any?
var port: Any?
var path: String
var name: String
val requestFilter: Any?
val stateChangeRequestFilter: Any?
val stateChangeUrl: URL?
val stateChangeUsesBody: Boolean
val stateChangeTeardown: Boolean
var packagesToScan: List
var verificationType: PactVerification?
var createClient: Any?
var insecure: Boolean
var trustStore: File?
var trustStorePassword: String?
}
interface IConsumerInfo {
var name: String
var stateChange: Any?
var stateChangeUsesBody: Boolean
var packagesToScan: List
var verificationType: PactVerification?
var pactSource: Any?
var pactFileAuthentication: List
}
open class ConsumerInfo @JvmOverloads constructor (
override var name: String = "",
override var stateChange: Any? = null,
override var stateChangeUsesBody: Boolean = true,
override var packagesToScan: List = emptyList(),
override var verificationType: PactVerification? = null,
override var pactSource: Any? = null,
override var pactFileAuthentication: List = emptyList()
) : IConsumerInfo {
fun toPactConsumer() = au.com.dius.pact.core.model.Consumer(name)
var stateChangeUrl: URL?
get() = if (stateChange != null) URL(stateChange.toString()) else null
set(value) { stateChange = value }
/**
* Sets the Pact File for the consumer
* @param file Pact file, either as a string or a PactSource
* @deprecated Use setPactSource instead, this will be removed in 4.0
*/
@Deprecated(
message = "Use setPactSource instead, this will be removed in 4.0",
replaceWith = ReplaceWith("setPactSource")
)
fun setPactFile(file: Any) {
pactSource = when (file) {
is PactSource -> file
is Supplier<*> -> ClosurePactSource(file as Supplier)
is Closure<*> -> ClosurePactSource(Supplier { file.call() })
is URL -> UrlSource(file.toString())
else -> FileSource(File(file.toString()))
}
}
/**
* Returns the Pact file for the consumer
* @deprecated Use getPactSource instead, this will be removed in 4.0
*/
@Deprecated(
message = "Use getPactSource instead, this will be removed in 4.0",
replaceWith = ReplaceWith("getPactSource")
)
fun getPactFile() = pactSource
override fun toString(): String {
return "ConsumerInfo(name='$name', stateChange=$stateChange, stateChangeUsesBody=$stateChangeUsesBody, " +
"packagesToScan=$packagesToScan, verificationType=$verificationType, pactSource=$pactSource, " +
"pactFileAuthentication=$pactFileAuthentication)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ConsumerInfo
if (name != other.name) return false
if (stateChange != other.stateChange) return false
if (stateChangeUsesBody != other.stateChangeUsesBody) return false
if (packagesToScan != other.packagesToScan) return false
if (verificationType != other.verificationType) return false
if (pactSource != other.pactSource) return false
if (pactFileAuthentication != other.pactFileAuthentication) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (stateChange?.hashCode() ?: 0)
result = 31 * result + stateChangeUsesBody.hashCode()
result = 31 * result + packagesToScan.hashCode()
result = 31 * result + (verificationType?.hashCode() ?: 0)
result = 31 * result + (pactSource?.hashCode() ?: 0)
result = 31 * result + pactFileAuthentication.hashCode()
return result
}
companion object : KLogging() {
@JvmStatic
fun from(consumer: PactBrokerConsumer) =
ConsumerInfo(name = consumer.name,
pactSource = BrokerUrlSource(url = consumer.source, pactBrokerUrl = consumer.pactBrokerUrl, tag = consumer.tag),
pactFileAuthentication = consumer.pactFileAuthentication
)
}
}
/**
* Client HTTP utility for providers
*/
open class ProviderClient(
val provider: IProviderInfo,
private val httpClientFactory: IHttpClientFactory
) {
companion object : KLogging() {
const val CONTENT_TYPE = "Content-Type"
const val UTF8 = "UTF-8"
const val REQUEST = "request"
const val ACTION = "action"
private fun invokeIfClosure(property: Any?) = if (property is Closure<*>) {
property.call()
} else {
property
}
private fun convertToInteger(port: Any?) = if (port is Number) {
port.toInt()
} else {
Integer.parseInt(port.toString())
}
@JvmStatic
fun urlEncodedFormPost(request: Request) = request.method.toLowerCase() == "post" &&
request.mimeType() == ContentType.APPLICATION_FORM_URLENCODED.mimeType
private fun isFunctionalInterface(requestFilter: Any) =
requestFilter::class.java.interfaces.any { it.isAnnotationPresent(FunctionalInterface::class.java) }
@JvmStatic
private fun stripTrailingSlash(basePath: String): String {
return when {
basePath == "/" -> ""
basePath.isNotEmpty() && basePath.last() == '/' -> basePath.substring(0, basePath.length - 1)
else -> basePath
}
}
}
open fun makeRequest(request: Request): Map {
val httpclient = getHttpClient()
val method = prepareRequest(request)
return executeRequest(httpclient, method)
}
open fun executeRequest(httpclient: CloseableHttpClient, method: HttpUriRequest): Map {
return httpclient.execute(method).use {
handleResponse(it)
}
}
open fun prepareRequest(request: Request): HttpUriRequest {
logger.debug { "Making request for provider $provider:" }
logger.debug { request.toString() }
val method = newRequest(request)
setupHeaders(request, method)
setupBody(request, method)
executeRequestFilter(method)
return method
}
open fun executeRequestFilter(method: HttpRequest) {
val requestFilter = provider.requestFilter
if (requestFilter != null) {
when (requestFilter) {
is Closure<*> -> requestFilter.call(method)
is Function1<*, *> -> (requestFilter as Function1).apply(method)
is org.apache.commons.collections4.Closure<*> ->
(requestFilter as org.apache.commons.collections4.Closure).execute(method)
else -> {
if (isFunctionalInterface(requestFilter)) {
invokeJavaFunctionalInterface(requestFilter, method)
} else {
val binding = Binding()
binding.setVariable(REQUEST, method)
val shell = GroovyShell(binding)
shell.evaluate(requestFilter as String)
}
}
}
}
}
private fun invokeJavaFunctionalInterface(functionalInterface: Any, httpRequest: HttpRequest) {
when (functionalInterface) {
is Consumer<*> -> (functionalInterface as Consumer).accept(httpRequest)
is Function<*, *> -> (functionalInterface as Function).apply(httpRequest)
is Callable<*> -> (functionalInterface as Callable).call()
else -> throw IllegalArgumentException("Java request filters must be either a Consumer or Function that " +
"takes at least one HttpRequest parameter")
}
}
open fun setupBody(request: Request, method: HttpRequest) {
if (method is HttpEntityEnclosingRequest && request.body != null && request.body!!.isPresent()) {
method.entity = StringEntity(request.body.valueAsString())
}
}
open fun setupHeaders(request: Request, method: HttpRequest) {
val headers = request.headers
if (headers != null && headers.isNotEmpty()) {
headers.forEach { key, value ->
method.addHeader(key, value.joinToString(", "))
}
}
if (!method.containsHeader(CONTENT_TYPE) && request.body?.isPresent() == true) {
method.addHeader(CONTENT_TYPE, "application/json")
}
}
open fun makeStateChangeRequest(
stateChangeUrl: Any?,
state: ProviderState,
postStateInBody: Boolean,
isSetup: Boolean,
stateChangeTeardown: Boolean
): CloseableHttpResponse? {
return if (stateChangeUrl != null) {
val httpclient = getHttpClient()
val urlBuilder = if (stateChangeUrl is URI) {
URIBuilder(stateChangeUrl)
} else {
URIBuilder(stateChangeUrl.toString())
}
val method: HttpPost?
if (postStateInBody) {
method = HttpPost(urlBuilder.build())
val map = mutableMapOf("state" to state.name)
if (state.params.isNotEmpty()) {
map["params"] = state.params
}
if (stateChangeTeardown) {
map["action"] = if (isSetup) "setup" else "teardown"
}
method.entity = StringEntity(Json.gsonPretty.toJson(map), ContentType.APPLICATION_JSON)
} else {
urlBuilder.setParameter("state", state.name)
state.params.forEach { (k, v) -> urlBuilder.setParameter(k, v.toString()) }
if (stateChangeTeardown) {
if (isSetup) {
urlBuilder.setParameter(ACTION, "setup")
} else {
urlBuilder.setParameter(ACTION, "teardown")
}
}
method = HttpPost(urlBuilder.build())
}
if (provider.stateChangeRequestFilter != null) {
when {
provider.stateChangeRequestFilter is Closure<*> ->
(provider.stateChangeRequestFilter as Closure<*>).call(method)
provider.stateChangeRequestFilter is Function1<*, *> ->
(provider.stateChangeRequestFilter as Function1).apply(method)
else -> {
val binding = Binding()
binding.setVariable(REQUEST, method)
val shell = GroovyShell(binding)
shell.evaluate(provider.stateChangeRequestFilter.toString())
}
}
}
httpclient.execute(method)
} else {
null
}
}
fun getHttpClient() = httpClientFactory.newClient(provider)
fun handleResponse(httpResponse: HttpResponse): Map {
logger.debug { "Received response: ${httpResponse.statusLine}" }
val response = mutableMapOf("statusCode" to httpResponse.statusLine.statusCode,
"contentType" to ContentType.TEXT_PLAIN)
response["headers"] = httpResponse.allHeaders.groupBy({ header -> header.name }, { header -> header.value })
val entity = httpResponse.entity
if (entity != null) {
val contentType = if (entity.contentType != null) {
ContentType.parse(entity.contentType.value)
} else {
ContentType.APPLICATION_JSON
}
response["contentType"] = contentType
response["data"] = EntityUtils.toString(entity, contentType.charset?.name() ?: UTF8)
}
logger.debug { "Response: $response" }
return response
}
open fun newRequest(request: Request): HttpUriRequest {
val scheme = provider.protocol
val host = invokeIfClosure(provider.host)
val port = convertToInteger(invokeIfClosure(provider.port))
var path = stripTrailingSlash(provider.path)
var urlBuilder = URIBuilder()
if (systemPropertySet("pact.verifier.disableUrlPathDecoding")) {
path += request.path
urlBuilder = URIBuilder("$scheme://$host:$port$path")
} else {
path += URLDecoder.decode(request.path, UTF8)
urlBuilder.scheme = provider.protocol
urlBuilder.host = invokeIfClosure(provider.host)?.toString()
urlBuilder.port = convertToInteger(invokeIfClosure(provider.port))
urlBuilder.path = path
}
if (request.query != null) {
request.query.forEach { entry ->
entry.value.forEach {
urlBuilder.addParameter(entry.key, it)
}
}
}
val url = urlBuilder.build().toString()
return when (request.method.toLowerCase()) {
"post" -> HttpPost(url)
"put" -> HttpPut(url)
"options" -> HttpOptions(url)
"delete" -> HttpDeleteWithEntity(url)
"head" -> HttpHead(url)
"patch" -> HttpPatch(url)
"trace" -> HttpTrace(url)
else -> HttpGet(url)
}
}
open fun systemPropertySet(property: String) = getBoolean(property)
internal class HttpDeleteWithEntity(uri: String) : HttpEntityEnclosingRequestBase() {
init {
setURI(URI.create(uri))
}
override fun getMethod(): String {
return HttpDelete.METHOD_NAME
}
}
}