au.com.dius.pact.server.Publish.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pact-jvm-server Show documentation
Show all versions of pact-jvm-server Show documentation
Pact server
===========
The pact server is a stand-alone interactions recorder and verifier, aimed at clients that are non-JVM or non-Ruby based.
The pact client for that platform will need to be implemented, but it only be responsible for generating the `JSON`
interactions, running the tests and communicating with the server.
The server implements a `JSON` `REST` Admin API with the following endpoints.
/ -> For diagnostics, currently returns a list of ports of the running mock servers.
/create -> For initialising a test server and submitting the JSON interactions. It returns a port
/complete -> For finalising and verifying the interactions with the server. It writes the `JSON` pact file to disk.
/publish -> For publishing contracts. It takes a contract from disk and publishes it to the configured broker
## Running the server
Pact server takes the following parameters:
```
Usage: pact-jvm-server [options] [port]
port
port to run on (defaults to 29999)
--help
prints this usage text
-h <value> | --host <value>
host to bind to (defaults to localhost)
-l <value> | --mock-port-lower <value>
lower bound to allocate mock ports (defaults to 20000)
-u <value> | --mock-port-upper <value>
upper bound to allocate mock ports (defaults to 40000)
-d | --daemon
run as a daemon process
-v <value> | --pact-version <value>
pact version to generate for (2 or 3)
-k <value> | --keystore-path <value>
Path to keystore
-p <value> | --keystore-password <value>
Keystore password
-s <value> | --ssl-port <value>
Ssl port the mock server should run on. lower and upper bounds are ignored
-b <value> | --broker <value>
The baseUrl of the broker to publish contracts to (for example https://organization.broker.com
-t <value | --token <value>
API token for authentication to the pact broker
--debug
run with debug logging
```
### Using trust store
Trust store can be used. However, it is limited to a single port for the time being.
### Using a distribution archive
You can download a [distribution from maven central](http://search.maven.org/remotecontent?filepath=au/com/dius/pact/pact-jvm-server/4.1.0/).
There is both a ZIP and TAR archive. Unpack it to a directory of choice and then run the script in the bin directory.
### Building a distribution bundle
You can build an application bundle with gradle by running:
$ ./gradlew :pact-jvm-server:installdist
This will create an app bundle in `build/install/pact-jvm-server`. You can then execute it with:
$ java -jar pact-jvm-server/build/install/pact-jvm-server/lib/pact-jvm-server-4.0.1.jar
or with the generated bundle script file:
$ pact-jvm-server/build/install/pact-jvm-server/bin/pact-jvm-server
By default will run on port `29999` but a port number can be optionally supplied.
### Running it with docker
You can use a docker image to execute the mock server as a docker container.
$ docker run -d -p 8080:8080 -p 20000-20010:20000-20010 uglyog/pact-jvm-server
This will run the main server on port 8080, and each created mock server on ports 20000-20010. You can map the ports to
any you require.
## Life cycle
The following actions are expected to occur
* The client calls `/create` to initialise a server with the expected `JSON` interactions and state
* The admin server will start a mock server on a random port and return the port number in the response
* The client will execute its interaction tests against the mock server with the supplied port
* Once finished, the client will call `/complete' on the Admin API, posting the port number
* The pact server will verify the interactions and write the `JSON` `pact` file to disk under `/target`
* The mock server running on the supplied port will be shutdown.
* The client will call `/publish` to publish the created contract to the configured pact broker
## Endpoints
### /create
The client will need `POST` to `/create` the generated `JSON` interactions, also providing a state as a query parameter
and a path.
For example:
POST http://localhost:29999/create?state=NoUsers&path=/sub/ref/path '{ "provider": { "name": "Animal_Service"}, ... }'
This will create a new running mock service provider on a randomly generated port. The port will be returned in the
`201` response:
{ "port" : 34423 }
But you can also reference the path from `/sub/ref/path` using the server port. The service will not strip
the prefix path, but instead will use it as a differentiator. If your services do not have differences
in the prefix of their path, then you will have to use the port method.
### /complete
Once the client has finished running its tests against the mock server on the supplied port (in this example port
`34423`) the client will need to `POST` to `/complete` the port number of the mock server that was used.
For example:
POST http://localhost:29999/complete '{ "port" : 34423 }'
This will cause the Pact server to verify the interactions, shutdown the mock server running on that port and writing
the pact `JSON` file to disk under the `target` directory.
### /publish
Once all interactions have been tested the `/publish` endpoint can be called to publish the created pact to the pact broker
For this it is required to run the pact-jvm-server with the -b parameter to configure the pact broker to publish the pacts to.
Optionaly an authentication token can be used for authentication against the broker.
For example:
POST http://localhost:29999/publish '{ "consumer": "Zoo", "consumerVersion": "0.0.1", "provider": "Animal_Service" }'
This will cause the Pact server to check for the pact `Zoo-Animal_Service.json` on disk under `target` and publish it to
the configured pact broker. After a successful publish the pact will be removed from disk.
### /
The `/` endpoint is for diagnostics and to check that the pact server is running. It will return all the currently
running mock servers port numbers.
For example:
GET http://localhost:29999/
'{ "ports": [23443,43232] }'
package au.com.dius.pact.server
import au.com.dius.pact.core.model.{OptionalBody, Request, Response}
import com.typesafe.scalalogging.StrictLogging
import scala.collection.JavaConverters._
import java.io.{File, IOException}
import au.com.dius.pact.core.pactbroker.{PactBrokerClient, PactBrokerClientConfig, RequestFailedException}
object Publish extends StrictLogging {
def apply(request: Request, oldState: ServerState, config: Config): Result = {
val jsonBody = JsonUtils.parseJsonString(request.getBody.valueAsString())
val consumer: Option[String] = getVarFromJson("consumer", jsonBody)
val consumerVersion: Option[String] = getVarFromJson("consumerVersion", jsonBody)
val provider: Option[String] = getVarFromJson("provider", jsonBody)
val tags: Option[::[String]] = getListFromJson("tags", jsonBody)
val broker: Option[String] = getBrokerUrlFromConfig(config)
val authToken: Option[String] = getVarFromConfig(config.authToken)
var response = new Response(500, ResponseUtils.CrossSiteHeaders.asJava)
if (broker.isDefined) {
if (consumer.isDefined && consumerVersion.isDefined && provider.isDefined) {
response = publishPact(consumer.get, consumerVersion.get, provider.get, broker.get, authToken, tags)
} else {
def errorJson: String = "{\"error\": \"body should contain consumer, consumerVersion and provider.\"}"
def body: OptionalBody = OptionalBody.body(errorJson.getBytes())
response = new Response(400, ResponseUtils.CrossSiteHeaders.asJava, body)
}
} else {
def errorJson: String = "{\"error\" : \"Broker url not correctly configured please run server with -b or --broker 'http://pact-broker.adomain.com' option\" }"
def body: OptionalBody = OptionalBody.body(errorJson.getBytes())
response = new Response(500, ResponseUtils.CrossSiteHeaders.asJava, body)
}
Result(response, oldState)
}
private def publishPact(consumer: String, consumerVersion: String, provider: String, broker: String, authToken: Option[String], tags: Option[::[String]]) = {
val fileName: String = s"$consumer-$provider.json"
val pact = new File(s"${System.getProperty("pact.rootDir", "target/pacts")}/$fileName")
logger.debug("Publishing pact with following details: ")
logger.debug("Consumer: " + consumer)
logger.debug("ConsumerVersion: " + consumerVersion)
logger.debug("Provider: " + provider)
logger.debug("Pact Broker: " + broker)
logger.debug("Tags: " + tags.getOrElse(None))
try {
val options = getOptions(authToken)
val brokerClient: PactBrokerClient = new PactBrokerClient(broker, options.asJava, new PactBrokerClientConfig())
val res = brokerClient.uploadPactFile(pact, consumerVersion, tags.getOrElse(List()).asJava)
if (res.errorValue() == null) {
logger.debug("Pact successfully shared. deleting file..")
removePact(pact)
new Response(200, ResponseUtils.CrossSiteHeaders.asJava, OptionalBody.body(res.get().getBytes()))
} else {
new Response(500, ResponseUtils.CrossSiteHeaders.asJava, OptionalBody.body(res.errorValue().getLocalizedMessage.getBytes()))
}
} catch {
case e: IOException => new Response(500, ResponseUtils.CrossSiteHeaders.asJava, OptionalBody.body(s"""{"error": "Got IO Exception while reading file. ${e.getMessage}"}""".getBytes()))
case e: RequestFailedException => new Response(e.getStatus, ResponseUtils.CrossSiteHeaders.asJava, OptionalBody.body(e.getBody.getBytes()))
case t: Throwable => new Response(500, ResponseUtils.CrossSiteHeaders.asJava, OptionalBody.body(t.getMessage.getBytes()))
}
}
private def getOptions(authToken: Option[String]): Map[String, Object] = {
var options: Map[String, Object]= Map()
if(authToken.isDefined) {
options = Map("authentication" -> List("bearer",authToken.get).asJava)
}
options
}
private def removePact(file: File): Unit = {
if (file.exists()) {
file.delete()
}
}
private def getVarFromConfig(variable: String) = {
if (!variable.isEmpty) Some(variable)
else None
}
def getBrokerUrlFromConfig(config: Config): Option[String] = {
if (!config.broker.isEmpty && config.broker.startsWith("http")) Some(config.broker)
else None
}
private def getVarFromJson(variable: String, json: Any): Option[String] = json match {
case map: Map[AnyRef, AnyRef] => {
if (map.contains(variable)) Some(map(variable).toString)
else None
}
case _ => None
}
private def getListFromJson(variable: String, json: Any): Option[::[String]] = json match {
case map: Map[AnyRef, AnyRef] => {
if (map.contains(variable)) Some(map(variable).asInstanceOf[::[String]])
else None
}
case _ => None
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy