Skip to content

Commit

Permalink
feat: enable HTTPS support with JUnit 5 consumer tests #1093
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed May 21, 2020
1 parent d59ca8d commit df85809
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 23 deletions.
1 change: 1 addition & 0 deletions consumer/junit5/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ dependencies {
testCompile('org.spockframework:spock-core:2.0-M2-groovy-3.0') {
exclude group: 'org.codehaus.groovy'
}
testCompile "org.apache.httpcomponents:httpclient:${project.httpClientVersion}"
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package au.com.dius.pact.consumer.junit5

import au.com.dius.pact.consumer.AbstractBaseMockServer
import au.com.dius.pact.consumer.BaseMockServer
import au.com.dius.pact.consumer.ConsumerPactBuilder
import au.com.dius.pact.consumer.MessagePactBuilder
import au.com.dius.pact.consumer.MockServer
import au.com.dius.pact.consumer.PactConsumerConfig
import au.com.dius.pact.consumer.PactTestRun
import au.com.dius.pact.consumer.PactVerificationResult
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.core.model.BasePact
import au.com.dius.pact.core.model.DefaultPactWriter
Expand Down Expand Up @@ -90,27 +93,45 @@ annotation class PactTestFor(
/**
* Type of provider (synchronous HTTP or asynchronous messages)
*/
val providerType: ProviderType = ProviderType.UNSPECIFIED
val providerType: ProviderType = ProviderType.UNSPECIFIED,

/**
* If HTTPS should be used. If enabled, a mock server with a self-signed cert will be started.
*/
val https: Boolean = false
)

data class ProviderInfo @JvmOverloads constructor(
val providerName: String = "",
val hostInterface: String = "",
val port: String = "",
val pactVersion: PactSpecVersion? = null,
val providerType: ProviderType? = null
val providerType: ProviderType? = null,
val https: Boolean = false
) {

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

fun merge(other: ProviderInfo): ProviderInfo {
return copy(providerName = if (providerName.isNotEmpty()) providerName else other.providerName,
hostInterface = if (hostInterface.isNotEmpty()) hostInterface else other.hostInterface,
port = if (port.isNotEmpty()) port else other.port,
pactVersion = pactVersion ?: other.pactVersion,
providerType = providerType ?: other.providerType)
providerType = providerType ?: other.providerType,
https = https || other.https
)
}

companion object {
Expand All @@ -123,15 +144,24 @@ data class ProviderInfo @JvmOverloads constructor(
when (annotation.providerType) {
ProviderType.UNSPECIFIED -> null
else -> annotation.providerType
})
}, annotation.https)
}
}

class JUnit5MockServerSupport(private val baseMockServer: BaseMockServer) : MockServer by baseMockServer,
class JUnit5MockServerSupport(private val baseMockServer: BaseMockServer) : AbstractBaseMockServer(),
ExtensionContext.Store.CloseableResource {
override fun close() {
baseMockServer.stop()
}

override fun start() = baseMockServer.start()
override fun stop() = baseMockServer.stop()
override fun waitForServer() = baseMockServer.waitForServer()
override fun getUrl() = baseMockServer.getUrl()
override fun getPort() = baseMockServer.getPort()
override fun <R> runAndWritePact(pact: RequestResponsePact, pactVersion: PactSpecVersion, testFn: PactTestRun<R>) =
baseMockServer.runAndWritePact(pact, pactVersion, testFn)
override fun validateMockServerState(testResult: Any?) = baseMockServer.validateMockServerState(testResult)
}

class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCallback, ParameterResolver, AfterTestExecutionCallback, AfterAllCallback {
Expand Down Expand Up @@ -184,21 +214,23 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
val (providerInfo, pactMethod) = lookupProviderInfo(context)
logger.debug { "providerInfo = $providerInfo" }

setupMockServer(providerInfo, pactMethod, context)
if (providerInfo.providerType != ProviderType.ASYNCH) {
val mockServer = setupMockServer(providerInfo, pactMethod, context)
mockServer.start()
mockServer.waitForServer()
}
}

private fun setupMockServer(providerInfo: ProviderInfo, pactMethod: String, context: ExtensionContext): MockServer? {
private fun setupMockServer(providerInfo: ProviderInfo, pactMethod: String, context: ExtensionContext): AbstractBaseMockServer {
val store = context.getStore(NAMESPACE)
return if (providerInfo.providerType != ProviderType.ASYNCH && store["mockServer"] == null) {
return if (store["mockServer"] == null) {
val config = providerInfo.mockServerConfig()
store.put("mockServerConfig", config)
val mockServer = mockServer(lookupPact(providerInfo, pactMethod, context) as RequestResponsePact, config) as BaseMockServer
mockServer.start()
mockServer.waitForServer()
val mockServer = mockServer(lookupPact(providerInfo, pactMethod, context) as RequestResponsePact, config)
store.put("mockServer", JUnit5MockServerSupport(mockServer))
mockServer
} else {
store["mockServer"] as MockServer?
store["mockServer"] as AbstractBaseMockServer
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PactConsumerTestExtSpec extends Specification {
def parameter = PactConsumerTestExtSpec.getMethod(testMethod, model).parameters[0]
def parameterContext = [getParameter: { parameter } ] as ParameterContext
def providerInfo = new ProviderInfo('test', 'localhost', '0', PactSpecVersion.V3,
providerType)
providerType, false)

def store = [get: { arg ->
arg == 'providerInfo' ? providerInfo : model.newInstance(new Provider(), new Consumer(), [])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;

@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "ArticlesProvider", https = true)
public class ArticlesHttpsTest {
private Map<String, String> headers = MapUtils.putAll(new HashMap<>(), new String[] {
"Content-Type", "application/json"
});

@BeforeEach
public void setUp(MockServer mockServer) {
assertThat(mockServer, is(notNullValue()));
assertThat(mockServer.getUrl(), startsWith("https://"));
}

@Pact(consumer = "ArticlesConsumer")
public RequestResponsePact articles(PactDslWithProvider builder) {
return builder
.given("Articles exist")
.uponReceiving("retrieving article data")
.path("/articles.json")
.method("GET")
.willRespondWith()
.headers(headers)
.status(200)
.body(
new PactDslJsonBody()
.minArrayLike("articles", 1)
.object("variants")
.eachKeyLike("0032")
.stringType("description", "sample description")
.closeObject()
.closeObject()
.closeObject()
.closeArray()
)
.toPact();
}

@Pact(consumer = "ArticlesConsumer")
public RequestResponsePact articlesDoNotExist(PactDslWithProvider builder) {
return builder
.given("No articles exist")
.uponReceiving("retrieving article data")
.path("/articles.json")
.method("GET")
.willRespondWith()
.headers(headers)
.status(404)
.toPact();
}

@Test
@PactTestFor(pactMethod = "articles")
void testArticles(MockServer mockServer) throws IOException, GeneralSecurityException {
HttpResponse httpResponse = get(mockServer.getUrl() + "/articles.json");
assertThat(httpResponse.getStatusLine().getStatusCode(), is(equalTo(200)));
assertThat(IOUtils.toString(httpResponse.getEntity().getContent()),
is(equalTo("{\"articles\":[{\"variants\":{\"0032\":{\"description\":\"sample description\"}}}]}")));
}

private HttpResponse get(String url) throws IOException, GeneralSecurityException {
return httpClient().execute(new HttpGet(url));
}

private HttpClient httpClient() throws GeneralSecurityException {
SSLSocketFactory socketFactory = new SSLSocketFactory(new TrustSelfSignedStrategy());
return HttpClientBuilder.create()
.setSSLSocketFactory(socketFactory)
.build();
}

@Test
@PactTestFor(pactMethod = "articlesDoNotExist")
void testArticlesDoNotExist(MockServer mockServer) throws IOException, GeneralSecurityException {
HttpResponse httpResponse = get(mockServer.getUrl() + "/articles.json");
assertThat(httpResponse.getStatusLine().getStatusCode(), is(equalTo(404)));
assertThat(IOUtils.toString(httpResponse.getEntity().getContent()), is(equalTo("")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
/**
* Returns a mock server for the pact and config
*/
fun mockServer(pact: RequestResponsePact, config: MockProviderConfig): MockServer {
fun mockServer(pact: RequestResponsePact, config: MockProviderConfig): BaseMockServer {
return when (config) {
is MockHttpsProviderConfig -> when (config.mockServerImplementation) {
MockServerImplementation.KTorServer -> KTorMockServer(pact, config)
Expand Down Expand Up @@ -75,16 +75,19 @@ interface MockServer {
fun validateMockServerState(testResult: Any?): PactVerificationResult
}

abstract class BaseMockServer(val pact: RequestResponsePact, val config: MockProviderConfig) : MockServer {
abstract class AbstractBaseMockServer : MockServer {
abstract fun start()
abstract fun stop()
abstract fun waitForServer()
}

abstract class BaseMockServer(val pact: RequestResponsePact, val config: MockProviderConfig) : AbstractBaseMockServer() {

private val mismatchedRequests = ConcurrentHashMap<Request, MutableList<PactVerificationResult>>()
private val matchedRequests = ConcurrentLinkedQueue<Request>()
private val requestMatcher = RequestMatching(pact.interactions)

abstract fun start()
abstract fun stop()

fun waitForServer() {
override fun waitForServer() {
val sf = SSLSocketFactory(TrustSelfSignedStrategy())
val httpclient = HttpClientBuilder.create()
.setConnectionManager(BasicHttpClientConnectionManager(RegistryBuilder.create<ConnectionSocketFactory>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package au.com.dius.pact.consumer.model

import au.com.dius.pact.core.model.PactSpecVersion
import au.com.dius.pact.core.support.Utils.randomPort
import java.io.File
import java.security.KeyStore

Expand All @@ -23,8 +24,13 @@ class MockHttpsProviderConfig @JvmOverloads constructor(
@JvmOverloads
fun httpsConfig(hostname: String = LOCALHOST, port: Int = 0, pactVersion: PactSpecVersion = PactSpecVersion.V3): MockHttpsProviderConfig {
val jksFile = File.createTempFile("PactTest", ".jks")
val p = if (port == 0) {
randomPort()
} else {
port
}
val keystore = io.ktor.network.tls.certificates.generateCertificate(jksFile, "SHA1withRSA", "PactTest", "changeit", "changeit", 1024)
return MockHttpsProviderConfig(hostname, port, pactVersion, keystore, "PactTest", "changeit", "changeit",
return MockHttpsProviderConfig(hostname, p, pactVersion, keystore, "PactTest", "changeit", "changeit",
MockServerImplementation.KTorServer)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package au.com.dius.pact.core.support

import org.apache.commons.lang3.RandomUtils
import java.io.IOException
import java.net.ServerSocket
import kotlin.reflect.full.cast

object Utils {
Expand Down Expand Up @@ -31,4 +34,32 @@ object Utils {
default
}
}

fun randomPort(lower: Int = 10000, upper: Int = 60000): 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++
}

return port ?: 0
}

fun portAvailable(p: Int): Boolean {
var socket: ServerSocket? = null
return try {
socket = ServerSocket(p)
true
} catch (_: IOException) {
false
} finally {
try {
socket?.close()
} catch (_: Throwable) { }
}
}
}

0 comments on commit df85809

Please sign in to comment.