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

au.com.dius.pact.provider.ProviderVerifier.groovy 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. ### Running Pacts Main takes 2 arguments: The first is the root folder of your pact files (all .json files in root and subfolders are assumed to be pacts) The second is the location of your pact config json file. ### Pact config The pact config is a simple mapping of provider names to endpoint url's paths will be appended to endpoint url's when interactions are attempted for an example see: https://github.com/DiUS/pact-jvm/blob/master/pact-jvm-provider/src/test/resources/pact-config.json ### 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 java, junit 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) ```java public class PactJVMProviderJUnitTest { @ClassRule public static TestRule startServiceRule = new DropwizardAppRule<DropwizardAppConfig>(DropwizardApp.class, "config.yml"); private static ProviderInfo serviceProvider; private static Pact testConsumerPact; @BeforeClass public static void setupProvider() { serviceProvider = new ProviderInfo("Dropwizard App"); serviceProvider.setProtocol("http"); serviceProvider.setHost("localhost"); serviceProvider.setPort(8080); serviceProvider.setPath("/"); ConsumerInfo consumer = new ConsumerInfo(); consumer.setName("test_consumer"); consumer.setPactFile(new File("target/pacts/ping_client-ping_service.json")); // serviceProvider.getConsumers().add(consumer); testConsumerPact = (Pact) new PactReader().loadPact(consumer.getPactFile()); } @Test @SuppressWarnings("unchecked") public void runConsumerPacts() { //grab the first interaction from the pact with consumer List<Interaction> interactions = scala.collection.JavaConversions.seqAsJavaList(testConsumerPact.interactions()); Interaction interaction1 = interactions.get(0); //setup any provider state //setup the client and interaction to fire against the provider ProviderClient client = new ProviderClient(); client.setProvider(serviceProvider); client.setRequest(interaction1.request()); Map<String, Object> clientResponse = (Map<String, Object>) client.makeRequest(); Map<String, Object> result = (Map<String, Object>) ResponseComparison.compareResponse(interaction1.response(), clientResponse, (int) clientResponse.get("statusCode"), (Map) clientResponse.get("headers"), (String) clientResponse.get("data")); //assert all good assertThat(result.get("method"), is(true)); // method type matches Map headers = (Map) result.get("headers"); //headers match headers.forEach( (k, v) -> assertThat(format("Header: [%s] does not match", k), v, org.hamcrest.Matchers.equalTo(true)) ); assertThat((Collection<Object>)((Map)result.get("body")).values(), org.hamcrest.Matchers.hasSize(0)); // empty list of body mismatches } } ``` ### 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 PactJVMProviderSpockSpec extends Specification { @ClassRule @Shared TestRule startServiceRule = new DropwizardAppRule<DropwizardAppConfig>(DropwizardApp.class, "config.yml"); @Shared ProviderInfo serviceProvider @Shared Pact testConsumerPact def setupSpec() { serviceProvider = new ProviderInfo("Dropwizard App") serviceProvider.protocol = "http" serviceProvider.host = "localhost" serviceProvider.port = 8080; serviceProvider.path = "/" def consumer = serviceProvider.hasPactWith("ping_consumer", { pactFile = new File('target/pacts/ping_client-ping_service.json') }) testConsumerPact = (Pact) new PactReader().loadPact(consumer.getPactFile()); } def cleanup() { //cleanup provider state //ie. db.truncateAllTables() } def cleanupSpec() { //cleanup provider } @Unroll def "Provider Pact - With Consumer"() { given: //setup provider state // ie. db.setupRecords() // serviceProvider.requestFilter = { req -> // req.addHeader('Authorization', token) // } when: ProviderClient client = new ProviderClient(provider: serviceProvider, request: interaction.request()) Map clientResponse = (Map) client.makeRequest() Map result = (Map) ResponseComparison.compareResponse(interaction.response(), clientResponse, clientResponse.statusCode, clientResponse.headers, clientResponse.data) then: // method matches result.method == true // headers all match, spock needs the size checked before // asserting each result if (result.headers.size() > 0) { result.headers.each() { k, v -> assert v == true } } // empty list of body mismatches result.body.size() == 0 where: interaction << scala.collection.JavaConversions.seqAsJavaList(testConsumerPact.interactions()) } } ```

The newest version!
package au.com.dius.pact.provider

import au.com.dius.pact.model.OptionalBody
import au.com.dius.pact.model.PactReader
import au.com.dius.pact.model.Response
import au.com.dius.pact.model.v3.messaging.Message
import au.com.dius.pact.model.v3.messaging.MessagePact
import au.com.dius.pact.provider.reporters.AnsiConsoleReporter
import groovy.util.logging.Slf4j
import org.apache.commons.lang3.StringUtils
import org.reflections.Reflections
import org.reflections.scanners.MethodAnnotationsScanner
import org.reflections.util.ConfigurationBuilder
import org.reflections.util.FilterBuilder
import scala.Function1

import java.lang.reflect.Method

/**
 * Verifies the providers against the defined consumers in the context of a build plugin
 */
@Slf4j
class ProviderVerifier {

  static final String PACT_FILTER_CONSUMERS = 'pact.filter.consumers'
  static final String PACT_FILTER_DESCRIPTION = 'pact.filter.description'
  static final String PACT_FILTER_PROVIDERSTATE = 'pact.filter.providerState'
  static final String PACT_SHOW_STACKTRACE = 'pact.showStacktrace'
  static final String PACT_SHOW_FULLDIFF = 'pact.showFullDiff'

  def projectHasProperty = { }
  def projectGetProperty = { }
  def pactLoadFailureMessage
  def isBuildSpecificTask = { }
  def executeBuildSpecificTask = { }
  def projectClasspath = { }
  def reporters = [ new AnsiConsoleReporter() ]
  def providerMethodInstance = { Method m -> m.declaringClass.newInstance() }
  def providerVersion = { }

  Map verifyProvider(ProviderInfo provider) {
    Map failures = [:]

    initialiseReporters(provider)

    def consumers = provider.consumers.findAll(this.&filterConsumers)
    if (consumers.empty) {
      reporters.each { it.warnProviderHasNoConsumers(provider) }
    }
    consumers.each(this.&runVerificationForConsumer.curry(failures, provider))

    failures
  }

  void initialiseReporters(ProviderInfo provider) {
    reporters.each {
      if (it.hasProperty('displayFullDiff')) {
        it.displayFullDiff = callProjectHasProperty(PACT_SHOW_FULLDIFF)
      }
      it.initialise(provider)
    }
  }

  void runVerificationForConsumer(Map failures, ProviderInfo provider, ConsumerInfo consumer) {
    reportVerificationForConsumer(consumer, provider)
    def pact = loadPactFileForConsumer(consumer)
    forEachInteraction(pact, this.&verifyInteraction.curry(provider, consumer, failures))
  }

  void reportVerificationForConsumer(ConsumerInfo consumer, ProviderInfo provider) {
    reporters.each { it.reportVerificationForConsumer(consumer, provider) }
  }

  List interactions(def pact) {
    if (pact instanceof MessagePact) {
      pact.messages.findAll(this.&filterInteractions)
    } else {
      pact.interactions.findAll(this.&filterInteractions)
    }
  }

  void forEachInteraction(def pact, Closure verifyInteraction) {
    List interactions = interactions(pact)
    if (interactions.empty) {
      reporters.each { it.warnPactFileHasNoInteractions(pact) }
    } else {
      interactions.each(verifyInteraction)
    }
  }

  @SuppressWarnings('ThrowRuntimeException')
  def loadPactFileForConsumer(ConsumerInfo consumer) {
    def pactFile = consumer.pactFile
    if (pactFile instanceof Closure) {
      pactFile = pactFile.call()
    }

    if (pactFile instanceof URL) {
      reporters.each { it.verifyConsumerFromUrl(pactFile, consumer) }
      def options = [:]
      if (consumer.pactFileAuthentication) {
        options.authentication = consumer.pactFileAuthentication
      }
      PactReader.loadPact(options, pactFile)
    } else if (pactFile instanceof File || ProviderUtils.pactFileExists(pactFile) || ProviderUtils.isS3Url(pactFile)) {
      reporters.each { it.verifyConsumerFromFile(pactFile, consumer) }
      PactReader.loadPact(pactFile)
    } else {
      String message = generateLoadFailureMessage(consumer)
      reporters.each { it.pactLoadFailureForConsumer(consumer, message) }
      throw new RuntimeException(message)
    }
  }

