Skip to content

Commit 1220ed2

Browse files
author
Ronald Holshausen
committed
feat: add support for validating an interaction via a plugin
1 parent 75e53c4 commit 1220ed2

File tree

24 files changed

+461
-117
lines changed

24 files changed

+461
-117
lines changed

core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ data class OptionalBody @JvmOverloads constructor(
228228
@JvmStatic
229229
fun body(body: ByteArray?, contentType: ContentType) = body(body, contentType, ContentTypeHint.DEFAULT)
230230

231+
@JvmStatic
232+
@JvmOverloads
233+
fun body(body: String?, contentType: ContentType = UNKNOWN) = body(body?.toByteArray(), contentType, ContentTypeHint.DEFAULT)
234+
231235
@JvmStatic
232236
fun body(
233237
body: ByteArray?,
@@ -261,11 +265,13 @@ fun OptionalBody?.isNotPresent() = this == null || this.isNotPresent()
261265

262266
fun OptionalBody?.orElse(defaultValue: ByteArray) = this?.orElse(defaultValue) ?: defaultValue
263267

264-
fun OptionalBody?.orEmpty() = this?.orElse(ByteArray(0))
268+
fun OptionalBody?.orEmpty() = this?.orElse(ByteArray(0)) ?: ByteArray(0)
265269

266270
fun OptionalBody?.valueAsString() = this?.valueAsString() ?: ""
267271

268272
fun OptionalBody?.isNullOrEmpty() = this == null || this.isEmpty() || this.isNull()
269273

270274
fun OptionalBody?.unwrap() = this?.unwrap() ?: throw UnwrapMissingBodyException(
271275
"Failed to unwrap value from a null body")
276+
277+
fun OptionalBody?.orEmptyBody() = this ?: OptionalBody.empty()

core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy

+28-28
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ class OptionalBodySpec extends Specification {
4242
body.null == value
4343

4444
where:
45-
body | value
46-
OptionalBody.missing() | false
47-
OptionalBody.empty() | false
48-
OptionalBody.nullBody() | true
49-
OptionalBody.body(null) | true
50-
OptionalBody.body('a'.bytes) | false
45+
body | value
46+
OptionalBody.missing() | false
47+
OptionalBody.empty() | false
48+
OptionalBody.nullBody() | true
49+
OptionalBody.body(null as byte[]) | true
50+
OptionalBody.body('a'.bytes) | false
5151
}
5252

5353
@Unroll
@@ -56,13 +56,13 @@ class OptionalBodySpec extends Specification {
5656
body.present == value
5757

5858
where:
59-
body | value
60-
OptionalBody.missing() | false
61-
OptionalBody.empty() | false
62-
OptionalBody.nullBody() | false
63-
OptionalBody.body(''.bytes) | false
64-
OptionalBody.body(null) | false
65-
OptionalBody.body('a'.bytes) | true
59+
body | value
60+
OptionalBody.missing() | false
61+
OptionalBody.empty() | false
62+
OptionalBody.nullBody() | false
63+
OptionalBody.body(''.bytes) | false
64+
OptionalBody.body(null as byte[]) | false
65+
OptionalBody.body('a'.bytes) | true
6666
}
6767

6868
@Unroll
@@ -71,13 +71,13 @@ class OptionalBodySpec extends Specification {
7171
body.notPresent == value
7272

7373
where:
74-
body | value
75-
OptionalBody.missing() | true
76-
OptionalBody.empty() | true
77-
OptionalBody.nullBody() | true
78-
OptionalBody.body(''.bytes) | true
79-
OptionalBody.body(null) | true
80-
OptionalBody.body('a'.bytes) | false
74+
body | value
75+
OptionalBody.missing() | true
76+
OptionalBody.empty() | true
77+
OptionalBody.nullBody() | true
78+
OptionalBody.body(''.bytes) | true
79+
OptionalBody.body(null as byte[]) | true
80+
OptionalBody.body('a'.bytes) | false
8181
}
8282

8383
@Unroll
@@ -86,13 +86,13 @@ class OptionalBodySpec extends Specification {
8686
body.orElse('default'.bytes) == value.bytes
8787

8888
where:
89-
body | value
90-
OptionalBody.missing() | 'default'
91-
OptionalBody.empty() | ''
92-
OptionalBody.nullBody() | 'default'
93-
OptionalBody.body(''.bytes) | ''
94-
OptionalBody.body(null) | 'default'
95-
OptionalBody.body('a'.bytes) | 'a'
89+
body | value
90+
OptionalBody.missing() | 'default'
91+
OptionalBody.empty() | ''
92+
OptionalBody.nullBody() | 'default'
93+
OptionalBody.body(''.bytes) | ''
94+
OptionalBody.body(null as byte[]) | 'default'
95+
OptionalBody.body('a'.bytes) | 'a'
9696
}
9797

9898
def 'unwrap throws an exception when the body is missing'() {
@@ -106,7 +106,7 @@ class OptionalBodySpec extends Specification {
106106
body << [
107107
OptionalBody.nullBody(),
108108
OptionalBody.missing(),
109-
OptionalBody.body(null)
109+
OptionalBody.body(null as byte[])
110110
]
111111
}
112112

core/support/src/main/kotlin/au/com/dius/pact/core/support/Utils.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import mu.KLogging
77
import org.apache.commons.lang3.RandomUtils
88
import java.io.IOException
99
import java.net.ServerSocket
10+
import java.util.Locale
1011
import java.util.jar.JarInputStream
1112
import kotlin.math.pow
1213
import kotlin.reflect.full.cast
@@ -127,6 +128,7 @@ object Utils : KLogging() {
127128
is Boolean -> value
128129
is String -> value
129130
is Number -> value
131+
is Enum<*> -> value.toString()
130132
is Map<*, *> -> value.entries.associate { it.key.toString() to jsonSafeValue(it.value) }
131133
is Collection<*> -> value.map { jsonSafeValue(it) }
132134
else -> objectToJsonMap(value)
@@ -162,7 +164,7 @@ object Utils : KLogging() {
162164
private val DATA_SIZES = listOf("b", "kb", "mb", "gb", "tb")
163165

164166
fun sizeOf(value: String): Result<Int, String> {
165-
val matchResult = SIZE_REGEX.matchEntire(value.toLowerCase())
167+
val matchResult = SIZE_REGEX.matchEntire(value.lowercase(Locale.getDefault()))
166168
return if (matchResult != null) {
167169
val unitPower = DATA_SIZES.indexOf(matchResult.groupValues[2])
168170
if (unitPower >= 0) {
@@ -199,5 +201,5 @@ object Utils : KLogging() {
199201
/**
200202
* Convert a value to snake-case form (a.b.c -> A_B_C)
201203
*/
202-
private fun snakeCase(key: String) = key.split('.').joinToString("_") { it.toUpperCase() }
204+
private fun snakeCase(key: String) = key.split('.').joinToString("_") { it.uppercase(Locale.getDefault()) }
203205
}

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ junit5Version=5.8.2
1717
bytebuddyVersion=1.12.7
1818
kotlinLogging=2.0.10
1919
kotlinResult=1.1.12
20-
pluginDriverVersion=0.1.0
20+
pluginDriverVersion=0.1.1

provider/junit5/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dependencies {
33
api project(':provider')
44
api "org.junit.jupiter:junit-jupiter-api:${project.junit5Version}"
55
implementation "org.slf4j:slf4j-api:${project.slf4jVersion}"
6+
implementation "io.pact.plugin.driver:core:${project.pluginDriverVersion}"
67

78
testRuntimeOnly "ch.qos.logback:logback-classic:${project.logbackVersion}"
89
testImplementation 'ru.lanwen.wiremock:wiremock-junit5:1.3.1'

provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationContext.kt

+46-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package au.com.dius.pact.provider.junit5
22

3+
import au.com.dius.pact.core.matchers.DefaultResponseGenerator
34
import au.com.dius.pact.core.model.Interaction
45
import au.com.dius.pact.core.model.PactSource
56
import au.com.dius.pact.core.model.RequestResponseInteraction
@@ -77,36 +78,55 @@ data class PactVerificationContext @JvmOverloads constructor(
7778
request: Any?,
7879
context: MutableMap<String, Any>
7980
): List<VerificationResult> {
80-
if (providerInfo.verificationType == null || providerInfo.verificationType == PactVerification.REQUEST_RESPONSE) {
81-
var interactionMessage = "Verifying a pact between ${consumer.name} and ${providerInfo.name}" +
82-
" - ${interaction.description}"
83-
if (interaction.isV4() && interaction.asV4Interaction().pending) {
84-
interactionMessage += " [PENDING]"
85-
}
86-
return try {
87-
val reqResInteraction = if (interaction is V4Interaction.SynchronousHttp) {
88-
interaction.asV3Interaction()
89-
} else {
90-
interaction as RequestResponseInteraction
81+
when (providerInfo.verificationType) {
82+
null, PactVerification.REQUEST_RESPONSE -> {
83+
var interactionMessage = "Verifying a pact between ${consumer.name} and ${providerInfo.name}" +
84+
" - ${interaction.description}"
85+
if (interaction.isV4() && interaction.asV4Interaction().pending) {
86+
interactionMessage += " [PENDING]"
9187
}
92-
val expectedResponse = reqResInteraction.response.generatedResponse(context, GeneratorTestMode.Provider)
93-
val actualResponse = target.executeInteraction(client, request)
88+
return try {
89+
val reqResInteraction = if (interaction is V4Interaction.SynchronousHttp) {
90+
interaction.asV3Interaction()
91+
} else {
92+
interaction as RequestResponseInteraction
93+
}
94+
val expectedResponse = DefaultResponseGenerator.generateResponse(reqResInteraction.response, context, GeneratorTestMode.Provider)
95+
val actualResponse = target.executeInteraction(client, request)
9496

95-
listOf(verifier!!.verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, mutableMapOf(),
96-
reqResInteraction.interactionId.orEmpty(), consumer.pending))
97-
} catch (e: Exception) {
98-
verifier!!.reporters.forEach {
99-
it.requestFailed(providerInfo, interaction, interactionMessage, e,
100-
verifier!!.projectHasProperty.apply(ProviderVerifier.PACT_SHOW_STACKTRACE))
97+
listOf(
98+
verifier!!.verifyRequestResponsePact(
99+
expectedResponse, actualResponse, interactionMessage, mutableMapOf(),
100+
reqResInteraction.interactionId.orEmpty(), consumer.pending
101+
)
102+
)
103+
} catch (e: Exception) {
104+
verifier!!.reporters.forEach {
105+
it.requestFailed(
106+
providerInfo, interaction, interactionMessage, e,
107+
verifier!!.projectHasProperty.apply(ProviderVerifier.PACT_SHOW_STACKTRACE)
108+
)
109+
}
110+
listOf(
111+
VerificationResult.Failed(
112+
"Request to provider failed with an exception", interactionMessage,
113+
mapOf(
114+
interaction.interactionId.orEmpty() to
115+
listOf(VerificationFailureType.ExceptionFailure("Request to provider failed with an exception", e))
116+
),
117+
consumer.pending
118+
)
119+
)
101120
}
102-
listOf(VerificationResult.Failed("Request to provider failed with an exception", interactionMessage,
103-
mapOf(interaction.interactionId.orEmpty() to
104-
listOf(VerificationFailureType.ExceptionFailure("Request to provider failed with an exception", e))),
105-
consumer.pending))
106121
}
107-
} else {
108-
return listOf(verifier!!.verifyResponseByInvokingProviderMethods(providerInfo, consumer, interaction,
109-
interaction.description, mutableMapOf(), false))
122+
PactVerification.PLUGIN -> {
123+
return listOf(verifier!!.verifyInteractionViaPlugin(providerInfo, consumer, interaction,
124+
client, request, context + ("userConfig" to target.userConfig)))
125+
}
126+
else -> {
127+
return listOf(verifier!!.verifyResponseByInvokingProviderMethods(providerInfo, consumer, interaction,
128+
interaction.description, mutableMapOf(), false))
129+
}
110130
}
111131
}
112132

provider/junit5/src/main/kotlin/au/com/dius/pact/provider/junit5/PactVerificationExtension.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ open class PactVerificationExtension(
115115

116116
val executionContext = testContext.executionContext ?: mutableMapOf()
117117
executionContext["ArrayContainsJsonGenerator"] = ArrayContainsJsonGenerator
118-
val requestAndClient = testContext.target.prepareRequest(interaction, executionContext)
118+
val requestAndClient = testContext.target.prepareRequest(pact, interaction, executionContext)
119119
if (requestAndClient != null) {
120120
val (request, client) = requestAndClient
121121
store.put("request", request)
@@ -135,7 +135,7 @@ open class PactVerificationExtension(
135135

136136
val verifier = ProviderVerifier()
137137
verifier.verificationSource = "junit5"
138-
testContext.target.prepareVerifier(verifier, extContext.requiredTestInstance)
138+
testContext.target.prepareVerifier(verifier, extContext.requiredTestInstance, pact)
139139

140140
setupReporters(verifier, serviceName, interaction.description, extContext, testContext.valueResolver)
141141

0 commit comments

Comments
 (0)