diff --git a/.github/workflows/compatability-suite.yml b/.github/workflows/compatability-suite.yml index f8104c37e..d93a1b8ee 100644 --- a/.github/workflows/compatability-suite.yml +++ b/.github/workflows/compatability-suite.yml @@ -58,3 +58,20 @@ jobs: name: cucumber-report path: compatibility-suite/build/cucumber-report-v3.html if: always() + v4: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 18 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 18 + - name: Build with Gradle + run: ./gradlew --no-daemon :compatibility-suite:v4 + - name: Archive cucumber results + uses: actions/upload-artifact@v3 + with: + name: cucumber-report + path: compatibility-suite/build/cucumber-report-v4.html + if: always() diff --git a/compatibility-suite/build.gradle b/compatibility-suite/build.gradle index 46aca5e2b..311300fe3 100644 --- a/compatibility-suite/build.gradle +++ b/compatibility-suite/build.gradle @@ -100,6 +100,29 @@ tasks.register('v3') { } } +tasks.register('v4') { + dependsOn assemble, testClasses + doLast { + def cucumberArgs = [ + '--plugin', 'pretty', + '--plugin', 'html:build/cucumber-report-v4.html', + '--glue', 'steps.shared', + '--glue', 'steps.v4', + 'pact-compatibility-suite/features/V4' + ] + if (project.hasProperty('cucumber.filter.tags')) { + cucumberArgs.add(0, project.property('cucumber.filter.tags')) + cucumberArgs.add(0, '-t') + } + javaexec { + main = "io.cucumber.core.cli.Main" + classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output + args = cucumberArgs + systemProperty 'pact_do_not_track', 'true' + } + } +} + tasks.register('all') { - dependsOn v1, v2, v3 + dependsOn v1, v2, v3, v4 } diff --git a/compatibility-suite/src/test/groovy/steps/shared/StubVerificationReporter.groovy b/compatibility-suite/src/test/groovy/steps/shared/StubVerificationReporter.groovy index b290dcf38..5e6425c1a 100644 --- a/compatibility-suite/src/test/groovy/steps/shared/StubVerificationReporter.groovy +++ b/compatibility-suite/src/test/groovy/steps/shared/StubVerificationReporter.groovy @@ -9,6 +9,8 @@ import au.com.dius.pact.provider.IProviderInfo import au.com.dius.pact.provider.IProviderVerifier import au.com.dius.pact.provider.VerificationResult import au.com.dius.pact.provider.reporters.BaseVerifierReporter +import au.com.dius.pact.provider.reporters.Event +import org.jetbrains.annotations.NotNull @SuppressWarnings('GetterMethodCouldBeProperty') class StubVerificationReporter extends BaseVerifierReporter { @@ -132,4 +134,15 @@ class StubVerificationReporter extends BaseVerifierReporter { @Override void metadataComparisonFailed(String key, Object value, Object comparison) { } + + @Override + void receive(@NotNull Event event) { + switch (event) { + case Event.DisplayInteractionComments: + events << [comments: event.comments] + break + default: + super.receive(event) + } + } } diff --git a/compatibility-suite/src/test/groovy/steps/shared/VerificationSteps.groovy b/compatibility-suite/src/test/groovy/steps/shared/VerificationSteps.groovy index eac8ddc3a..e1384b2de 100644 --- a/compatibility-suite/src/test/groovy/steps/shared/VerificationSteps.groovy +++ b/compatibility-suite/src/test/groovy/steps/shared/VerificationSteps.groovy @@ -94,13 +94,16 @@ class VerificationSteps { @Then('the verification will be successful') void the_verification_will_be_successful() { assert verificationData.verificationResults.inject(true) { acc, result -> - acc && result instanceof VerificationResult.Ok + acc && (result instanceof VerificationResult.Ok || + (result instanceof VerificationResult.Failed && result.pending)) } } @Then('the verification will NOT be successful') void the_verification_will_not_be_successful() { - assert verificationData.verificationResults.any { it instanceof VerificationResult.Failed } + assert verificationData.verificationResults.any { + it instanceof VerificationResult.Failed && !it.pending + } } @Then('the verification results will contain a {string} error') diff --git a/compatibility-suite/src/test/groovy/steps/v4/HttpConsumer.groovy b/compatibility-suite/src/test/groovy/steps/v4/HttpConsumer.groovy new file mode 100644 index 000000000..9008b6e3d --- /dev/null +++ b/compatibility-suite/src/test/groovy/steps/v4/HttpConsumer.groovy @@ -0,0 +1,52 @@ +package steps.v4 + +import au.com.dius.pact.consumer.dsl.HttpInteractionBuilder +import io.cucumber.java.After +import io.cucumber.java.Before +import io.cucumber.java.Scenario +import io.cucumber.java.en.Given + +class HttpConsumer { + HttpInteractionBuilder httpBuilder + SharedV4PactData v4Data + + HttpConsumer(SharedV4PactData v4Data) { + this.v4Data = v4Data + } + + @Before + void before(Scenario scenario) { + v4Data.scenarioId = scenario.id + } + + @After + void after(Scenario scenario) { + if (!scenario.failed) { + def dir = "build/compatibility-suite/v4/${v4Data.scenarioId}" as File + dir.deleteDir() + } + } + + @Given('an HTTP interaction is being defined for a consumer test') + void an_http_interaction_is_being_defined_for_a_consumer_test() { + httpBuilder = new HttpInteractionBuilder('HTTP interaction', [], []) + v4Data.builderCallbacks << { + httpBuilder.build() + } + } + + @Given('a key of {string} is specified for the HTTP interaction') + void a_key_of_is_specified(String key) { + httpBuilder.key(key) + } + + @Given('the HTTP interaction is marked as pending') + void the_interaction_is_marked_as_pending() { + httpBuilder.pending(true) + } + + @Given('a comment {string} is added to the HTTP interaction') + void a_comment_is_added(String value) { + httpBuilder.comment(value) + } +} diff --git a/compatibility-suite/src/test/groovy/steps/v4/HttpMatching.groovy b/compatibility-suite/src/test/groovy/steps/v4/HttpMatching.groovy new file mode 100644 index 000000000..9bcf7ee9c --- /dev/null +++ b/compatibility-suite/src/test/groovy/steps/v4/HttpMatching.groovy @@ -0,0 +1,151 @@ +package steps.v4 + +import au.com.dius.pact.core.matchers.BodyMismatch +import au.com.dius.pact.core.matchers.HeaderMismatch +import au.com.dius.pact.core.matchers.Mismatch +import au.com.dius.pact.core.matchers.RequestMatchResult +import au.com.dius.pact.core.model.HttpRequest +import au.com.dius.pact.core.model.HttpResponse +import au.com.dius.pact.core.support.json.JsonParser +import au.com.dius.pact.core.support.json.JsonValue +import io.cucumber.datatable.DataTable +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When + +import static au.com.dius.pact.core.matchers.RequestMatching.requestMismatches +import static au.com.dius.pact.core.matchers.ResponseMatching.responseMismatches +import static steps.shared.SharedSteps.configureBody +import static steps.shared.SharedSteps.determineContentType + +class HttpMatching { + HttpRequest expectedRequest + List receivedRequests = [] + HttpResponse expectedResponse + List receivedResponses = [] + List responseResults = [] + List requestResults = [] + + @Given('an expected response configured with the following:') + void an_expected_response_configured_with_the_following(DataTable dataTable) { + expectedResponse = new HttpResponse() + def entry = dataTable.entries().first() + + if (entry['status']) { + expectedResponse.status = entry['status'].toInteger() + } + + if (entry['body']) { + def part = configureBody(entry['body'], determineContentType(entry['body'], expectedResponse.contentTypeHeader())) + expectedResponse.body = part.body + expectedResponse.headers.putAll(part.headers) + } + + if (entry['matching rules']) { + JsonValue json + if (entry['matching rules'].startsWith('JSON:')) { + json = JsonParser.INSTANCE.parseString(entry['matching rules'][5..-1]) + } else { + File contents = new File("pact-compatibility-suite/fixtures/${entry['matching rules']}") + contents.withInputStream { + json = JsonParser.INSTANCE.parseStream(it) + } + } + expectedResponse.matchingRules.fromV3Json(json) + } + } + + @Given('a status {int} response is received') + void a_status_response_is_received(Integer status) { + receivedResponses << new HttpResponse(status) + } + + @When('the response is compared to the expected one') + void the_response_is_compared_to_the_expected_one() { + responseResults.addAll(responseMismatches(expectedResponse, receivedResponses[0])) + } + + @Then('the response comparison should be OK') + void the_response_comparison_should_be_ok() { + assert responseResults.empty + } + + @Then('the response comparison should NOT be OK') + void the_response_comparison_should_not_be_ok() { + assert !responseResults.empty + } + + @Then('the response mismatches will contain a {string} mismatch with error {string}') + void the_response_mismatches_will_contain_a_mismatch_with_error(String type, String error) { + assert responseResults.find { + it.type() == type && it.description() == error + } + } + + @Given('an expected request configured with the following:') + void an_expected_request_configured_with_the_following(DataTable dataTable) { + expectedRequest = new HttpRequest() + def entry = dataTable.entries().first() + if (entry['body']) { + def part = configureBody(entry['body'], determineContentType(entry['body'], expectedRequest.contentTypeHeader())) + expectedRequest.body = part.body + expectedRequest.headers.putAll(part.headers) + } + + if (entry['matching rules']) { + JsonValue json + if (entry['matching rules'].startsWith('JSON:')) { + json = JsonParser.INSTANCE.parseString(entry['matching rules'][5..-1]) + } else { + File contents = new File("pact-compatibility-suite/fixtures/${entry['matching rules']}") + contents.withInputStream { + json = JsonParser.INSTANCE.parseStream(it) + } + } + expectedRequest.matchingRules.fromV3Json(json) + } + } + + @Given('a request is received with the following:') + void a_request_is_received_with_the_following(DataTable dataTable) { + receivedRequests << new HttpRequest() + def entry = dataTable.entries().first() + if (entry['body']) { + def part = configureBody(entry['body'], determineContentType(entry['body'], + receivedRequests[0].contentTypeHeader())) + receivedRequests[0].body = part.body + receivedRequests[0].headers.putAll(part.headers) + } + } + + @When('the request is compared to the expected one') + void the_request_is_compared_to_the_expected_one() { + requestResults << requestMismatches(expectedRequest, receivedRequests[0]) + } + + @Then('the comparison should be OK') + void the_comparison_should_be_ok() { + assert requestResults.every { it.mismatches.empty } + } + + @Then('the comparison should NOT be OK') + void the_comparison_should_not_be_ok() { + assert requestResults.any { !it.mismatches.empty } + } + + @Then('the mismatches will contain a mismatch with error {string} -> {string}') + @SuppressWarnings('SpaceAfterOpeningBrace') + void the_mismatches_will_contain_a_mismatch_with_error(String path, String error) { + assert requestResults.any { + it.mismatches.find { + def pathMatches = switch (it) { + case HeaderMismatch -> it.headerKey == path + case BodyMismatch -> it.path == path + default -> false + } + def desc = it.description() + desc.contains(error) && pathMatches + } != null + } + } +} diff --git a/compatibility-suite/src/test/groovy/steps/v4/HttpProvider.groovy b/compatibility-suite/src/test/groovy/steps/v4/HttpProvider.groovy new file mode 100644 index 000000000..2aa4a831a --- /dev/null +++ b/compatibility-suite/src/test/groovy/steps/v4/HttpProvider.groovy @@ -0,0 +1,104 @@ +package steps.v4 + +import au.com.dius.pact.core.model.Consumer +import au.com.dius.pact.core.model.DefaultPactWriter +import au.com.dius.pact.core.model.PactSpecVersion +import au.com.dius.pact.core.model.Provider +import au.com.dius.pact.core.model.StringSource +import au.com.dius.pact.core.model.V4Pact +import au.com.dius.pact.provider.ConsumerInfo +import au.com.dius.pact.provider.VerificationResult +import io.cucumber.datatable.DataTable +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import steps.shared.CompatibilitySuiteWorld +import steps.shared.SharedHttpProvider +import steps.shared.VerificationData + +class HttpProvider { + CompatibilitySuiteWorld world + SharedHttpProvider sharedProvider + VerificationData verificationData + + HttpProvider(CompatibilitySuiteWorld world, SharedHttpProvider sharedProvider, VerificationData verificationData) { + this.world = world + this.sharedProvider = sharedProvider + this.verificationData = verificationData + } + + @Given('a Pact file for interaction {int} is to be verified, but is marked pending') + void a_pact_file_for_interaction_is_to_be_verified_but_is_marked_pending(Integer index) { + def interaction = world.interactions[index - 1].asV4Interaction() + interaction.pending = true + V4Pact pact = new V4Pact( + new Consumer('v3-compatibility-suite-c'), + new Provider('p'), + [ interaction ] + ) + StringWriter writer = new StringWriter() + writer.withPrintWriter { + DefaultPactWriter.INSTANCE.writePact(pact, it, PactSpecVersion.V4) + } + ConsumerInfo consumerInfo = new ConsumerInfo('c') + consumerInfo.pactSource = new StringSource(writer.toString()) + if (verificationData.providerInfo.stateChangeRequestFilter) { + consumerInfo.stateChange = verificationData.providerInfo.stateChangeRequestFilter + } + verificationData.providerInfo.consumers << consumerInfo + } + + @Given('a Pact file for interaction {int} is to be verified with the following comments:') + void a_pact_file_for_interaction_is_to_be_verified_with_the_following_comments(Integer index, DataTable dataTable) { + def interaction = world.interactions[index - 1].asV4Interaction() + + for (comment in dataTable.asMaps()) { + switch (comment['type']) { + case 'text': + interaction.addTextComment(comment['comment']) + break + case 'testname': + interaction.setTestName(comment['comment']) + break + } + } + + V4Pact pact = new V4Pact( + new Consumer('v3-compatibility-suite-c'), + new Provider('p'), + [ interaction ] + ) + StringWriter writer = new StringWriter() + writer.withPrintWriter { + DefaultPactWriter.INSTANCE.writePact(pact, it, PactSpecVersion.V4) + } + ConsumerInfo consumerInfo = new ConsumerInfo('c') + consumerInfo.pactSource = new StringSource(writer.toString()) + if (verificationData.providerInfo.stateChangeRequestFilter) { + consumerInfo.stateChange = verificationData.providerInfo.stateChangeRequestFilter + } + verificationData.providerInfo.consumers << consumerInfo + } + + @Then('there will be a pending {string} error') + void there_will_be_a_pending_error(String error) { + assert verificationData.verificationResults.any { + it instanceof VerificationResult.Failed && it.pending && it.description == error + } + } + + @Then('the comment {string} will have been printed to the console') + void the_comment_will_have_been_printed_to_the_console(String comment) { + def comments = verificationData.verifier.reporters.first().events.find { + it.comments + } + assert comments && comments.comments.text.values.any { it == comment } + } + + @Then('the {string} will displayed as the original test name') + void the_will_displayed_as_the_original_test_name(String name) { + def comments = verificationData.verifier.reporters.first().events.find { + it.comments + } + assert comments && comments.comments.testname == name + } +} diff --git a/compatibility-suite/src/test/groovy/steps/v4/MessageConsumer.groovy b/compatibility-suite/src/test/groovy/steps/v4/MessageConsumer.groovy new file mode 100644 index 000000000..c1d8b8dec --- /dev/null +++ b/compatibility-suite/src/test/groovy/steps/v4/MessageConsumer.groovy @@ -0,0 +1,37 @@ +package steps.v4 + +import au.com.dius.pact.consumer.dsl.MessageInteractionBuilder +import io.cucumber.java.After +import io.cucumber.java.Before +import io.cucumber.java.Scenario +import io.cucumber.java.en.Given + +class MessageConsumer { + SharedV4PactData v4Data + MessageInteractionBuilder builder + + MessageConsumer(SharedV4PactData v4Data) { + this.v4Data = v4Data + } + + @Before + void before(Scenario scenario) { + v4Data.scenarioId = scenario.id + } + + @After + void after(Scenario scenario) { + if (!scenario.failed) { + def dir = "build/compatibility-suite/v4/${v4Data.scenarioId}" as File + dir.deleteDir() + } + } + + @Given('a message interaction is being defined for a consumer test') + void a_message_interaction_is_being_defined_for_a_consumer_test() { + builder = new MessageInteractionBuilder('a message', [], []) + v4Data.builderCallbacks << { + builder.build() + } + } +} diff --git a/compatibility-suite/src/test/groovy/steps/v4/SharedV4Data.groovy b/compatibility-suite/src/test/groovy/steps/v4/SharedV4Data.groovy new file mode 100644 index 000000000..6ab2c6fbb --- /dev/null +++ b/compatibility-suite/src/test/groovy/steps/v4/SharedV4Data.groovy @@ -0,0 +1,65 @@ +package steps.v4 + +import au.com.dius.pact.consumer.dsl.PactBuilder +import au.com.dius.pact.core.model.PactSpecVersion +import au.com.dius.pact.core.model.V4Pact +import au.com.dius.pact.core.support.Json +import au.com.dius.pact.core.support.json.JsonParser +import au.com.dius.pact.core.support.json.JsonValue +import io.cucumber.java.ParameterType +import io.cucumber.java.en.Then +import io.cucumber.java.en.When + +class SharedV4PactData { + String scenarioId + PactBuilder pactBuilder = new PactBuilder('V4 consumer', 'V4 provider', PactSpecVersion.V4) + List builderCallbacks = [] + + @SuppressWarnings('UnnecessaryConstructor') + SharedV4PactData() { } +} + +@ParameterType('first|second|third') +@SuppressWarnings(['SpaceAfterOpeningBrace']) +static Integer numType(String numType) { + switch (numType) { + case 'first' -> yield 0 + case 'second'-> yield 1 + case 'third' -> yield 2 + default -> throw new IllegalArgumentException("$numType is not a valid number type") + } +} + +class SharedV4Steps { + SharedV4PactData sharedV4PactData + V4Pact pact + String pactJsonStr + JsonValue.Object pactJson + + SharedV4Steps(SharedV4PactData sharedV4PactData) { + this.sharedV4PactData = sharedV4PactData + } + + @When('the Pact file for the test is generated') + void the_pact_file_for_the_test_is_generated() { + sharedV4PactData.builderCallbacks.forEach { + sharedV4PactData.pactBuilder.interactions.add(it.call()) + } + pact = sharedV4PactData.pactBuilder.toPact() + pactJsonStr = Json.INSTANCE.prettyPrint(pact.toMap(PactSpecVersion.V3)) + pactJson = JsonParser.parseString(pactJsonStr).asObject() + } + + @Then('the {numType} interaction in the Pact file will have a type of {string}') + void the_interaction_in_the_pact_file_will_have_a_type_of(Integer index, String type) { + JsonValue.Array interactions = pactJson['interactions'].asArray() + assert interactions.get(index)['type'] == type + } + + @Then('the {numType} interaction in the Pact file will have {string} = {string}') + void the_first_interaction_in_the_pact_file_will_have(Integer index, String name, String value) { + JsonValue.Array interactions = pactJson['interactions'].asArray() + def json = JsonParser.parseString(value) + assert interactions.get(index)[name] == json + } +} diff --git a/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt b/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt index 55866f29a..e03f6475c 100644 --- a/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt +++ b/consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/messaging/SynchronousMessageBuilder.kt @@ -50,7 +50,7 @@ open class SynchronousMessageBuilder( */ fun testname(testname: String): SynchronousMessageBuilder { if (testname.isNotEmpty()) { - interaction.comments["testname"] = JsonValue.StringValue(testname) + interaction.setTestName(testname) } return this } diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt index 6daea001a..719b2676f 100644 --- a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt @@ -572,7 +572,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal if (providerInfo.pactVersion != null && providerInfo.pactVersion >= PactSpecVersion.V4) { pact.asV4Pact().unwrap().interactions.forEach { i -> - i.comments["testname"] = Json.toJson(context.testClass.map { it.name + "." }.orElse("") + + (i as V4Interaction).setTestName(context.testClass.map { it.name + "." }.orElse("") + context.displayName) } } diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpInteractionBuilder.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpInteractionBuilder.kt index 956041cbf..7a2164135 100644 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpInteractionBuilder.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpInteractionBuilder.kt @@ -106,11 +106,7 @@ open class HttpInteractionBuilder( * Adds a text comment to the interaction */ fun comment(comment: String): HttpInteractionBuilder { - if (interaction.comments.containsKey("text")) { - interaction.comments["text"]!!.add(JsonValue.StringValue(comment)) - } else { - interaction.comments["text"] = JsonValue.Array(mutableListOf(JsonValue.StringValue(comment))) - } + interaction.addTextComment(comment) return this } } diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/MessageInteractionBuilder.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/MessageInteractionBuilder.kt new file mode 100644 index 000000000..f5bc2bf4f --- /dev/null +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/MessageInteractionBuilder.kt @@ -0,0 +1,26 @@ +package au.com.dius.pact.consumer.dsl + +import au.com.dius.pact.core.model.ProviderState +import au.com.dius.pact.core.model.V4Interaction +import au.com.dius.pact.core.support.json.JsonValue + +/** + * Pact Message builder DSL that supports V4 formatted Pact files + */ +open class MessageInteractionBuilder( + description: String, + providerStates: MutableList, + comments: MutableList +) { + val interaction = V4Interaction.AsynchronousMessage(description, providerStates) + + init { + if (comments.isNotEmpty()) { + interaction.comments["text"] = JsonValue.Array(comments.toMutableList()) + } + } + + fun build(): V4Interaction { + return interaction + } +} diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt index 6bec8f780..e9ef5fa73 100644 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactBuilder.kt @@ -495,6 +495,33 @@ open class PactBuilder( return this } + /** + * Creates a new asynchronous message interaction with the given description, and passes a builder to the builder + * function to construct it. + */ + fun expectsToReceiveMessageInteraction( + description: String, + builderFn: (MessageInteractionBuilder) -> MessageInteractionBuilder? + ): PactBuilder { + if (currentInteraction != null) { + interactions.add(currentInteraction!!) + currentInteraction = null + } + + val builder = MessageInteractionBuilder(description, providerStates, comments) + val result = builderFn(builder) + if (result != null) { + interactions.add(result.build()) + } else { + interactions.add(builder.build()) + } + + providerStates.clear() + comments.clear() + + return this + } + companion object : KLogging() { @Suppress("LongMethod", "ComplexMethod") fun setupMessageContents( diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt index 7e11eccb9..5513d052b 100755 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt @@ -64,6 +64,7 @@ fun valueOf(value: Any?): String { is Element -> "<${QualifiedName(value)}>" is Text -> "'${value.wholeText}'" is JsonValue -> value.serialise() + is ByteArray -> "${value.size} byte(s)" else -> value.toString() } } @@ -139,6 +140,7 @@ fun domatch( mismatchFn: MismatchFactory, cascaded: Boolean ): List { + logger.debug { "Matching value at $path with $matcher" } return when (matcher) { is RegexMatcher -> matchRegex(matcher.regex, path, expected, actual, mismatchFn) is TypeMatcher -> matchType(path, expected, actual, mismatchFn, true) @@ -239,6 +241,8 @@ fun matchType( emptyList() } else if (expected is String && actual is String || expected is List<*> && actual is List<*> || + expected is Array<*> && actual is Array<*> || + expected is ByteArray && actual is ByteArray || expected is JsonValue.Array && actual is JsonValue.Array || expected is Map<*, *> && actual is Map<*, *> || expected is JsonValue.Object && actual is JsonValue.Object) { @@ -248,6 +252,8 @@ fun matchType( val empty = when (actual) { is String -> actual.isEmpty() is List<*> -> actual.isEmpty() + is Array<*> -> actual.isEmpty() + is ByteArray -> actual.isEmpty() is Map<*, *> -> actual.isEmpty() is JsonValue.Array -> actual.size == 0 is JsonValue.Object -> actual.size == 0 @@ -264,7 +270,22 @@ fun matchType( ((expected.isBoolean && actual.isBoolean) || (expected.isNumber && actual.isNumber) || (expected.isString && actual.isString))) { + if (allowEmpty) { emptyList() + } else { + val empty = when (actual) { + is JsonValue.Array -> actual.size == 0 + is JsonValue.Object -> actual.size == 0 + is JsonValue.StringValue -> actual.value.chars.isEmpty() + else -> false + } + if (empty) { + listOf(mismatchFactory.create(expected, actual, + "Expected ${valueOf(actual)} (${typeOf(actual)}) to not be empty", path)) + } else { + emptyList() + } + } } else if (expected == null || expected is JsonValue.Null) { if (actual == null || actual is JsonValue.Null) { emptyList() diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt index a6dbac00c..40924b737 100644 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt @@ -223,8 +223,8 @@ object Matching : KLogging() { return when { rootMatcher != null && rootMatcher.canMatch(expectedContentType) -> BodyMatchResult(null, - listOf(BodyItemMatchResult("$", domatch(rootMatcher, listOf("$"), expected.body.unwrap(), - actual.body.unwrap(), BodyMismatchFactory)))) + listOf(BodyItemMatchResult("$", domatch(rootMatcher, listOf("$"), expected.body.orEmpty(), + actual.body.orEmpty(), BodyMismatchFactory)))) expectedContentType.getBaseType() == actualContentType.getBaseType() -> { val matcher = MatchingConfig.lookupContentMatcher(actualContentType.getBaseType()) if (matcher != null) { diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt index f79bc1381..557c7477a 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/V4Pact.kt @@ -157,6 +157,24 @@ sealed class V4Interaction( abstract fun updateProperties(values: Map) + /** + * Adds a freeform text comment to the interaction. Comments may be displayed during verification. + */ + fun addTextComment(comment: String) { + if (comments.containsKey("text")) { + comments["text"]!!.add(JsonValue.StringValue(comment)) + } else { + comments["text"] = JsonValue.Array(mutableListOf(JsonValue.StringValue(comment))) + } + } + + /** + * Sets the test name that generated the interaction + */ + fun setTestName(name: String) { + comments["testname"] = JsonValue.StringValue(name) + } + /** * Add configuration values from the plugin to this interaction */ diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/NotEmptyMatcher.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/NotEmptyMatcher.kt index 07255ba76..b9d8dca47 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/NotEmptyMatcher.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/NotEmptyMatcher.kt @@ -1,10 +1,11 @@ package au.com.dius.pact.core.model.matchingrules +import au.com.dius.pact.core.model.ContentType import au.com.dius.pact.core.model.PactSpecVersion import au.com.dius.pact.core.support.json.JsonValue /** - * String type matcher that checks the string length + * Type matcher that checks the length of the type */ object NotEmptyMatcher : MatchingRule { override fun toMap(spec: PactSpecVersion?) = when (spec) { @@ -14,6 +15,8 @@ object NotEmptyMatcher : MatchingRule { override fun validateForVersion(pactVersion: PactSpecVersion?): List = listOf() + override fun canMatch(contentType: ContentType) = true + override val name: String get() = "not-empty" override val attributes: Map