-
-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(pact-jvm-server): Converted MockProvider to kotlin
- Loading branch information
1 parent
0234216
commit 5c94820
Showing
14 changed files
with
339 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/KTorHttpsKeystoreMockProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {}) | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/KTorMockProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {}) | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/MockProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.