diff --git a/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Create.kt b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Create.kt new file mode 100644 index 000000000..b6247ed67 --- /dev/null +++ b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/Create.kt @@ -0,0 +1,123 @@ +package au.com.dius.pact.server + +import au.com.dius.pact.consumer.model.MockHttpsProviderConfig +import au.com.dius.pact.consumer.model.MockProviderConfig +import au.com.dius.pact.core.model.DefaultPactReader +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.PactSpecVersion +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.Response +import au.com.dius.pact.core.support.isNotEmpty +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.commons.lang3.RandomUtils +import java.io.File +import java.io.IOException +import java.net.ServerSocket +import java.security.KeyStore + +private val logger = KotlinLogging.logger {} + +object Create { + + private val CrossSiteHeaders = mapOf("Access-Control-Allow-Origin" to listOf("*")) + + @JvmStatic + fun create(_state: String, path: List?, requestBody: String, oldState: ServerState, config: Config): Result { + val pact = DefaultPactReader.loadPact(requestBody) + + val mockConfig : MockProviderConfig = if (config.keystorePath.isNotEmpty()) { + val ks = KeyStore.getInstance(File(config.keystorePath), config.keystorePassword.toCharArray()) + MockHttpsProviderConfig( + config.host, + config.sslPort, + PactSpecVersion.fromInt(config.pactVersion), + ks, + "localhost", + config.keystorePassword, + config.keystorePassword + ) + } + else { + MockProviderConfig(config.host, randomPort(config.portLowerBound, config.portUpperBound), + PactSpecVersion.fromInt(config.pactVersion)) + } + val server = DefaultMockProvider.apply(mockConfig) + + val port = server.config.port + val portEntry = port.toString() to server + + val newState = (oldState.state.entries.map { it.toPair() } + portEntry).toMutableSet() + if (path != null) { + for (p in path) { + if (p != null) { + newState += (p to server) + } + } + } + + val body = OptionalBody.body(("{\"port\": $port}").toByteArray()) + + server.start(pact) + + val headers = CrossSiteHeaders + mapOf("Content-Type" to listOf("application/json")) + return Result(Response(201, headers.toMutableMap(), body), ServerState(newState.associate { it })) + } + + @JvmStatic + fun apply(request: Request, oldState: ServerState, config: Config): Result { + logger.debug { "path=${request.path}" } + logger.debug { "query=${request.query}" } + logger.debug { "body=${request.body}" } + + if (request.query.isNotEmpty()) { + val stateList = request.query["state"] + if (stateList != null) { + val state = stateList.first() + if (state != null) { + val paths = request.query["path"] + if (request.body.isPresent()) { + return create(state, paths, request.body.valueAsString(), oldState, config) + } + } + } + } + + val errorJson = OptionalBody.body("{\"error\": \"please provide state param and path param and pact body\"}".toByteArray()) + return Result(Response(400, CrossSiteHeaders.toMutableMap(), errorJson), oldState) + } + + private fun randomPort(lower: Int, upper: Int): Int { + var port: Int? = null + var count = 0 + while (port == null && count < 20) { + val randomPort = RandomUtils.nextInt(lower, upper) + if (portAvailable(randomPort)) { + port = randomPort + } + count += 1 + } + + if (port == null) { + port = 0 + } + + return port + } + + private fun portAvailable(p: Int): Boolean { + var socket: ServerSocket? = null + return try { + socket = ServerSocket(p) + true + } catch (_: IOException) { + false + } finally { + if (socket != null) { + try { + socket.close() + } catch (_: IOException) { + } + } + } + } +} diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Create.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Create.scala deleted file mode 100644 index f078c7530..000000000 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Create.scala +++ /dev/null @@ -1,110 +0,0 @@ -package au.com.dius.pact.server - -import java.io.{File, IOException} -import java.net.ServerSocket -import au.com.dius.pact.consumer.model.{MockHttpsProviderConfig, MockProviderConfig} -import au.com.dius.pact.core.model._ -import com.typesafe.scalalogging.StrictLogging -import org.apache.commons.lang3.RandomUtils - -import java.security.KeyStore -import scala.collection.JavaConverters._ - -object Create extends StrictLogging { - - def create(state: String, path: List[String], requestBody: String, oldState: ServerState, config: Config): Result = { - val pact = DefaultPactReader.INSTANCE.loadPact(requestBody).asInstanceOf[RequestResponsePact] - - val mockConfig : MockProviderConfig = { - if (config.getKeystorePath.nonEmpty) { - val ks = KeyStore.getInstance(new File(config.getKeystorePath), config.getKeystorePassword.toCharArray) - new MockHttpsProviderConfig( - config.getHost, - config.getSslPort, - PactSpecVersion.fromInt(config.getPactVersion), - ks, - "localhost", - config.getKeystorePassword, - config.getKeystorePassword - ) - } - else { - new MockProviderConfig(config.getHost, randomPort(config.getPortLowerBound, config.getPortUpperBound), - PactSpecVersion.fromInt(config.getPactVersion)) - } - } - val server = DefaultMockProvider.INSTANCE.apply(mockConfig) - - val port = server.getConfig.getPort - val portEntry = port.toString -> server - - // Not very scala... - val newState = (oldState.getState.asScala + portEntry) ++ - (for ( - pathValue <- path - ) yield (pathValue -> server)) - - val body = OptionalBody.body(("{\"port\": " + port + "}").getBytes) - - server.start(pact) - - new Result(new Response(201, (ResponseUtils.CrossSiteHeaders ++ Map("Content-Type" -> List("application/json").asJava)).asJava, body), new ServerState(newState.asJava)) - } - - def apply(request: Request, oldState: ServerState, config: Config): Result = { - def errorJson = OptionalBody.body("{\"error\": \"please provide state param and path param and pact body\"}".getBytes) - def clientError = new Result(new Response(400, ResponseUtils.CrossSiteHeaders.asJava, errorJson), - oldState) - - logger.debug(s"path=${request.getPath}") - logger.debug(s"query=${request.getQuery}") - logger.debug(request.getBody.toString) - - val result = if (request.getQuery != null) { - for { - stateList <- CollectionUtils.javaLMapToScalaLMap(request.getQuery).get("state") - state <- stateList.headOption - paths <- CollectionUtils.javaLMapToScalaLMap(request.getQuery).get("path") - body <- Option(request.getBody) - } yield create(state, paths, body.valueAsString(), oldState, config) - } else None - - result getOrElse clientError - } - - def randomPort(lower: Int, upper: Int): Int = { - var port: Integer = null - var count = 0 - while (port == null && count < 20) { - val randomPort = RandomUtils.nextInt(lower, upper) - if (portAvailable(randomPort)) { - port = randomPort - } - count += 1 - } - - if (port == null) { - port = 0 - } - - port - } - - private def portAvailable(p: Int): Boolean = { - var socket: ServerSocket = null - try { - socket = new ServerSocket(p) - true - } catch { - case _: IOException => false - } finally { - if (socket != null) { - try { - socket.close() - } catch { - case _: IOException => {} - } - } - } - } -} 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 7a0871623..939ee8a7c 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 @@ -31,7 +31,7 @@ object RequestRouter extends StrictLogging { val urlPattern ="/(\\w*)\\?{0,1}.*".r val urlPattern(action) = request.getPath action match { - case "create" => Create(request, oldState, config) + case "create" => Create.apply(request, oldState, config) case "complete" => Complete(request, oldState) case "publish" => Publish(request, oldState, config) case "" => ListServers(oldState) diff --git a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/CreateSpec.groovy b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/CreateSpec.groovy index b2ce5f038..b4dc2cc5d 100644 --- a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/CreateSpec.groovy +++ b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/CreateSpec.groovy @@ -1,6 +1,7 @@ package au.com.dius.pact.server -import scala.collection.JavaConverters +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Request import spock.lang.Specification import java.nio.file.Paths @@ -15,7 +16,7 @@ class CreateSpec extends Specification { when: def result = Create.create( 'test state', - JavaConverters.asScalaBuffer(['/data']).toList(), + ['/data'], pact, new ServerState(), new Config(4444, 'localhost', false, 20000, 40000, true, @@ -43,10 +44,10 @@ class CreateSpec extends Specification { when: def result = Create.create('test state', - JavaConverters.asScalaBuffer([]).toList(), + [], pact, new ServerState(), - new au.com.dius.pact.server.Config(4444, 'localhost', false, 20000, 40000, true, + new Config(4444, 'localhost', false, 20000, 40000, true, 2, keystorePath, password, 8444, '', '')) then: @@ -60,4 +61,69 @@ class CreateSpec extends Specification { } } } + + def 'apply returns an error if there is no query parameters'() { + given: + def request = new Request() + def serverState = new ServerState() + def config = new Config() + + when: + def result = Create.apply(request, serverState, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if there is no state query parameter'() { + given: + def request = new Request('GET', '/path', [qp: ['some value']]) + def serverState = new ServerState() + def config = new Config() + + when: + def result = Create.apply(request, serverState, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if the state query parameter is empty'() { + given: + def request = new Request('GET', '/path', [qp: []]) + def serverState = new ServerState() + def config = new Config() + + when: + def result = Create.apply(request, serverState, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if there is no path query parameter'() { + given: + def request = new Request('GET', '/path', [state: ['test']]) + def serverState = new ServerState() + def config = new Config() + + when: + def result = Create.apply(request, serverState, config) + + then: + result.response.status == 400 + } + + def 'apply returns an error if the request body is empty'() { + given: + def request = new Request('GET', '/path', [state: ['test'], path: ['/test']], [:], OptionalBody.empty()) + def serverState = new ServerState() + def config = new Config() + + when: + def result = Create.apply(request, serverState, config) + + then: + result.response.status == 400 + } }