  private generateLoadFailureMessage(ConsumerInfo consumer) {
    if (pactLoadFailureMessage instanceof Closure) {
      pactLoadFailureMessage.call(consumer) as String
    } else if (pactLoadFailureMessage instanceof Function1) {
      pactLoadFailureMessage.apply(consumer) as String
    } else {
      pactLoadFailureMessage as String
    }
  }

  boolean filterConsumers(def consumer) {
    !callProjectHasProperty(PACT_FILTER_CONSUMERS) ||
      consumer.name in callProjectGetProperty(PACT_FILTER_CONSUMERS).split(',')*.trim()
  }

  boolean filterInteractions(def interaction) {
    if (callProjectHasProperty(PACT_FILTER_DESCRIPTION) && callProjectHasProperty(PACT_FILTER_PROVIDERSTATE)) {
      matchDescription(interaction) && matchState(interaction)
    } else if (callProjectHasProperty(PACT_FILTER_DESCRIPTION)) {
      matchDescription(interaction)
    } else if (callProjectHasProperty(PACT_FILTER_PROVIDERSTATE)) {
      matchState(interaction)
    } else {
      true
    }
  }

  private boolean matchState(interaction) {
    if (interaction.providerState) {
      interaction.providerState ==~ callProjectGetProperty(PACT_FILTER_PROVIDERSTATE)
    } else {
      callProjectGetProperty(PACT_FILTER_PROVIDERSTATE).empty
    }
  }

  private boolean matchDescription(interaction) {
    interaction.description ==~ callProjectGetProperty(PACT_FILTER_DESCRIPTION)
  }

  void verifyInteraction(ProviderInfo provider, ConsumerInfo consumer, Map failures, def interaction) {
    def interactionMessage = "Verifying a pact between ${consumer.name} and ${provider.name}" +
      " - ${interaction.description}"

    def stateChangeOk = true
    if (interaction.providerState) {
      stateChangeOk = stateChange(interaction.providerState, provider, consumer)
      log.debug "State Change: \"${interaction.providerState}\" -> ${stateChangeOk}"
      if (stateChangeOk != true) {
        failures[interactionMessage] = stateChangeOk
        stateChangeOk = false
      } else {
        interactionMessage += " Given ${interaction.providerState}"
      }
    }

    if (stateChangeOk) {
      reportInteractionDescription(interaction)

      if (ProviderUtils.verificationType(provider, consumer) == PactVerification.REQUST_RESPONSE) {
        log.debug('Verifying via request/response')
        verifyResponseFromProvider(provider, interaction, interactionMessage, failures)
      } else {
        log.debug('Verifying via annotated test method')
        verifyResponseByInvokingProviderMethods(provider, consumer, interaction, interactionMessage, failures)
      }

      if (provider.stateChangeTeardown) {
        stateChange(interaction.providerState, provider, consumer, false)
      }
    }
  }

  void reportInteractionDescription(interaction) {
    reporters.each { it.interactionDescription(interaction) }
  }

  def stateChange(String state, ProviderInfo provider, ConsumerInfo consumer, boolean isSetup = true) {
    reportStateForInteraction(state, provider, consumer, isSetup)
    try {
      def stateChangeHandler = consumer.stateChange
      def stateChangeUsesBody = consumer.stateChangeUsesBody
      if (stateChangeHandler == null) {
        stateChangeHandler = provider.stateChangeUrl
        stateChangeUsesBody = provider.stateChangeUsesBody
      }
      if (stateChangeHandler == null || (stateChangeHandler instanceof String
        && StringUtils.isBlank(stateChangeHandler))) {
        reporters.each { it.warnStateChangeIgnored(state, provider, consumer) }
        return true
      } else if (stateChangeHandler instanceof Closure) {
        def result
        if (provider.stateChangeTeardown) {
          result = stateChangeHandler.call(state, isSetup ? 'setup' : 'teardown')
        } else {
          result = stateChangeHandler.call(state)
        }
        log.debug "Invoked state change closure -> ${result}"
        if (!(result instanceof URL)) {
          return result
        }
        stateChangeHandler = result
      } else if (isBuildSpecificTask(stateChangeHandler)) {
        log.debug "Invokeing build specific task ${stateChangeHandler}"
        executeBuildSpecificTask(stateChangeHandler, state)
        return true
      }
      return executeHttpStateChangeRequest(stateChangeHandler, stateChangeUsesBody, state, provider, isSetup)
    } catch (e) {
      reporters.each {
        it.stateChangeRequestFailedWithException(state, provider, consumer, isSetup, e,
          callProjectHasProperty(PACT_SHOW_STACKTRACE))
      }
      return e
    }
  }

  void reportStateForInteraction(String state, ProviderInfo provider, ConsumerInfo consumer, boolean isSetup) {
    reporters.each { it.stateForInteraction(state, provider, consumer, isSetup) }
  }

  private executeHttpStateChangeRequest(stateChangeHandler, useBody, String state, ProviderInfo provider,
                                        boolean isSetup) {
    try {
      def url = stateChangeHandler instanceof URI ? stateChangeHandler
        : new URI(stateChangeHandler.toString())
      ProviderClient client = new ProviderClient(provider: provider)
      def response = client.makeStateChangeRequest(url, state, useBody, isSetup, provider.stateChangeTeardown)
      log.debug "Invoked state change $url -> ${response?.statusLine}"
      if (response) {
        try {
          if (response.statusLine.statusCode >= 400) {
            reporters.each { it.stateChangeRequestFailed(state, provider, isSetup, response.statusLine.toString()) }
            return 'State Change Request Failed - ' + response.statusLine.toString()
          }
        } finally {
          response.close()
        }
      }
    } catch (URISyntaxException ex) {
      reporters.each { it.warnStateChangeIgnoredDueToInvalidUrl(state, provider, isSetup, stateChangeHandler) }
    }
    true
  }

  void verifyResponseFromProvider(ProviderInfo provider, def interaction, String interactionMessage, Map failures) {
    try {
      ProviderClient client = new ProviderClient(request: interaction.request, provider: provider)

      def expectedResponse = interaction.response
      def actualResponse = client.makeRequest()

      verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, failures)
    } catch (e) {
      failures[interactionMessage] = e
      reporters.each {
        it.requestFailed(provider, interaction, interactionMessage, e, callProjectHasProperty(PACT_SHOW_STACKTRACE))
      }
    }
  }

  void verifyRequestResponsePact(Response expectedResponse, Map actualResponse, String interactionMessage,
                                 Map failures) {
    def comparison = ResponseComparison.compareResponse(expectedResponse, actualResponse,
      actualResponse.statusCode, actualResponse.headers, actualResponse.data)

    reporters.each { it.returnsAResponseWhich() }

    def s = ' returns a response which'
    displayStatusResult(failures, expectedResponse.status, comparison.method, interactionMessage + s)
    displayHeadersResult(failures, expectedResponse.headers, comparison.headers, interactionMessage + s)
    displayBodyResult(failures, comparison.body, interactionMessage + s)
  }

  void displayStatusResult(Map failures, int status, def comparison, String comparisonDescription) {
    if (comparison == true) {
      reporters.each { it.statusComparisonOk(status) }
    } else {
      reporters.each { it.statusComparisonFailed(status, comparison) }
      failures["$comparisonDescription has status code $status"] = comparison
    }
  }

  void displayHeadersResult(Map failures, def expected, Map comparison, String comparisonDescription) {
    if (!comparison.isEmpty()) {
      reporters.each { it.includesHeaders() }
      Map expectedHeaders = expected
      comparison.each { key, headerComparison ->
        def expectedHeaderValue = expectedHeaders[key]
        if (headerComparison == true) {
          reporters.each { it.headerComparisonOk(key, expectedHeaderValue) }
        } else {
          reporters.each { it.headerComparisonFailed(key, expectedHeaderValue, headerComparison) }
          failures["$comparisonDescription includes headers \"$key\" with value \"$expectedHeaderValue\""] =
            headerComparison
        }
      }
    }
  }

  void displayBodyResult(Map failures, def comparison, String comparisonDescription) {
    if (comparison.isEmpty()) {
      reporters.each { it.bodyComparisonOk() }
    } else {
      reporters.each { it.bodyComparisonFailed(comparison) }
      failures["$comparisonDescription has a matching body"] = comparison
    }
  }

  @SuppressWarnings(['ThrowRuntimeException', 'ParameterCount'])
  void verifyResponseByInvokingProviderMethods(ProviderInfo providerInfo, ConsumerInfo consumer,
                                               def interaction, String interactionMessage, Map failures) {
    try {
      def urls = projectClasspath()
      URLClassLoader loader = new URLClassLoader(urls, GroovyObject.classLoader)
      def configurationBuilder = new ConfigurationBuilder()
        .setScanners(new MethodAnnotationsScanner())
        .addClassLoader(loader)
        .addUrls(loader.URLs)

      def scan = ProviderUtils.packagesToScan(providerInfo, consumer)
      if (!scan.empty) {
        def filterBuilder = new FilterBuilder()
        scan.each { filterBuilder.include(it) }
        configurationBuilder.filterInputsBy(filterBuilder)
      }

      Reflections reflections = new Reflections(configurationBuilder)
      def methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(PactVerifyProvider)
      def providerMethods = methodsAnnotatedWith.findAll { Method m ->
        log.debug("Found annotated method $m")
        def annotation = m.annotations.find { it.annotationType().toString() == PactVerifyProvider.toString() }
        log.debug("Found annotation $annotation")
        annotation?.value() == interaction.description
      }

      if (providerMethods.empty) {
        reporters.each { it.errorHasNoAnnotatedMethodsFoundForInteraction(interaction) }
        throw new RuntimeException('No annotated methods were found for interaction ' +
          "'${interaction.description}'")
      } else {
        if (interaction instanceof Message) {
          verifyMessagePact(providerMethods, interaction as Message, interactionMessage, failures)
        } else {
          def expectedResponse = interaction.response
          providerMethods.each {
            def actualResponse = invokeProviderMethod(it)
            verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, failures)
          }
        }
      }
    } catch (e) {
      failures[interactionMessage] = e
      reporters.each { it.verificationFailed(interaction, e, callProjectHasProperty(PACT_SHOW_STACKTRACE)) }
    }
  }

  boolean callProjectHasProperty(String property) {
    if (projectHasProperty instanceof Function1) {
      projectHasProperty.apply(property)
    } else {
      projectHasProperty(property)
    }
  }

  String callProjectGetProperty(String property) {
    if (projectGetProperty instanceof Function1) {
      projectGetProperty.apply(property)
    } else {
      projectGetProperty(property)
    }
  }

  void verifyMessagePact(Set methods, Message message, String interactionMessage, Map failures) {
    methods.each {
      reporters.each { it.generatesAMessageWhich() }
      def actualMessage = OptionalBody.body(invokeProviderMethod(it, providerMethodInstance(it)) as String)
      def comparison = ResponseComparison.compareMessage(message, actualMessage)
      def s = ' generates a message which'
      displayBodyResult(failures, comparison, interactionMessage + s)
    }
  }

  @SuppressWarnings('ThrowRuntimeException')
  static invokeProviderMethod(Method m, Object instance) {
    try {
      m.invoke(instance)
    } catch (e) {
      throw new RuntimeException("Failed to invoke provider method '${m.name}'", e)
    }
  }

  void displayFailures(Map failures) {
    reporters.each { it.displayFailures(failures) }
  }

  void finialiseReports() {
    reporters.each { it.finaliseReport() }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy