Skip to content

Commit

Permalink
feat: Update mock server to handle compressed bodies #556
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed Jun 12, 2020
1 parent 05dddad commit 5bf94d7
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import au.com.dius.pact.consumer.MockServer
import au.com.dius.pact.consumer.PactTestExecutionContext
import au.com.dius.pact.consumer.PactVerificationResult
import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.consumer.model.MockServerImplementation
import au.com.dius.pact.core.model.Consumer
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.PactReaderKt
Expand Down Expand Up @@ -353,7 +354,8 @@ class PactBuilder extends BaseBuilder {
def pact = new RequestResponsePact(provider, consumer, interactions)

def pactVersion = options.specificationVersion ?: PactSpecVersion.V3
MockProviderConfig config = MockProviderConfig.httpConfig(LOCALHOST, port ?: 0, pactVersion as PactSpecVersion)
MockProviderConfig config = MockProviderConfig.httpConfig(LOCALHOST, port ?: 0, pactVersion as PactSpecVersion,
MockServerImplementation.Default)

def runTest = closure
if (closure.maximumNumberOfParameters < 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import au.com.dius.pact.consumer.ConsumerPactBuilder;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.model.MockServerImplementation;
import au.com.dius.pact.core.model.annotations.Pact;
import au.com.dius.pact.core.model.annotations.PactFolder;
import au.com.dius.pact.consumer.PactVerificationResult;
Expand Down Expand Up @@ -37,7 +38,7 @@ public BaseProviderRule(Object target, String provider, String hostInterface, In
this.target = target;
this.provider = provider;
config = MockProviderConfig.httpConfig(StringUtils.isEmpty(hostInterface) ? MockProviderConfig.LOCALHOST : hostInterface,
port == null ? 0 : port, pactVersion);
port == null ? 0 : port, pactVersion, MockServerImplementation.Default);
}

public MockProviderConfig getConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import au.com.dius.pact.consumer.junit.JUnitTestSupport
import au.com.dius.pact.consumer.mockServer
import au.com.dius.pact.consumer.model.MockHttpsProviderConfig
import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.consumer.model.MockServerImplementation
import au.com.dius.pact.core.model.BasePact
import au.com.dius.pact.core.model.Consumer
import au.com.dius.pact.core.model.Interaction
Expand Down Expand Up @@ -99,7 +100,13 @@ annotation class PactTestFor(
/**
* If HTTPS should be used. If enabled, a mock server with a self-signed cert will be started.
*/
val https: Boolean = false
val https: Boolean = false,

/**
* The type of mock server implementation to use. The default is to use the Java server for HTTP and the KTor
* server for HTTPS
*/
val mockServerImplementation: MockServerImplementation = MockServerImplementation.Default
)

data class ProviderInfo @JvmOverloads constructor(
Expand All @@ -108,20 +115,22 @@ data class ProviderInfo @JvmOverloads constructor(
val port: String = "",
val pactVersion: PactSpecVersion? = null,
val providerType: ProviderType? = null,
val https: Boolean = false
val https: Boolean = false,
val mockServerImplementation: MockServerImplementation = MockServerImplementation.Default
) {

fun mockServerConfig() = if (https) {
MockHttpsProviderConfig.httpsConfig(
if (hostInterface.isEmpty()) MockProviderConfig.LOCALHOST else hostInterface,
if (port.isEmpty()) 0 else port.toInt(),
pactVersion ?: PactSpecVersion.V3
pactVersion ?: PactSpecVersion.V3,
mockServerImplementation
)
} else {
MockProviderConfig.httpConfig(
if (hostInterface.isEmpty()) MockProviderConfig.LOCALHOST else hostInterface,
if (port.isEmpty()) 0 else port.toInt(),
pactVersion ?: PactSpecVersion.V3
pactVersion ?: PactSpecVersion.V3,
mockServerImplementation
)
}

Expand All @@ -131,7 +140,8 @@ data class ProviderInfo @JvmOverloads constructor(
port = if (port.isNotEmpty()) port else other.port,
pactVersion = pactVersion ?: other.pactVersion,
providerType = providerType ?: other.providerType,
https = https || other.https
https = https || other.https,
mockServerImplementation = mockServerImplementation.merge(other.mockServerImplementation)
)
}

Expand All @@ -145,7 +155,7 @@ data class ProviderInfo @JvmOverloads constructor(
when (annotation.providerType) {
ProviderType.UNSPECIFIED -> null
else -> annotation.providerType
}, annotation.https)
}, annotation.https, annotation.mockServerImplementation)
}
}

Expand Down Expand Up @@ -227,6 +237,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
val store = context.getStore(NAMESPACE)
return if (store["mockServer"] == null) {
val config = providerInfo.mockServerConfig()

store.put("mockServerConfig", config)
val mockServer = mockServer(lookupPact(providerInfo, pactMethod, context) as RequestResponsePact, config)
store.put("mockServer", JUnit5MockServerSupport(mockServer))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ class DateTimeWithTimezoneTest {
.uponReceiving('a request with some datetime info')
.method('POST')
.path('/values')
.body(new PactDslJsonBody().datetime('datetime', "YYYY-MM-dd'T'HH:mm:ss.SSSxxx"))
.body(new PactDslJsonBody().datetime('datetime', "YYYY-MM-dd'T'HH:mm:ss.SSSXXX"))
.willRespondWith()
.status(200)
.body(new PactDslJsonBody().datetime('datetime', "YYYY-MM-dd'T'HH:mm:ss.SSSxxx"))
.body(new PactDslJsonBody().datetime('datetime', "YYYY-MM-dd'T'HH:mm:ss.SSSXXX"))
.toPact()
}

@Test
void testFiles(MockServer mockServer) {
HttpResponse httpResponse = Request.Post("${mockServer.url}/values")
.body(new StringEntity('{"datetime": "' +
DateTimeFormatter.ofPattern("YYYY-MM-dd'T'HH:mm:ss.SSSxxx").format(ZonedDateTime.now())
DateTimeFormatter.ofPattern("YYYY-MM-dd'T'HH:mm:ss.SSSXXX").format(ZonedDateTime.now())
+ '"}', 'application/json', 'UTF-8'))
.execute().returnResponse()
assert httpResponse.statusLine.statusCode == 200
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package au.com.dius.pact.consumer.junit5

import au.com.dius.pact.consumer.MockServer
import au.com.dius.pact.consumer.dsl.PactDslJsonBody
import au.com.dius.pact.consumer.dsl.PactDslWithProvider
import au.com.dius.pact.core.model.RequestResponsePact
import au.com.dius.pact.core.model.annotations.Pact
import org.apache.http.client.HttpClient
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.ContentType
import org.apache.http.impl.client.HttpClients
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(PactConsumerTestExt)
@PactTestFor(providerName = 'ProviderThatAcceptsGZippedBodies')
class GZippedBodyTest {
@Pact(consumer = 'Consumer')
RequestResponsePact pact(PactDslWithProvider builder) {
builder
.uponReceiving('a request with a zipped body')
.method('POST')
.path('/values')
.body(new PactDslJsonBody().integerType('id'))
.willRespondWith()
.status(200)
.body(new PactDslJsonBody().integerType('id'))
.toPact()
}

@Test
void testFiles(MockServer mockServer) {
def entity = EntityBuilder.create()
.setText('{"id": 1}')
.setContentType(ContentType.APPLICATION_JSON)
.gzipCompress()
.build()
HttpClient httpClient = HttpClients.createDefault()
def post = new HttpPost("${mockServer.url}/values")
post.setEntity(entity)
post.setHeader('Content-Type', ContentType.APPLICATION_JSON.toString())
post.setHeader('Content-Encoding', 'gzip')
post.setHeader('Accept-Encoding', 'gzip, deflate')
def response = httpClient.execute(post)
assert response.statusLine.statusCode == 200
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package au.com.dius.pact.consumer.junit5

import au.com.dius.pact.consumer.MockServer
import au.com.dius.pact.consumer.dsl.PactDslJsonBody
import au.com.dius.pact.consumer.dsl.PactDslWithProvider
import au.com.dius.pact.consumer.model.MockServerImplementation
import au.com.dius.pact.core.model.RequestResponsePact
import au.com.dius.pact.core.model.annotations.Pact
import org.apache.http.client.HttpClient
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.ContentType
import org.apache.http.impl.client.HttpClients
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(PactConsumerTestExt)
@PactTestFor(providerName = 'ProviderThatAcceptsGZippedBodies', port = '42567',
mockServerImplementation = MockServerImplementation.KTorServer)
class KTorGZippedBodyTest {
@Pact(consumer = 'KTorGZippedBodyTestConsumer')
RequestResponsePact pact(PactDslWithProvider builder) {
builder
.uponReceiving('a request with a zipped body')
.method('POST')
.path('/values')
.body(new PactDslJsonBody().integerType('id'))
.willRespondWith()
.status(200)
.body(new PactDslJsonBody().integerType('id'))
.toPact()
}

@Test
void testFiles(MockServer mockServer) {
def entity = EntityBuilder.create()
.setText('{"id": 1}')
.setContentType(ContentType.APPLICATION_JSON)
.gzipCompress()
.build()
HttpClient httpClient = HttpClients.createDefault()
def post = new HttpPost("${mockServer.url}/values")
post.setEntity(entity)
post.setHeader('Content-Type', ContentType.APPLICATION_JSON.toString())
post.setHeader('Content-Encoding', 'gzip')
post.setHeader('Accept-Encoding', 'gzip, deflate')
def response = httpClient.execute(post)
assert response.statusLine.statusCode == 200
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.request.httpMethod
import io.ktor.request.path
import io.ktor.request.receiveText
import io.ktor.request.receiveStream
import io.ktor.response.header
import io.ktor.response.respond
import io.ktor.response.respondBytes
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.server.engine.sslConnector
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import mu.KLogging
import java.util.zip.DeflaterInputStream
import java.util.zip.GZIPInputStream

class KTorMockServer(
pact: RequestResponsePact,
Expand Down Expand Up @@ -71,10 +73,10 @@ class KTorMockServer(
}
}

private var server: ApplicationEngine = embeddedServer(Netty, environment = env, configure = {})
private var server: NettyApplicationEngine = embeddedServer(Netty, environment = env, configure = {})

private suspend fun pactResponseToKTorResponse(response: Response, call: ApplicationCall) {
response.headers?.forEach { entry ->
response.headers.forEach { entry ->
entry.value.forEach {
call.response.headers.append(entry.key, it, safeOnly = false)
}
Expand All @@ -90,19 +92,25 @@ class KTorMockServer(

private suspend fun toPactRequest(call: ApplicationCall): Request {
val headers = call.request.headers
val bodyContents = call.receiveText()
val stream = call.receiveStream()
val bodyContents = 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.toByteArray(), ContentType(headers["Content-Type"] ?: ContentType.JSON.contentType))
OptionalBody.body(bodyContents, ContentType(headers["Content-Type"] ?: ContentType.JSON.contentType))
}
return Request(call.request.httpMethod.value, call.request.path(),
call.request.queryParameters.entries().associate { it.toPair() }.toMutableMap(),
headers.entries().associate { it.toPair() }.toMutableMap(), body)
}

override fun getUrl() =
"${config.scheme}://${server.environment.connectors.first().host}:${this.getPort()}"
override fun getUrl(): String {
return "${config.scheme}://${server.environment.connectors.first().host}:${this.getPort()}"
}

override fun getPort() = server.environment.connectors.first().port

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import au.com.dius.pact.core.model.RequestResponsePact
import au.com.dius.pact.core.model.Response
import au.com.dius.pact.core.model.generators.GeneratorTestMode
import au.com.dius.pact.core.model.queryStringToMap
import au.com.dius.pact.core.support.CustomServiceUnavailableRetryStrategy
import com.sun.net.httpserver.Headers
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpHandler
Expand All @@ -35,6 +36,8 @@ import java.lang.Thread.sleep
import java.nio.charset.Charset
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.zip.DeflaterInputStream
import java.util.zip.GZIPInputStream

/**
* Returns a mock server for the pact and config
Expand Down Expand Up @@ -79,6 +82,14 @@ abstract class AbstractBaseMockServer : MockServer {
abstract fun start()
abstract fun stop()
abstract fun waitForServer()

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

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

abstract class BaseMockServer(val pact: RequestResponsePact, val config: MockProviderConfig) : AbstractBaseMockServer() {
Expand All @@ -89,12 +100,14 @@ abstract class BaseMockServer(val pact: RequestResponsePact, val config: MockPro

override fun waitForServer() {
val sf = SSLSocketFactory(TrustSelfSignedStrategy())
val retryStrategy = CustomServiceUnavailableRetryStrategy(5, 500)
val httpclient = HttpClientBuilder.create()
.setConnectionManager(BasicHttpClientConnectionManager(RegistryBuilder.create<ConnectionSocketFactory>()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sf)
.build()))
.setSSLSocketFactory(sf)
.setServiceUnavailableRetryStrategy(retryStrategy)
.build()

val httpOptions = HttpOptions(getUrl())
Expand Down Expand Up @@ -227,11 +240,16 @@ abstract class BaseJdkMockServer(

private fun toPactRequest(exchange: HttpExchange): Request {
val headers = exchange.requestHeaders
val bodyContents = exchange.requestBody.readBytes()
val contentType = contentType(headers)
val bodyContents = when (bodyIsCompressed(headers.getFirst("Content-Encoding"))) {
"gzip" -> GZIPInputStream(exchange.requestBody).readBytes()
"deflate" -> DeflaterInputStream(exchange.requestBody).readBytes()
else -> exchange.requestBody.readBytes()
}
val body = if (bodyContents.isEmpty()) {
OptionalBody.empty()
} else {
OptionalBody.body(bodyContents, contentType(headers))
OptionalBody.body(bodyContents, contentType)
}
return Request(exchange.requestMethod, exchange.requestURI.path,
queryStringToMap(exchange.requestURI.rawQuery).toMutableMap(), headers.toMutableMap(), body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ class MockHttpsProviderConfig @JvmOverloads constructor(
companion object {
@JvmStatic
@JvmOverloads
fun httpsConfig(hostname: String = LOCALHOST, port: Int = 0, pactVersion: PactSpecVersion = PactSpecVersion.V3): MockHttpsProviderConfig {
fun httpsConfig(
hostname: String = LOCALHOST,
port: Int = 0,
pactVersion: PactSpecVersion = PactSpecVersion.V3,
implementation: MockServerImplementation = MockServerImplementation.KTorServer
): MockHttpsProviderConfig {
val jksFile = File.createTempFile("PactTest", ".jks")
val p = if (port == 0) {
randomPort()
Expand All @@ -31,7 +36,7 @@ class MockHttpsProviderConfig @JvmOverloads constructor(
}
val keystore = io.ktor.network.tls.certificates.generateCertificate(jksFile, "SHA1withRSA", "PactTest", "changeit", "changeit", 1024)
return MockHttpsProviderConfig(hostname, p, pactVersion, keystore, "PactTest", "changeit", "changeit",
MockServerImplementation.KTorServer)
implementation.merge(MockServerImplementation.KTorServer))
}
}
}
Loading

0 comments on commit 5bf94d7

Please sign in to comment.