Skip to content

Commit

Permalink
Merge branch 'v3.5.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed Sep 9, 2018
2 parents 025918c + 7b126a2 commit a1bb988
Show file tree
Hide file tree
Showing 57 changed files with 716 additions and 162 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,28 @@
To generate the log, run `git log --pretty='* %h - %s (%an, %ad)' TAGNAME..HEAD` replacing TAGNAME and HEAD as appropriate.

# 3.5.22 - Bugfix Release

* aaaa0719 - fix: only enable wildcard matching logic with an explicit system property #759 (Ronald Holshausen, Sun Sep 9 12:36:55 2018 +1000)
* 22efe9c4 - feat: implemented state change teardown support in the JUnit 5 extension (Ronald Holshausen, Sun Sep 9 10:25:16 2018 +1000)
* aa332bb5 - feat: Update readme with provider state teardown #750 (Ronald Holshausen, Sun Sep 9 10:00:35 2018 +1000)
* fbe067df - Merge branch 'tinexw-teardown-junit' into v3.5.x (Ronald Holshausen, Sun Sep 9 09:39:27 2018 +1000)
* 0883da3c - fix: run the statechange teardown methods after the interaction (Ronald Holshausen, Sun Sep 9 09:38:34 2018 +1000)
* 1ef26f55 - Merge branch 'teardown-junit' of https://github.com/tinexw/pact-jvm into tinexw-teardown-junit (Ronald Holshausen, Sun Sep 9 08:47:43 2018 +1000)
* 5f7479e6 - fix: handle the case where the query parameters are a string in a V3 pact (Ronald Holshausen, Sat Sep 8 18:03:27 2018 +1000)
* 31d09845 - fix: Only write the pact file if the JUnit 5 consumer test passes #762 (Ronald Holshausen, Sun Aug 26 20:21:59 2018 +1000)
* 9c601735 - fix: Need to write the pact file once the JUnit 5 message consumer test passes #762 (Ronald Holshausen, Sun Aug 26 20:14:22 2018 +1000)
* 1d00a5e9 - feat: Implemented support for message pact tests in JUnit consumer tests #762 (Ronald Holshausen, Sun Aug 26 19:34:30 2018 +1000)
* c988a554 - fix: only process the consumer tags after the value resolver has been set (Ronald Holshausen, Sun Aug 26 13:27:10 2018 +1000)
* e6070fbb - Put back oddly removed param for pactSource initialization (carlz, Sun Aug 26 13:08:20 2018 +1200)
* 721dca94 - Fix lintKotlinTest by replacing wildcard import with explicit imports (carlz, Sun Aug 26 12:43:44 2018 +1200)
* 18a523a0 - Add ability to filter PactBroker loaded pacts by consumers (carlz, Sun Aug 26 12:16:02 2018 +1200)
* 37b50e9c - Fix PactBroker `tags` description, fix PactBrokerAnnotationDefaultsTest tags tests (carlz, Sun Aug 26 12:15:20 2018 +1200)
* af69fd1f - fix: only process the tags after the value resolver has been set #757 (Ronald Holshausen, Sun Aug 26 12:28:29 2018 +1000)
* 6b9fe1c7 - chore: removed jackson-databind #687 (Ronald Holshausen, Sun Aug 26 11:08:56 2018 +1000)
* 04e90dad - Add state teardown support to junit provider (tinexw, Sat Aug 4 14:14:52 2018 +0200)
* df608ed2 - fix: update the uberjar to conform to maven central rules (Ronald Holshausen, Sun Aug 12 17:49:30 2018 +1000)
* 4361b3b7 - bump version to 3.5.22 (Ronald Holshausen, Sun Aug 12 15:54:24 2018 +1000)

# 3.5.21 - Bugfix Release

* 3fe199a3 - doc: update version in readme (Ronald Holshausen, Sun Aug 12 15:11:05 2018 +1000)
Expand Down
3 changes: 3 additions & 0 deletions pact-jvm-consumer-groovy/README.md
Expand Up @@ -491,6 +491,9 @@ For an example, have a look at [WildcardPactSpec](src/test/au/com/dius/pact/cons
**NOTE:** The `keyLike` method adds a `*` to the matching path, so the matching definition will be applied to all keys
of the map if there is not a more specific matcher defined for a particular key. Having more than one `keyLike` condition
applied to a map will result in only one being applied when the pact is verified (probably the last).

**Further Note: From version 3.5.22 onwards pacts with wildcards applied to map keys will require the Java system property
"pact.matching.wildcard" set to value "true" when the pact file is verified.**

### Matching with an OR (3.5.0+)

Expand Down
Expand Up @@ -261,6 +261,10 @@ class PactBodyBuilder extends BaseBuilder {
}
}

/**
* Matches the values of the map ignoring the keys. Note: this needs the Java system property
* "pact.matching.wildcard" set to value "true" when the pact file is verified.
*/
def keyLike(String key, def value) {
if (FeatureToggles.isFeatureSet(Feature.UseMatchValuesMatcher)) {
setMatcherAttribute(new ValuesMatcher(), path)
Expand Down
Expand Up @@ -531,7 +531,8 @@ public LambdaDslObject eachArrayWithMinMaxLike(String name, Integer minSize, Int
}

/**
* Accepts any key, and each key is mapped to a list of items that must match the following object definition
* Accepts any key, and each key is mapped to a list of items that must match the following object definition.
* Note: this needs the Java system property "pact.matching.wildcard" set to value "true" when the pact file is verified.
*
* @param exampleKey Example key to use for generating bodies
*/
Expand All @@ -544,7 +545,8 @@ public LambdaDslObject eachKeyMappedToAnArrayLike(String exampleKey, Consumer<La
}

/**
* Accepts any key, and each key is mapped to a map that must match the following object definition
* Accepts any key, and each key is mapped to a map that must match the following object definition.
* Note: this needs the Java system property "pact.matching.wildcard" set to value "true" when the pact file is verified.
*
* @param exampleKey Example key to use for generating bodies
*/
Expand Down
3 changes: 3 additions & 0 deletions pact-jvm-consumer-junit/README.md
Expand Up @@ -529,6 +529,9 @@ For an example, have a look at [WildcardKeysTest](src/test/java/au/com/dius/pact
**NOTE:** The `eachKeyLike` method adds a `*` to the matching path, so the matching definition will be applied to all keys
of the map if there is not a more specific matcher defined for a particular key. Having more than one `eachKeyLike` condition
applied to a map will result in only one being applied when the pact is verified (probably the last).

**Further Note: From version 3.5.22 onwards pacts with wildcards applied to map keys will require the Java system property
"pact.matching.wildcard" set to value "true" when the pact file is verified.**

#### Combining matching rules with AND/OR

Expand Down
@@ -1,5 +1,6 @@
package au.com.dius.pact.consumer;

import au.com.dius.pact.consumer.junit.JUnitTestSupport;
import au.com.dius.pact.model.PactSpecVersion;
import au.com.dius.pact.model.v3.messaging.Message;
import au.com.dius.pact.model.v3.messaging.MessagePact;
Expand Down Expand Up @@ -152,26 +153,13 @@ private Optional<Method> findPactMethod(PactVerification pactVerification) {
Pact pact = method.getAnnotation(Pact.class);
if (pact != null && pact.provider().equals(provider)
&& (pactFragment.isEmpty() || pactFragment.equals(method.getName()))) {

validatePactSignature(method);
JUnitTestSupport.conformsToMessagePactSignature(method);
return Optional.of(method);
}
}
return Optional.empty();
}

private void validatePactSignature(Method method) {
boolean hasValidPactSignature =
MessagePact.class.isAssignableFrom(method.getReturnType())
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0].isAssignableFrom(MessagePactBuilder.class);

if (!hasValidPactSignature) {
throw new UnsupportedOperationException("Method " + method.getName() +
" does not conform required method signature 'public MessagePact xxx(MessagePactBuilder builder)'");
}
}

@SuppressWarnings("unchecked")
private Map<String, Message> parsePacts() {
if (providerStateMessages == null) {
Expand Down
Expand Up @@ -2,15 +2,19 @@ package au.com.dius.pact.consumer.junit5

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.Pact
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.pactDirectory
import au.com.dius.pact.model.BasePact
import au.com.dius.pact.model.Interaction
import au.com.dius.pact.model.MockProviderConfig
import au.com.dius.pact.model.PactSpecVersion
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.v3.messaging.MessagePact
import mu.KLogging
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
Expand All @@ -23,22 +27,69 @@ import org.junit.platform.commons.support.HierarchyTraversalMode
import org.junit.platform.commons.support.ReflectionSupport
import java.lang.annotation.Inherited

/**
* The type of provider (synchronous or asynchronous)
*/
enum class ProviderType {
/**
* Synchronous provider (HTTP)
*/
SYNCH,
/**
* Asynchronous provider (Messages)
*/
ASYNCH,
/**
* Unspecified, will default to synchronous
*/
UNSPECIFIED
}

/**
* Main test annotation for a JUnit 5 test
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Inherited
annotation class PactTestFor(
/**
* Providers name. This will be recorded in the pact file
*/
val providerName: String = "",

/**
* Host interface to use for the mock server. Only used for synchronous provider tests and defaults to the
* loopback adapter (127.0.0.1).
*/
val hostInterface: String = "",

/**
* Port number to bind to. Only used for synchronous provider tests and defaults to 8080.
*/
val port: String = "",

/**
* Pact specification version to support. Default is V3.
*/
val pactVersion: PactSpecVersion = PactSpecVersion.V3,
val pactMethod: String = ""

/**
* Test method that provides the Pact to use for the test. Default behaviour is to use the first one found.
*/
val pactMethod: String = "",

/**
* Type of provider (synchronous HTTP or asynchronous messages)
*/
val providerType: ProviderType = ProviderType.UNSPECIFIED
)

data class ProviderInfo(
val providerName: String = "",
val hostInterface: String = "",
val port: String = "",
val pactVersion: PactSpecVersion? = null
val pactVersion: PactSpecVersion? = null,
val providerType: ProviderType? = null
) {

fun mockServerConfig() =
Expand All @@ -49,12 +100,17 @@ data class 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)
pactVersion = pactVersion ?: other.pactVersion,
providerType = providerType ?: other.providerType)
}

companion object {
fun fromAnnotation(annotation: PactTestFor): ProviderInfo =
ProviderInfo(annotation.providerName, annotation.hostInterface, annotation.port, annotation.pactVersion)
ProviderInfo(annotation.providerName, annotation.hostInterface, annotation.port, annotation.pactVersion,
when (annotation.providerType) {
ProviderType.UNSPECIFIED -> null
else -> annotation.providerType
})
}
}

Expand All @@ -67,12 +123,25 @@ class JUnit5MockServerSupport(private val baseMockServer: BaseMockServer) : Mock

class PactConsumerTestExt : Extension, BeforeEachCallback, ParameterResolver, AfterEachCallback {

override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext) =
parameterContext.parameter.type.isAssignableFrom(MockServer::class.java)
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val providerInfo = store["providerInfo"] as ProviderInfo
return when (providerInfo.providerType) {
ProviderType.ASYNCH -> parameterContext.parameter.type.isAssignableFrom(List::class.java)
else -> parameterContext.parameter.type.isAssignableFrom(MockServer::class.java)
}
}

override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
return store["mockServer"]
val providerInfo = store["providerInfo"] as ProviderInfo
return when (providerInfo.providerType) {
ProviderType.ASYNCH -> {
val pact = store["pact"] as MessagePact
pact.messages
}
else -> store["mockServer"]
}
}

override fun beforeEach(context: ExtensionContext) {
Expand All @@ -83,12 +152,16 @@ class PactConsumerTestExt : Extension, BeforeEachCallback, ParameterResolver, Af
val pact = lookupPact(providerInfo, pactMethod, context)
val store = context.getStore(ExtensionContext.Namespace.create("pact-jvm"))
store.put("pact", pact)
val config = providerInfo.mockServerConfig()
store.put("mockServerConfig", config)
val mockServer = mockServer(pact, config) as BaseMockServer
mockServer.start()
mockServer.waitForServer()
store.put("mockServer", JUnit5MockServerSupport(mockServer))
store.put("providerInfo", providerInfo)

if (providerInfo.providerType != ProviderType.ASYNCH) {
val config = providerInfo.mockServerConfig()
store.put("mockServerConfig", config)
val mockServer = mockServer(pact as RequestResponsePact, config) as BaseMockServer
mockServer.start()
mockServer.waitForServer()
store.put("mockServer", JUnit5MockServerSupport(mockServer))
}
}

fun lookupProviderInfo(context: ExtensionContext): Pair<ProviderInfo, String> {
Expand Down Expand Up @@ -120,7 +193,7 @@ class PactConsumerTestExt : Extension, BeforeEachCallback, ParameterResolver, Af
}
}

fun lookupPact(providerInfo: ProviderInfo, pactMethod: String, context: ExtensionContext): RequestResponsePact {
fun lookupPact(providerInfo: ProviderInfo, pactMethod: String, context: ExtensionContext): BasePact<out Interaction> {
val providerName = if (providerInfo.providerName.isEmpty()) "default" else providerInfo.providerName
val methods = AnnotationSupport.findAnnotatedMethods(context.requiredTestClass, Pact::class.java,
HierarchyTraversalMode.TOP_DOWN)
Expand All @@ -137,41 +210,66 @@ class PactConsumerTestExt : Extension, BeforeEachCallback, ParameterResolver, Af
else -> {
logger.debug { "Looking for first @Pact method for provider '$providerName'" }
methods.firstOrNull {
AnnotationSupport.findAnnotation(it, Pact::class.java).get().provider == providerInfo.providerName
val annotationProviderName = AnnotationSupport.findAnnotation(it, Pact::class.java).get().provider
annotationProviderName.isEmpty() || annotationProviderName == providerInfo.providerName
}
}
}

val providerType = providerInfo.providerType ?: ProviderType.SYNCH
if (method == null) {
throw UnsupportedOperationException("No method annotated with @Pact was found on test class " +
context.requiredTestClass.simpleName + " for provider '${providerInfo.providerName}'")
} else if (!JUnitTestSupport.conformsToSignature(method)) {
} else if (providerType == ProviderType.SYNCH && !JUnitTestSupport.conformsToSignature(method)) {
throw UnsupportedOperationException("Method ${method.name} does not conform to required method signature " +
"'public RequestResponsePact xxx(PactDslWithProvider builder)'")
} else if (providerType == ProviderType.ASYNCH && !JUnitTestSupport.conformsToMessagePactSignature(method)) {
throw UnsupportedOperationException("Method ${method.name} does not conform to required method signature " +
"'public MessagePact xxx(MessagePactBuilder builder)'")
}

val pactAnnotation = AnnotationSupport.findAnnotation(method, Pact::class.java).get()
logger.debug { "Invoking method '${method.name}' to get Pact for the test " +
"'${context.testMethod.map { it.name }.orElse("unknown")}'" }
return ReflectionSupport.invokeMethod(method, context.requiredTestInstance,
ConsumerPactBuilder.consumer(pactAnnotation.consumer).hasPactWith(pactAnnotation.provider)) as RequestResponsePact

val providerNameToUse = if (pactAnnotation.provider.isNotEmpty()) pactAnnotation.provider else providerName
return when (providerType) {
ProviderType.SYNCH, ProviderType.UNSPECIFIED -> ReflectionSupport.invokeMethod(method, context.requiredTestInstance,
ConsumerPactBuilder.consumer(pactAnnotation.consumer).hasPactWith(providerNameToUse)) as BasePact<*>
ProviderType.ASYNCH -> ReflectionSupport.invokeMethod(method, context.requiredTestInstance,
MessagePactBuilder.consumer(pactAnnotation.consumer).hasPactWith(providerNameToUse)) as BasePact<*>
}
}

override fun afterEach(context: ExtensionContext) {
val store = context.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val mockServer = store["mockServer"] as JUnit5MockServerSupport
val pact = store["pact"] as RequestResponsePact
val config = store["mockServerConfig"] as MockProviderConfig
Thread.sleep(100) // give the mock server some time to have consistent state
mockServer.close()
val result = mockServer.validateMockServerState()
if (result === PactVerificationResult.Ok) {
if (!context.executionException.isPresent) {
val store = context.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val providerInfo = store["providerInfo"] as ProviderInfo
val pactDirectory = pactDirectory()
logger.debug { "Writing pact ${pact.consumer.name} -> ${pact.provider.name} to file " +
"${pact.fileForPact(pactDirectory)}" }
pact.write(pactDirectory, config.pactVersion)
} else {
JUnitTestSupport.validateMockServerResult(result)
if (providerInfo.providerType != ProviderType.ASYNCH) {
val mockServer = store["mockServer"] as JUnit5MockServerSupport
val pact = store["pact"] as RequestResponsePact
val config = store["mockServerConfig"] as MockProviderConfig
Thread.sleep(100) // give the mock server some time to have consistent state
mockServer.close()
val result = mockServer.validateMockServerState()
if (result === PactVerificationResult.Ok) {
logger.debug {
"Writing pact ${pact.consumer.name} -> ${pact.provider.name} to file " +
"${pact.fileForPact(pactDirectory)}"
}
pact.write(pactDirectory, config.pactVersion)
} else {
JUnitTestSupport.validateMockServerResult(result)
}
} else {
val pact = store["pact"] as MessagePact
logger.debug {
"Writing pact ${pact.consumer.name} -> ${pact.provider.name} to file " +
"${pact.fileForPact(pactDirectory)}"
}
pact.write(pactDirectory, PactSpecVersion.V3)
}
}
}

Expand Down
Expand Up @@ -107,8 +107,8 @@ class PactConsumerTestExtSpec {
'getTestInstance': { Optional.of(new TestClass()) },
'getTestMethod': { Optional.empty() }
] as ExtensionContext
def pact = subject.lookupPact(new ProviderInfo('junit5_provider', 'localhost', '8080', PactSpecVersion.V3),
'pactMethod', context)
def pact = subject.lookupPact(new ProviderInfo('junit5_provider', 'localhost', '8080',
PactSpecVersion.V3, ProviderType.SYNCH), 'pactMethod', context)
assertThat(pact, Matchers.is(this.pact))
}

Expand Down

0 comments on commit a1bb988

Please sign in to comment.