From ff7a287a1b557f45858f04d597d0f68c179285af Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Wed, 20 Nov 2024 15:37:25 +1100 Subject: [PATCH] chore(pact-jvm-server): Converted Publish to kotlin --- .../kotlin/au/com/dius/pact/server/Publish.kt | 118 ++++++++ .../au/com/dius/pact/server/Publish.scala | 109 -------- .../com/dius/pact/server/RequestRouter.scala | 2 +- .../com/dius/pact/server/PublishSpec.groovy | 257 +++++++++++++++++- 4 files changed, 371 insertions(+), 115 deletions(-) create mode 100644 pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Publish.kt delete mode 100644 pact-jvm-server/src/main/scala/au/com/dius/pact/server/Publish.scala diff --git a/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Publish.kt b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Publish.kt new file mode 100644 index 0000000000..a19ad9ecfd --- /dev/null +++ b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Publish.kt @@ -0,0 +1,118 @@ +package au.com.dius.pact.server + +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.Response +import au.com.dius.pact.core.pactbroker.IPactBrokerClient +import au.com.dius.pact.core.pactbroker.PactBrokerClient +import au.com.dius.pact.core.pactbroker.PactBrokerClientConfig +import au.com.dius.pact.core.pactbroker.RequestFailedException +import au.com.dius.pact.core.support.Json +import au.com.dius.pact.core.support.isNotEmpty +import au.com.dius.pact.core.support.json.JsonParser +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File +import java.io.IOException + +private val logger = KotlinLogging.logger {} + +object Publish { + + private val CrossSiteHeaders = mapOf("Access-Control-Allow-Origin" to listOf("*")) + + @JvmStatic + fun apply(request: Request, oldState: ServerState, config: Config): Result { + val jsonBody = Json.fromJson(JsonParser.parseString(request.body.valueAsString())) + val consumer = getVarFromJson("consumer", jsonBody) + val consumerVersion = getVarFromJson("consumerVersion", jsonBody) + val provider = getVarFromJson("provider", jsonBody) + val tags = getListFromJson("tags", jsonBody) + val broker = getBrokerUrlFromConfig(config) + val authToken = getVarFromConfig(config.authToken) + + var response = Response(500, CrossSiteHeaders.toMutableMap()) + if (broker != null) { + if (consumer != null && consumerVersion != null && provider != null) { + val options = getOptions(authToken) + val brokerClient = PactBrokerClient(broker, options.toMutableMap(), PactBrokerClientConfig()) + response = publishPact(consumer, consumerVersion, provider, broker, brokerClient, tags) + } else { + val errorJson = "{\"error\": \"body should contain consumer, consumerVersion and provider.\"}" + val body = OptionalBody.body(errorJson.toByteArray()) + response = Response(400, CrossSiteHeaders.toMutableMap(), body) + } + } else { + val errorJson = "{\"error\" : \"Broker url not correctly configured please run server with -b or --broker 'http://pact-broker.adomain.com' option\" }" + val body = OptionalBody.body(errorJson.toByteArray()) + response = Response(500, CrossSiteHeaders.toMutableMap(), body) + } + return Result(response, oldState) + } + + fun publishPact(consumer: String, consumerVersion: String, provider: String, broker: String, brokerClient: IPactBrokerClient, tags: List?): Response { + val fileName = "$consumer-$provider.json" + val pact = File("${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" } + + return try { + val res = brokerClient.uploadPactFile(pact, consumerVersion, tags.orEmpty()) + if (res.errorValue() == null) { + logger.debug { "Pact successfully shared. deleting file.." } + removePact(pact) + Response(200, CrossSiteHeaders.toMutableMap(), OptionalBody.body(res.get()!!.toByteArray())) + } else { + Response(500, CrossSiteHeaders.toMutableMap(), OptionalBody.body(res.errorValue()!!.localizedMessage.toByteArray())) + } + } catch (e: IOException) { + Response(500, CrossSiteHeaders.toMutableMap(), OptionalBody.body("{\"error\": \"Got IO Exception while reading file. ${e.message}\"}".toByteArray())) + } catch (e: RequestFailedException) { + Response(e.status, CrossSiteHeaders.toMutableMap(), OptionalBody.body(e.body?.toByteArray())) + } catch (t: Throwable) { + Response(500, CrossSiteHeaders.toMutableMap(), OptionalBody.body(t.message?.toByteArray())) + } + } + + fun getOptions(authToken: String?): Map { + var options = mapOf() + if (authToken != null) { + options = mapOf("authentication" to listOf("bearer", authToken)) + } + return options + } + + private fun removePact(file: File) { + if (file.exists()) { + file.delete() + } + } + + fun getVarFromConfig(variable: String) = + if (!variable.isEmpty()) variable + else null + + fun getBrokerUrlFromConfig(config: Config) = + if (config.broker.isNotEmpty() && config.broker.startsWith("http")) config.broker + else null + + fun getVarFromJson(variable: String, json: Any?) = when(json) { + is Map<*, *> -> { + if (json.contains(variable)) json[variable].toString() + else null + } + else -> null + } + + fun getListFromJson(variable: String, json: Any?): List? = when(json) { + is Map<*, *> -> { + if (json.contains(variable)) json[variable] as List + else null + } + else -> null + } +} diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Publish.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Publish.scala deleted file mode 100644 index 28550f0e3b..0000000000 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Publish.scala +++ /dev/null @@ -1,109 +0,0 @@ -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.getAuthToken) - - 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) - } - new 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.getBroker.nonEmpty && config.getBroker.startsWith("http")) Some(config.getBroker) - 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 - } - -} diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala index bfe091b33c..399732a554 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala @@ -33,7 +33,7 @@ object RequestRouter extends StrictLogging { action match { case "create" => Create.apply(request, oldState, config) case "complete" => Complete.apply(request, oldState) - case "publish" => Publish(request, oldState, config) + case "publish" => Publish.apply(request, oldState, config) case "" => ListServers.apply(oldState) case _ => new Result(pactDispatch(request, oldState), oldState) } diff --git a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PublishSpec.groovy b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PublishSpec.groovy index a124ae4727..96529b85c4 100644 --- a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PublishSpec.groovy +++ b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PublishSpec.groovy @@ -1,19 +1,40 @@ package au.com.dius.pact.server +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.pactbroker.IPactBrokerClient +import scala.Option +import scala.collection.JavaConverters +import scala.collection.immutable.List import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties +import java.nio.file.Files +import java.nio.file.Path + +@RestoreSystemProperties class PublishSpec extends Specification { + static Path pactDir + + def setupSpec() { + pactDir = Files.createTempDirectory('pacts') + System.setProperty('pact.rootDir', pactDir.toString()) + } + + def cleanupSpec() { + System.properties.remove('pact.rootDir') + pactDir.toFile().deleteDir() + } def 'invalid broker url in config will not set broker'() { given: def config = new Config(80, '0.0.0.0', false, 100, 200, false, 3, '', '', 0, 'invalid', 'abc#3') -// def pact = PublishSpec.getResourceAsStream('/create-pact.json').text when: - def result = Publish.getBrokerUrlFromConfig(config) + def result = Publish.INSTANCE.getBrokerUrlFromConfig(config) then: - !result.defined + !result } def 'valid broker url will set broker'() { @@ -33,9 +54,235 @@ class PublishSpec extends Specification { ) when: - def result = Publish.getBrokerUrlFromConfig(config) + def result = Publish.INSTANCE.getBrokerUrlFromConfig(config) then: - result.defined + result } + + def 'getVarFromJson'() { + expect: + Publish.INSTANCE.getVarFromJson(variable, json) == result + + where: + + variable | json | result + 'null' | null | null + 'null' | 'null' | null + 'null' | [a: 'b'] | null + 'c' | [a: 'b'] | null + 'a' | [a: 'b'] | 'b' + 'a' | [a: 100] | '100' + } + + def 'getListFromJson'() { + expect: + Publish.INSTANCE.getListFromJson(variable, json) == result + + where: + + variable | json | result + 'null' | null | null + 'null' | 'null' | null + 'null' | [a: 'b'] | null + 'c' | [a: 'b'] | null + 'a' | [a: ['b']] | ['b'] + } + + def 'getBrokerUrlFromConfig'() { + expect: + Publish.INSTANCE.getBrokerUrlFromConfig(new Config().copyBroker(broker)) == result + + where: + + broker | result + '' | null + 'hgjhkhj' | null + 'http://hgjhkhj' | 'http://hgjhkhj' + } + + def 'getVarFromConfig'() { + expect: + Publish.INSTANCE.getVarFromConfig(variable) == result + + where: + + variable | result + '' | null + 'a' | 'a' + } + + def 'getOptions'() { + expect: + Publish.INSTANCE.getOptions(authToken) == result + + where: + + authToken | result + null | [:] + 'token' | ['authentication': ['bearer', 'token']] + } + + def 'apply returns an error if the broker URL is not defined'() { + given: + def request = new Request() + request.body = OptionalBody.body('{}') + def state = new ServerState() + def config = new Config() + + when: + def result = Publish.apply(request, state, config) + + then: + result.response.status == 500 + } + + def 'apply returns an error if there is no consumer in the request body'() { + given: + def request = new Request() + request.body = OptionalBody.body('{"consumerVersion":"123","provider":"provider"}') + def state = new ServerState() + def config = new Config().copyBroker('http://test-broker') + + when: + def result = Publish.apply(request, state, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if there is no consumerVersion in the request body'() { + given: + def request = new Request() + request.body = OptionalBody.body('{"consumer":"123","provider":"provider"}') + def state = new ServerState() + def config = new Config().copyBroker('http://test-broker') + + when: + def result = Publish.apply(request, state, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if there is no provider in the request body'() { + given: + def request = new Request() + request.body = OptionalBody.body('{"consumerVersion":"123","consumer":"provider"}') + def state = new ServerState() + def config = new Config().copyBroker('http://test-broker') + + when: + def result = Publish.apply(request, state, config) + + then: + result.response.status == 400 + } + + def 'publishPact calls uploadPactFile on the broker client'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = null + + when: + def result = Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', []) >> new au.com.dius.pact.core.support.Result.Ok('OK') + result.status == 200 + result.body.valueAsString() == 'OK' + } + + def 'publishPact deletes the Pact file after publishing it'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = null + def pactFile = new File(pactDir.toFile(), "${consumer}-${provider}.json".toString()) + pactFile.text = '{}' + + when: + Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', []) >> new au.com.dius.pact.core.support.Result.Ok('OK') + !pactFile.exists() + } + + def 'publishPact returns an error if the publish call fails'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = null + + when: + def result = Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', []) >> new au.com.dius.pact.core.support.Result.Err(new RuntimeException('Boom')) + result.status == 500 + result.body.valueAsString() == 'Boom' + } + + def 'publishPact does not delete the Pact file if publishing it fails'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = null + def pactFile = new File(pactDir.toFile(), "${consumer}-${provider}.json".toString()) + pactFile.text = '{}' + + when: + Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', []) >> new au.com.dius.pact.core.support.Result.Err(new RuntimeException('Boom')) + pactFile.exists() + } + + def 'publishPact passes any tags to the publish call'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = ['a', 'b', 'c'] + + when: + Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', ['a', 'b', 'c']) >> new au.com.dius.pact.core.support.Result.Ok('OK') + } + + def 'publishPact handles any IO exception'() { + given: + String consumer = 'consumer' + String consumerVersion = 'version' + String provider = 'provider' + String broker = 'http://test-broker' + IPactBrokerClient client = Mock(IPactBrokerClient) + def tags = null + + when: + def result = Publish.INSTANCE.publishPact(consumer, consumerVersion, provider, broker, client, tags) + + then: + 1 * client.uploadPactFile(_, 'version', []) >> { throw new IOException("Boom!") } + result.status == 500 + } }