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

au.com.dius.pact.provider.broker.HalClient.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.broker

import groovy.transform.Canonical
import groovy.util.logging.Slf4j
import groovyx.net.http.RESTClient
import org.apache.http.message.BasicHeaderValueParser
import org.apache.http.util.EntityUtils

import static groovyx.net.http.ContentType.JSON
import static groovyx.net.http.Method.POST
import static groovyx.net.http.Method.PUT

/**
 * HAL client for navigating the HAL links
 */
@Slf4j
@Canonical
@SuppressWarnings('DuplicateStringLiteral')
class HalClient {
  private static final String ROOT = '/'

  def baseUrl
  Map options = [:]
  def http
  private pathInfo
  def lastUrl

  @SuppressWarnings('DuplicateNumberLiteral')
  private void setupHttpClient() {
    if (http == null) {
      http = newHttpClient()
      if (options.authentication instanceof List) {
        switch (options.authentication.first().toLowerCase()) {
          case 'basic':
            if (options.authentication.size() > 2) {
              http.auth.basic(options.authentication[1].toString(), options.authentication[2].toString())
            } else {
              log.warn('Basic authentication requires a username and password, ignoring.')
            }
            break
        }
      } else if (options.authentication) {
        log.warn('Authentication options needs to be a list of values, ignoring.')
      }
    }
  }

  private newHttpClient() {
    http = new RESTClient(baseUrl)
    http.parser.'application/hal+json' = http.parser.'application/json'
    http.handler.'404' = {
      throw new NotFoundHalResponse("404 Not Found response from the pact broker (URL: '${baseUrl}'," +
        " LINK: '${lastUrl}')")
    }
    http
  }

  HalClient navigate(Map options = [:], String link) {
    pathInfo = pathInfo ?: fetch(ROOT)
    pathInfo = fetchLink(link, options)
    this
  }

  private fetchLink(String link, Map options) {
    if (pathInfo == null || pathInfo['_links'] == null) {
      throw new InvalidHalResponse('Expected a HAL+JSON response from the pact broker, but got ' +
        "a response with no '_links'. URL: '${baseUrl}', LINK: '${link}'")
    }

    def linkData = pathInfo.'_links'[link]
    if (linkData == null) {
      throw new InvalidHalResponse("Link '$link' was not found in the response, only the following links where " +
        "found: ${pathInfo['_links'].keySet()}. URL: '${baseUrl}', LINK: '${link}'")
    }

    if (linkData instanceof List) {
      if (options.containsKey('name')) {
        def linkByName = linkData.find { it.name == options.name }
        if (linkByName?.templated) {
          this.fetch(parseLinkUrl(linkByName.href, options))
        } else if (linkByName) {
          this.fetch(linkByName.href)
        } else {
          throw new InvalidNavigationRequest("Link '$link' does not have an entry with name '${options.name}'. " +
            "URL: '${baseUrl}', LINK: '${link}'")
        }
      } else {
        throw new InvalidNavigationRequest("Link '$link' has multiple entries. You need to filter by the link name. " +
          "URL: '${baseUrl}', LINK: '${link}'")
      }
    } else if (linkData.templated) {
      this.fetch(parseLinkUrl(linkData.href, options))
    } else {
      this.fetch(linkData.href)
    }
  }

  static String parseLinkUrl(String linkUrl, Map options) {
    def m = linkUrl =~ /\{(\w+)\}/
    def result = ''
    int index = 0
    while (m.find()) {
      def start = m.start() - 1
      if (start >= index) {
        result += linkUrl[index..start]
      }
      index = m.end()
      def key = m.group(1)
      result += options[key] ?: m.group(0)
    }

    if (index < linkUrl.size()) {
      result += linkUrl[index..-1]
    }
    result
  }

  private fetch(String path) {
    lastUrl = path
    setupHttpClient()
    log.debug "Fetching: $path"
    def response = http.get(path: path, requestContentType: 'application/json',
      headers: [Accept: 'application/hal+json, application/json'])
    def contentType = response.headers.'Content-Type'
    def headerParser = new BasicHeaderValueParser()
    def headerElements = headerParser.parseElements(contentType as String, headerParser)
    if (headerElements[0].name != 'application/json' && headerElements[0].name != 'application/hal+json') {
      throw new InvalidHalResponse('Expected a HAL+JSON response from the pact broker, but got ' +
        "'$contentType'. URL: '${baseUrl}', PATH: '${path}'")
    }
    response.data
  }

  def methodMissing(String name, args) {
    pathInfo = pathInfo ?: fetch(ROOT)
    def matchingLink = pathInfo.'_links'[name]
    if (matchingLink != null) {
      if (args && args.last() instanceof Closure) {
        if (matchingLink instanceof Collection) {
          return matchingLink.each(args.last() as Closure)
        }
        return args.last().call(matchingLink)
      }
      return matchingLink
    }
    throw new MissingMethodException(name, this.class, args)
  }

  String linkUrl(String name) {
    pathInfo.'_links'[name].href
  }

  def uploadJson(String path, String bodyJson, Closure closure = null) {
    setupHttpClient()
    http.request(PUT) {
      uri.path = path
      body = bodyJson
      requestContentType = JSON

      response.success = { resp ->
        consumeEntity(resp)
        closure?.call('OK', resp.statusLine as String)
      }

      response.failure = { resp, body -> handleFailure(resp, body, closure) }

      response.'409' = { resp, body ->
        closure?.call('FAILED', "${resp.statusLine.statusCode} ${resp.statusLine.reasonPhrase} - ${body.readLine()}")
      }
    }
  }

  private consumeEntity(resp) {
    EntityUtils.consume(resp.entity)
  }

  private handleFailure(resp, body, Closure closure) {
    if (body instanceof Reader) {
      closure.call('FAILED', "${resp.statusLine.statusCode} ${resp.statusLine.reasonPhrase} - ${body.readLine()}")
    } else {
      def error = 'Unknown error'
      if (body?.errors instanceof List) {
        error = body.errors.join(', ')
      } else if (body?.errors instanceof Map) {
        error = body.errors.collect { entry -> "${entry.key}: ${entry.value}" }.join(', ')
      }
      closure.call('FAILED', "${resp.statusLine.statusCode} ${resp.statusLine.reasonPhrase} - ${error}")
    }
  }

  def post(String path, Map bodyJson) {
    setupHttpClient()
    http.request(POST) {
      uri.path = path
      body = bodyJson
      requestContentType = JSON

      response.success = { resp -> "SUCCESS - ${resp.statusLine as String}" }

      response.failure = { resp, respBody ->
        log.error("Request failed: $resp.statusLine $respBody")
        "FAILED - ${resp.statusLine as String}"
      }
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy