All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.com.dius.pact.provider.ProviderClient.kt Maven / Gradle / Ivy

Go to download

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

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.pactbroker.PactResult
import au.com.dius.pact.core.pactbroker.VerificationNotice
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.nio.charset.UnsupportedCharsetException
import java.util.concurrent.Callable
import java.util.function.Consumer
import java.util.function.Function
import java.util.function.Supplier
import au.com.dius.pact.core.model.ContentType as PactContentType
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
  var notices: 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(),
  override var notices: 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
      )

    fun from(consumer: PactResult) =
      ConsumerInfo(name = consumer.name,
        pactSource = BrokerUrlSource(url = consumer.source, pactBrokerUrl = consumer.pactBrokerUrl),
        pactFileAuthentication = consumer.pactFileAuthentication, notices = consumer.notices
      )
  }
}

/**
 * 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

    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.isPresent()) {
      val contentTypeHeader = request.contentTypeHeader()
      if (null != contentTypeHeader) {
        try {
          val contentType = ContentType.parse(contentTypeHeader)
          method.entity = StringEntity(request.body.valueAsString(), contentType)
        } catch (e: UnsupportedCharsetException) {
          method.entity = StringEntity(request.body.valueAsString())
        }
      } else {
        method.entity = StringEntity(request.body.valueAsString())
      }
    }
  }

  open fun setupHeaders(request: Request, method: HttpRequest) {
    val headers = request.headers
    if (headers.isNotEmpty()) {
      headers.forEach { (key, value) ->
        method.addHeader(key, value.joinToString(", "))
      }
    }

    if (!method.containsHeader(CONTENT_TYPE) && request.body.isPresent()) {
      val contentType = when (request.body.contentType) {
        PactContentType.UNKNOWN -> "text/plain; charset=ISO-8859-1"
        else -> request.body.contentType.toString()
      }
      method.addHeader(CONTENT_TYPE, contentType)
    }
  }

  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.toString())
        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)
          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.split(',').map { it.trim() } })
      .mapValues { it.value.flatten() }

    val entity = httpResponse.entity
    if (entity != null) {
      val contentType = if (entity.contentType != null) {
        ContentType.parse(entity.contentType.value)
      } else {
        ContentType.TEXT_PLAIN
      }
      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
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy