Skip to content

Commit

Permalink
chore(pact-jvm-server): Converted MockProvider to kotlin
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Nov 18, 2024
1 parent 0234216 commit 5c94820
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 207 deletions.
13 changes: 10 additions & 3 deletions pact-jvm-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ mainClassName = 'au.com.dius.pact.server.Server'
dependencies {
implementation project(':consumer')
implementation project(':core:pactbroker')
implementation 'ch.qos.logback:logback-core:1.4.4'
implementation 'ch.qos.logback:logback-classic:1.4.4'
implementation 'ch.qos.logback:logback-core:1.5.6'
implementation 'ch.qos.logback:logback-classic:1.5.6'
implementation 'com.github.scopt:scopt_2.12:3.5.0'
implementation('com.typesafe.scala-logging:scala-logging_2.12:3.7.2') {
exclude group: 'org.scala-lang'
Expand All @@ -21,10 +21,17 @@ dependencies {
exclude module: 'netty-transport-native-kqueue'
exclude module: 'netty-transport-native-epoll'
}
implementation 'io.github.oshai:kotlin-logging-jvm'
implementation 'org.apache.commons:commons-io:1.3.2'
implementation 'org.apache.commons:commons-lang3'
implementation 'org.apache.commons:commons-text'
implementation 'org.apache.tika:tika-core'
implementation('io.netty:netty-handler') {
exclude module: 'netty-transport-native-kqueue'
}
implementation 'io.ktor:ktor-server-netty'
implementation 'io.ktor:ktor-network-tls-certificates'
implementation 'io.ktor:ktor-server-call-logging'

testImplementation 'org.apache.groovy:groovy'
testImplementation 'org.apache.groovy:groovy-json'
Expand All @@ -49,7 +56,7 @@ compileScala {
}

test {
dependsOn(':pact-jvm-server:assembleDist')
dependsOn(':pact-jvm-server:installDist')
systemProperty('appExecutable', (new File(buildDir, 'install/pact-jvm-server/bin/pact-jvm-server')).path)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package au.com.dius.pact.server

import au.com.dius.pact.consumer.model.MockHttpsProviderConfig
import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.Response
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.install
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.embeddedServer
import io.ktor.server.engine.sslConnector
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.request.httpMethod
import io.ktor.server.response.header
import io.ktor.server.response.respond

private val logger = KotlinLogging.logger {}

class KTorHttpsKeystoreMockProvider(override val config: MockHttpsProviderConfig): BaseKTorMockProvider(config) {
private val serverHostname = config.hostname
private val serverPort = config.port
private val keyStore = config.keyStore!!
private val keyStoreAlias = config.keyStoreAlias
private val password = config.keystorePassword
private val privateKeyPassword = config.privateKeyPassword

private val env = applicationEngineEnvironment {
sslConnector(keyStore = keyStore,
keyAlias = keyStoreAlias,
keyStorePassword = { password.toCharArray() },
privateKeyPassword = { privateKeyPassword.toCharArray() }) {
host = serverHostname
port = serverPort
}

module {
install(CallLogging)
intercept(ApplicationCallPipeline.Call) {
if (context.request.httpMethod == HttpMethod.Options && context.request.headers.contains("X-PACT-BOOTCHECK")) {
context.response.header("X-PACT-BOOTCHECK", "true")
context.respond(HttpStatusCode.OK)
} else {
try {
val request = toPactRequest(context)
val response = handleRequest(request)
pactResponseToKTorResponse(response, context)
} catch (e: Exception) {
logger.error(e) { "Failed to generate response" }
pactResponseToKTorResponse(
Response(500, mutableMapOf("Content-Type" to listOf("application/json")),
OptionalBody.body("{\"error\": ${e.message}}".toByteArray(), ContentType.JSON)), context)
}
}
}
}
}

init {
server = embeddedServer(Netty, environment = env, configure = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package au.com.dius.pact.server

import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.IResponse
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 io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.install
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.request.httpMethod
import io.ktor.server.request.path
import io.ktor.server.request.receiveStream
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.zip.DeflaterInputStream
import java.util.zip.GZIPInputStream

private val logger = KotlinLogging.logger {}

abstract class BaseKTorMockProvider(override val config: MockProviderConfig): StatefulMockProvider() {

lateinit var server: NettyApplicationEngine

suspend fun toPactRequest(call: ApplicationCall): Request {
val request = call.request
val headers = request.headers
val bodyContents = withContext(Dispatchers.IO) {
val stream = call.receiveStream()
when (bodyIsCompressed(headers["Content-Encoding"])) {
"gzip" -> GZIPInputStream(stream).readBytes()
"deflate" -> DeflaterInputStream(stream).readBytes()
else -> stream.readBytes()
}
}
val body = if (bodyContents.isEmpty()) {
OptionalBody.empty()
} else {
OptionalBody.body(bodyContents, ContentType.fromString(headers["Content-Type"]).or(ContentType.JSON))
}
return Request(request.httpMethod.value, request.path(),
request.queryParameters.entries().associate { it.toPair() }.toMutableMap(),
headers.entries().associate { it.toPair() }.toMutableMap(), body)
}

private fun bodyIsCompressed(encoding: String?): String? {
return if (COMPRESSED_ENCODINGS.contains(encoding)) encoding else null
}

suspend fun pactResponseToKTorResponse(response: IResponse, call: ApplicationCall) {
response.headers.forEach { entry ->
entry.value.forEach {
call.response.headers.append(entry.key, it, safeOnly = false)
}
}

val body = response.body
if (body.isPresent()) {
call.respondBytes(status = HttpStatusCode.fromValue(response.status), bytes = body.unwrap())
} else {
call.respond(HttpStatusCode.fromValue(response.status))
}
}

override fun start() {
logger.debug { "Starting mock server" }
server.start()
logger.debug { "Mock server started: ${server.environment.connectors}" }
}

override fun stop() {
server.stop(100)
logger.debug { "Mock server shutdown" }
}

companion object {
private val COMPRESSED_ENCODINGS = setOf("gzip", "deflate")
}
}

class KTorMockProvider(override val config: MockProviderConfig): BaseKTorMockProvider(config) {
private val serverHostname = config.hostname
private val serverPort = config.port

private val env = applicationEngineEnvironment {
connector {
host = serverHostname
port = serverPort
}

module {
install(CallLogging)
intercept(ApplicationCallPipeline.Call) {
if (context.request.httpMethod == HttpMethod.Options && context.request.headers.contains("X-PACT-BOOTCHECK")) {
context.response.header("X-PACT-BOOTCHECK", "true")
context.respond(HttpStatusCode.OK)
} else {
try {
val request = toPactRequest(context)
val response = handleRequest(request)
pactResponseToKTorResponse(response, context)
} catch (e: Exception) {
logger.error(e) { "Failed to generate response" }
pactResponseToKTorResponse(
Response(500, mutableMapOf("Content-Type" to listOf("application/json")),
OptionalBody.body("{\"error\": ${e.message}}".toByteArray(), ContentType.JSON)), context)
}
}
}
}
}

init {
server = embeddedServer(Netty, environment = env, configure = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package au.com.dius.pact.server

import au.com.dius.pact.consumer.model.MockHttpsKeystoreProviderConfig
import au.com.dius.pact.consumer.model.MockHttpsProviderConfig
import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.core.model.IResponse
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactSpecVersion
import au.com.dius.pact.core.model.Request
import au.com.dius.pact.core.support.handleWith
import au.com.dius.pact.core.support.Result
import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}

interface MockProvider {
val config: MockProviderConfig
val session: PactSession
fun start(pact: Pact)
fun <T> run(code: () -> T): Result<T, Exception>
fun <T> runAndClose(pact: Pact, code: () -> T): Result<Pair<T, PactSessionResults>, Exception>
fun stop()
}

object DefaultMockProvider {

fun withDefaultConfig(pactVersion: PactSpecVersion = PactSpecVersion.V3) =
apply(MockProviderConfig.createDefault(pactVersion))

// Constructor providing a default implementation of StatefulMockProvider.
// Users should not explicitly be forced to choose a variety.
fun apply(config: MockProviderConfig): StatefulMockProvider =
when (config) {
is MockHttpsProviderConfig -> KTorHttpsKeystoreMockProvider(config)
// is MockHttpsProviderConfig -> UnfilteredHttpsMockProvider(config)
else -> KTorMockProvider(config)
}
}

abstract class StatefulMockProvider: MockProvider {
private var sessionVar = PactSession.empty
private var pactVar: Pact? = null

private fun waitForRequestsToFinish() = Thread.sleep(100)

override val session: PactSession
get() = sessionVar
val pact: Pact?
get() = pactVar

abstract fun start()

@Synchronized
override fun start(pact: Pact) {
pactVar = pact
sessionVar = PactSession.forPact(pact)
start()
}

override fun <T> run(code: () -> T): Result<T, Exception> {
return handleWith {
val codeResult = code()
waitForRequestsToFinish()
codeResult
}
}

override fun <T> runAndClose(pact: Pact, code: () -> T): Result<Pair<T, PactSessionResults>, Exception> {
return handleWith {
try {
start(pact)
val codeResult = code()
waitForRequestsToFinish()
(codeResult to session.remainingResults())
} finally {
stop()
}
}
}

@Synchronized
fun handleRequest(req: Request): IResponse {
logger.debug { "Received request: $req" }
val (response, newSession) = session.receiveRequest(req)
logger.debug { "Generating response: $response" }
sessionVar = newSession
return response
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.Request
import au.com.dius.pact.core.model.Response
import io.github.oshai.kotlinlogging.KotlinLogging
import org.apache.commons.text.StringEscapeUtils

private val logger = KotlinLogging.logger {}

data class PactSession(
val expected: Pact?,
val results: PactSessionResults
Expand All @@ -32,6 +35,7 @@ data class PactSession(
(invalidResponse to recordUnexpected(req))
}
} else {
logger.warn { "Expected Pact is not set!" }
invalidResponse to this
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package au.com.dius.pact.server

import java.io.File

import au.com.dius.pact.core.model._
import com.typesafe.scalalogging.StrictLogging

import scala.collection.JavaConverters._
import scala.util.Success

object Complete {
object Complete extends StrictLogging {

def getPort(j: Any): Option[String] = j match {
case map: Map[AnyRef, AnyRef] => {
Expand All @@ -32,17 +32,17 @@ object Complete {
val result = for {
port <- getPort(JsonUtils.parseJsonString(request.getBody.valueAsString()))
mockProvider <- oldState.get(port)
sessionResults = mockProvider.session.remainingResults
pact <- mockProvider.pact
sessionResults = mockProvider.getSession.remainingResults
pact <- Option(mockProvider.getPact)
} yield {
mockProvider.stop()

writeIfMatching(pact, sessionResults, mockProvider.config.getPactVersion) match {
writeIfMatching(pact, sessionResults, mockProvider.getConfig.getPactVersion) match {
case PactVerified => pactWritten(new Response(200, ResponseUtils.CrossSiteHeaders.asJava),
mockProvider.config.getPort.toString)
mockProvider.getConfig.getPort.toString)
case error => pactWritten(new Response(400,
Map("Content-Type" -> List("application/json").asJava).asJava, toJson(error)),
mockProvider.config.getPort.toString)
mockProvider.getConfig.getPort.toString)
}
}

Expand Down
Loading

0 comments on commit 5c94820

Please sign in to comment.