From 2a12a32129b98a4b745ad1c07d8d378bf0facb69 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Sun, 8 Aug 2021 13:29:49 +1000 Subject: [PATCH] fix: min/max type matchers must not apply the limits when cascading #396 --- .../pact/core/matchers/MatcherExecutor.kt | 105 ++++++++++-------- .../com/dius/pact/core/matchers/Matchers.kt | 25 +++-- .../com/dius/pact/core/matchers/Matching.kt | 16 ++- .../core/matchers/MatcherExecutorSpec.groovy | 47 ++++---- .../core/matchers/MaximumMatcherSpec.groovy | 14 ++- .../core/matchers/MinimumMatcherSpec.groovy | 15 ++- .../pact/core/model/matchingrules/Category.kt | 4 +- .../core/model/matchingrules/MatchingRules.kt | 3 +- .../model/matchingrules/MatchingRulesImpl.kt | 1 + ...on that does not match in element xml.json | 2 +- ...on that does not match in element xml.json | 2 +- provider/junit5/build.gradle | 1 + .../MinLikeMatcherWithChildArraysTest.groovy | 55 +++++++++ .../src/test/resources/pacts/issue396.json | 54 +++++++++ 14 files changed, 251 insertions(+), 93 deletions(-) create mode 100644 provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/MinLikeMatcherWithChildArraysTest.groovy create mode 100644 provider/junit5/src/test/resources/pacts/issue396.json 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 1373bddf4a..a5cc7f44f2 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 @@ -95,7 +95,7 @@ fun domatch( mismatchFn: MismatchFactory ): List { val result = matchers.rules.map { matchingRule -> - domatch(matchingRule, path, expected, actual, mismatchFn) + domatch(matchingRule, path, expected, actual, mismatchFn, matchers.cascaded) } return if (matchers.ruleLogic == RuleLogic.AND) { @@ -114,7 +114,8 @@ fun domatch( path: List, expected: Any?, actual: Any?, - mismatchFn: MismatchFactory + mismatchFn: MismatchFactory, + cascaded: Boolean ): List { return when (matcher) { is RegexMatcher -> matchRegex(matcher.regex, path, expected, actual, mismatchFn) @@ -123,10 +124,10 @@ fun domatch( is DateMatcher -> matchDate(matcher.format, path, expected, actual, mismatchFn) is TimeMatcher -> matchTime(matcher.format, path, expected, actual, mismatchFn) is TimestampMatcher -> matchDateTime(matcher.format, path, expected, actual, mismatchFn) - is MinTypeMatcher -> matchMinType(matcher.min, path, expected, actual, mismatchFn) - is MaxTypeMatcher -> matchMaxType(matcher.max, path, expected, actual, mismatchFn) - is MinMaxTypeMatcher -> matchMinType(matcher.min, path, expected, actual, mismatchFn) + - matchMaxType(matcher.max, path, expected, actual, mismatchFn) + is MinTypeMatcher -> matchMinType(matcher.min, path, expected, actual, mismatchFn, cascaded) + is MaxTypeMatcher -> matchMaxType(matcher.max, path, expected, actual, mismatchFn, cascaded) + is MinMaxTypeMatcher -> matchMinType(matcher.min, path, expected, actual, mismatchFn, cascaded) + + matchMaxType(matcher.max, path, expected, actual, mismatchFn, cascaded) is IncludeMatcher -> matchInclude(matcher.value, path, expected, actual, mismatchFn) is NullMatcher -> matchNull(path, actual, mismatchFn) is ContentTypeMatcher -> matchContentType(path, ContentType.fromString(matcher.contentType), actual, mismatchFn) @@ -381,29 +382,37 @@ fun matchMinType( path: List, expected: Any?, actual: Any?, - mismatchFactory: MismatchFactory + mismatchFactory: MismatchFactory, + cascaded: Boolean ): List { logger.debug { "comparing ${valueOf(actual)} with minimum $min at $path" } - return if (actual is List<*>) { - if (actual.size < min) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) - } else { - emptyList() - } - } else if (actual is JsonValue.Array) { - if (actual.size < min) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) - } else { - emptyList() - } - } else if (actual is Element) { - if (actual.childNodes.length < min) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) - } else { - emptyList() + return if (!cascaded) { + when (actual) { + is List<*> -> { + if (actual.size < min) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) + } else { + emptyList() + } + } + is JsonValue.Array -> { + if (actual.size < min) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) + } else { + emptyList() + } + } + is Element -> { + if (actual.childNodes.length < min) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have minimum $min", path)) + } else { + emptyList() + } + } + else -> matchType(path, expected, actual, mismatchFactory) } } else { - matchType(path, expected, actual, mismatchFactory) + matchType(path, expected, actual, mismatchFactory) } } @@ -412,29 +421,37 @@ fun matchMaxType( path: List, expected: Any?, actual: Any?, - mismatchFactory: MismatchFactory + mismatchFactory: MismatchFactory, + cascaded: Boolean ): List { logger.debug { "comparing ${valueOf(actual)} with maximum $max at $path" } - return if (actual is List<*>) { - if (actual.size > max) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) - } else { - emptyList() - } - } else if (actual is JsonValue.Array) { - if (actual.size > max) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) - } else { - emptyList() - } - } else if (actual is Element) { - if (actual.childNodes.length > max) { - listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) - } else { - emptyList() + return if (!cascaded) { + when (actual) { + is List<*> -> { + if (actual.size > max) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) + } else { + emptyList() + } + } + is JsonValue.Array -> { + if (actual.size > max) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) + } else { + emptyList() + } + } + is Element -> { + if (actual.childNodes.length > max) { + listOf(mismatchFactory.create(expected, actual, "Expected ${valueOf(actual)} to have maximum $max", path)) + } else { + emptyList() + } + } + else -> matchType(path, expected, actual, mismatchFactory) } } else { - matchType(path, expected, actual, mismatchFactory) + matchType(path, expected, actual, mismatchFactory) } } diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt index be33286911..c6356cecbc 100755 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt @@ -53,15 +53,15 @@ object Matchers : KLogging() { category: String, items: List, pathComparator: Comparator - ) = if (category == "body") - matchers.rulesForCategory(category).filter(Predicate { - matchesPath(it, items) > 0 - }) - else if (category == "header" || category == "query" || category == "metadata") - matchers.rulesForCategory(category).filter(Predicate { key -> - items.all { pathComparator.compare(key, it) == 0 } - }) - else matchers.rulesForCategory(category) + ) = when (category) { + "body" -> matchers.rulesForCategory(category).filter(Predicate { + matchesPath(it, items) > 0 + }) + "header", "query", "metadata" -> matchers.rulesForCategory(category).filter(Predicate { key -> + items.all { pathComparator.compare(key, it) == 0 } + }) + else -> matchers.rulesForCategory(category) + } @JvmStatic @JvmOverloads @@ -117,8 +117,8 @@ object Matchers : KLogging() { pathComparator: Comparator = Comparator.naturalOrder() ): MatchingRuleGroup { val matcherCategory = resolveMatchers(matchers, category, path, pathComparator) - return if (category == "body") - matcherCategory.maxBy(Comparator { a, b -> + return if (category == "body") { + val result = matcherCategory.maxBy(Comparator { a, b -> val weightA = calculatePathWeight(a, path) val weightB = calculatePathWeight(b, path) when { @@ -131,7 +131,8 @@ object Matchers : KLogging() { else -> -1 } }) - else { + result?.second?.copy(cascaded = parsePath(result.first).size != path.size) ?: MatchingRuleGroup() + } else { matcherCategory.matchingRules.values.first() } } 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 16d61056d5..c6f76a0464 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 @@ -40,7 +40,8 @@ object Matching : KLogging() { matchers ?: MatchingRulesImpl()) }.filterNotNull()) } else { - list + HeaderMatchResult(values.key, listOf(HeaderMismatch(values.key, values.value.joinToString(separator = ", "), "", + list + HeaderMatchResult(values.key, + listOf(HeaderMismatch(values.key, values.value.joinToString(separator = ", "), "", "Expected a header '${values.key}' but was missing"))) } } @@ -77,8 +78,11 @@ object Matching : KLogging() { } } } else { - if (expected.body.isMissing() || expected.body.isNull() || expected.body.isEmpty()) BodyMatchResult(null, emptyList()) - else BodyMatchResult(BodyTypeMismatch(expectedContentType.getBaseType(), actualContentType.getBaseType()), emptyList()) + if (expected.body.isMissing() || expected.body.isNull() || expected.body.isEmpty()) + BodyMatchResult(null, emptyList()) + else + BodyMatchResult(BodyTypeMismatch(expectedContentType.getBaseType(), actualContentType.getBaseType()), + emptyList()) } } @@ -115,11 +119,13 @@ object Matching : KLogging() { null -> acc + QueryMatchResult(entry.key, listOf(QueryMismatch(entry.key, entry.value.joinToString(","), "", "Expected query parameter '${entry.key}' but was missing", listOf("$", "query", entry.key).joinToString(".")))) - else -> acc + QueryMatchResult(entry.key, QueryMatcher.compareQuery(entry.key, entry.value, value, expected.matchingRules)) + else -> acc + + QueryMatchResult(entry.key, QueryMatcher.compareQuery(entry.key, entry.value, value, expected.matchingRules)) } } + actual.query.entries.fold(emptyList()) { acc, entry -> when (expected.query[entry.key]) { - null -> acc + QueryMatchResult(entry.key, listOf(QueryMismatch(entry.key, "", entry.value.joinToString(","), + null -> acc + + QueryMatchResult(entry.key, listOf(QueryMismatch(entry.key, "", entry.value.joinToString(","), "Unexpected query parameter '${entry.key}' received", listOf("$", "query", entry.key).joinToString(".")))) else -> acc diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatcherExecutorSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatcherExecutorSpec.groovy index 9cf3010eae..472e318cbc 100755 --- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatcherExecutorSpec.groovy +++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatcherExecutorSpec.groovy @@ -42,7 +42,8 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'equals matcher matches using equals'() { expect: - MatcherExecutorKt.domatch(EqualsMatcher.INSTANCE, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(EqualsMatcher.INSTANCE, path, expected, actual, mismatchFactory, false).empty == + mustBeEmpty where: expected | actual || mustBeEmpty @@ -71,7 +72,8 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'regex matcher matches using the provided regex'() { expect: - MatcherExecutorKt.domatch(new RegexMatcher(regex), path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(new RegexMatcher(regex), path, expected, actual, mismatchFactory, false).empty == + mustBeEmpty where: expected | actual | regex || mustBeEmpty @@ -87,7 +89,8 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'type matcher matches on types'() { expect: - MatcherExecutorKt.domatch(TypeMatcher.INSTANCE, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(TypeMatcher.INSTANCE, path, expected, actual, mismatchFactory, false).empty == + mustBeEmpty where: expected | actual || mustBeEmpty @@ -116,8 +119,8 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'number type matcher matches on types'() { expect: - MatcherExecutorKt.domatch(new NumberTypeMatcher(numberType), path, expected, actual, mismatchFactory).empty == - mustBeEmpty + MatcherExecutorKt.domatch(new NumberTypeMatcher(numberType), path, expected, actual, mismatchFactory, + false).empty == mustBeEmpty where: numberType | expected | actual || mustBeEmpty @@ -153,7 +156,7 @@ class MatcherExecutorSpec extends Specification { @SuppressWarnings('LineLength') def 'timestamp matcher'() { expect: - MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory, false).empty == mustBeEmpty where: expected | actual | pattern || mustBeEmpty @@ -181,7 +184,7 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'time matcher'() { expect: - MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory, false).empty == mustBeEmpty where: expected | actual | pattern || mustBeEmpty @@ -196,7 +199,7 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'date matcher'() { expect: - MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory, false).empty == mustBeEmpty where: expected | actual | pattern || mustBeEmpty @@ -211,7 +214,7 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'include matcher matches if the expected is included in the actual'() { expect: - MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory, false).empty == mustBeEmpty where: expected | actual || mustBeEmpty @@ -241,19 +244,22 @@ class MatcherExecutorSpec extends Specification { @Unroll def 'list type matcher matches on array sizes - #matcher'() { expect: - MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory).empty == mustBeEmpty + MatcherExecutorKt.domatch(matcher, path, expected, actual, mismatchFactory, cascaded).empty == mustBeEmpty where: - matcher | expected | actual || mustBeEmpty - TypeMatcher.INSTANCE | [0] | [1] || true - new MinTypeMatcher(1) | [0] | [1] || true - new MinTypeMatcher(2) | [0, 1] | [1] || false - new MaxTypeMatcher(2) | [0] | [1] || true - new MaxTypeMatcher(1) | [0] | [1, 1] || false - new MinMaxTypeMatcher(1, 2) | [0] | [1] || true - new MinMaxTypeMatcher(2, 3) | [0, 1] | [1] || false - new MinMaxTypeMatcher(1, 2) | [0, 1] | [1, 1] || true - new MinMaxTypeMatcher(1, 2) | [0] | [1, 1, 2] || false + matcher | expected | actual | cascaded || mustBeEmpty + TypeMatcher.INSTANCE | [0] | [1] | false || true + new MinTypeMatcher(1) | [0] | [1] | false || true + new MinTypeMatcher(2) | [0, 1] | [1] | false || false + new MinTypeMatcher(2) | [0, 1] | [1] | true || true + new MaxTypeMatcher(2) | [0] | [1] | false || true + new MaxTypeMatcher(1) | [0] | [1, 1] | false || false + new MaxTypeMatcher(1) | [0] | [1, 1] | true || true + new MinMaxTypeMatcher(1, 2) | [0] | [1] | false || true + new MinMaxTypeMatcher(2, 3) | [0, 1] | [1] | false || false + new MinMaxTypeMatcher(1, 2) | [0, 1] | [1, 1] | false || true + new MinMaxTypeMatcher(1, 2) | [0] | [1, 1, 2] | false || false + new MinMaxTypeMatcher(1, 2) | [0] | [1, 1, 2] | true || true } @Unroll @@ -314,5 +320,4 @@ class MatcherExecutorSpec extends Specification { xml('') || '<{a}foo>' xml('text').firstChild || "'text'" } - } diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MaximumMatcherSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MaximumMatcherSpec.groovy index 9123e1db9f..d6816c5228 100755 --- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MaximumMatcherSpec.groovy +++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MaximumMatcherSpec.groovy @@ -18,7 +18,7 @@ class MaximumMatcherSpec extends Specification { @Unroll def 'with an array match if the array #condition'() { expect: - MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory).empty + MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty where: condition | expected | actual @@ -29,7 +29,7 @@ class MaximumMatcherSpec extends Specification { @Unroll def 'with an array not match if the array #condition'() { expect: - !MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory).empty + !MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty where: condition | expected | actual @@ -39,7 +39,7 @@ class MaximumMatcherSpec extends Specification { @Unroll def 'with a non array default to a type matcher'() { expect: - MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory).empty == beEmpty + MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty == beEmpty where: expected | actual || beEmpty @@ -47,4 +47,12 @@ class MaximumMatcherSpec extends Specification { 'Fred' | 100 || false } + def 'when cascaded, do not apply the limits'() { + given: + def expected = [1, 2] + def actual = [1, 2, 3] + + expect: + MatcherExecutorKt.domatch(new MaxTypeMatcher(2), path, expected, actual, mismatchFactory, true).empty + } } diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MinimumMatcherSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MinimumMatcherSpec.groovy index e9f3c15ada..b0461b34b1 100755 --- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MinimumMatcherSpec.groovy +++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MinimumMatcherSpec.groovy @@ -18,7 +18,7 @@ class MinimumMatcherSpec extends Specification { @Unroll def 'with an array match if the array #condition'() { expect: - MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory).empty + MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty where: condition | expected | actual @@ -29,7 +29,7 @@ class MinimumMatcherSpec extends Specification { @Unroll def 'with an array not match if the array #condition'() { expect: - !MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory).empty + !MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty where: condition | expected | actual @@ -39,7 +39,8 @@ class MinimumMatcherSpec extends Specification { @Unroll def 'with a non array default to a type matcher'() { expect: - MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory).empty == beEmpty + MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory, false).empty + == beEmpty where: expected | actual || beEmpty @@ -47,4 +48,12 @@ class MinimumMatcherSpec extends Specification { 'Fred' | 100 || false } + def 'when cascaded, do not apply the limits'() { + given: + def expected = [1, 2] + def actual = [1] + + expect: + MatcherExecutorKt.domatch(new MinTypeMatcher(2), path, expected, actual, mismatchFactory, true).empty + } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/Category.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/Category.kt index e22f01a9c0..95052f1d1c 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/Category.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/Category.kt @@ -88,9 +88,9 @@ data class Category @JvmOverloads constructor( fun filter(predicate: Predicate) = copy(matchingRules = matchingRules.filter { predicate.test(it.key) }.toMutableMap()) - fun maxBy(comparator: Comparator): MatchingRuleGroup { + fun maxBy(comparator: Comparator): Pair? { val max = matchingRules.maxWith(Comparator { a, b -> comparator.compare(a.key, b.key) }) - return max?.value ?: MatchingRuleGroup() + return max?.toPair() } /** diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRules.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRules.kt index 3a86ff9a4f..6d7a10a95f 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRules.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRules.kt @@ -133,7 +133,8 @@ data class ContentTypeMatcher @JvmOverloads constructor (val contentType: String data class MatchingRuleGroup @JvmOverloads constructor( val rules: MutableList = mutableListOf(), - val ruleLogic: RuleLogic = RuleLogic.AND + val ruleLogic: RuleLogic = RuleLogic.AND, + val cascaded: Boolean = false ) { fun toMap(pactSpecVersion: PactSpecVersion): Map { return if (pactSpecVersion < PactSpecVersion.V3) { diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRulesImpl.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRulesImpl.kt index 0578e002c6..cac921ab4c 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRulesImpl.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRulesImpl.kt @@ -50,6 +50,7 @@ class MatchingRulesImpl : MatchingRules { override fun getCategories(): Set = rules.keys override fun toString(): String = "MatchingRules(rules=$rules)" + override fun equals(other: Any?): Boolean = when (other) { is MatchingRulesImpl -> other.rules == rules else -> false diff --git a/pact-specification-test/src/main/resources/v2/request/body/array with regular expression that does not match in element xml.json b/pact-specification-test/src/main/resources/v2/request/body/array with regular expression that does not match in element xml.json index d360652550..7e29511188 100644 --- a/pact-specification-test/src/main/resources/v2/request/body/array with regular expression that does not match in element xml.json +++ b/pact-specification-test/src/main/resources/v2/request/body/array with regular expression that does not match in element xml.json @@ -10,7 +10,7 @@ "$.body.animals": {"min": 1, "match": "type"}, "$.body.animals.0": {"match": "type"}, "$.body.animals.1": {"match": "type"}, - "$.body.animals[*].alligator['@phoneNumber']": {"match": "regex", "regex": "\\d+"} + "$.body.animals.alligator['@phoneNumber']": {"match": "regex", "regex": "\\d+"} }, "body": "" }, diff --git a/pact-specification-test/src/main/resources/v3/request/body/array with regular expression that does not match in element xml.json b/pact-specification-test/src/main/resources/v3/request/body/array with regular expression that does not match in element xml.json index 9908c20a51..c981fdbecc 100644 --- a/pact-specification-test/src/main/resources/v3/request/body/array with regular expression that does not match in element xml.json +++ b/pact-specification-test/src/main/resources/v3/request/body/array with regular expression that does not match in element xml.json @@ -30,7 +30,7 @@ } ] }, - "$.animals[*].alligator['@phoneNumber']": { + "$.animals.alligator['@phoneNumber']": { "matchers": [ { "match": "regex", diff --git a/provider/junit5/build.gradle b/provider/junit5/build.gradle index dc151b7213..d08c6b3705 100644 --- a/provider/junit5/build.gradle +++ b/provider/junit5/build.gradle @@ -10,6 +10,7 @@ dependencies { testRuntime "org.junit.jupiter:junit-jupiter-engine:${project.junit5Version}" testRuntime "org.junit.vintage:junit-vintage-engine:${project.junit5Version}" testCompile "org.codehaus.groovy:groovy:${project.groovyVersion}" + testCompile "org.codehaus.groovy:groovy-json:${project.groovyVersion}" testCompile('org.spockframework:spock-core:2.0-groovy-3.0') { exclude group: 'org.codehaus.groovy' } diff --git a/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/MinLikeMatcherWithChildArraysTest.groovy b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/MinLikeMatcherWithChildArraysTest.groovy new file mode 100644 index 0000000000..e791d13a3a --- /dev/null +++ b/provider/junit5/src/test/groovy/au/com/dius/pact/provider/junit5/MinLikeMatcherWithChildArraysTest.groovy @@ -0,0 +1,55 @@ +package au.com.dius.pact.provider.junit5 + +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import com.github.tomakehurst.wiremock.WireMockServer +import groovy.json.JsonOutput +import groovy.util.logging.Slf4j +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import ru.lanwen.wiremock.ext.WiremockResolver +import ru.lanwen.wiremock.ext.WiremockUriResolver + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse +import static com.github.tomakehurst.wiremock.client.WireMock.get +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo + +@Provider('Issue396Service') +@PactFolder('pacts') +@ExtendWith([ + WiremockResolver, + WiremockUriResolver +]) +@Slf4j +class MinLikeMatcherWithChildArraysTest { + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider) + void testTemplate(PactVerificationContext context) { + context.verifyInteraction() + } + + @BeforeEach + void before(PactVerificationContext context, @WiremockResolver.Wiremock WireMockServer server, + @WiremockUriResolver.WiremockUri String uri) throws MalformedURLException { + context.setTarget(HttpTestTarget.fromUrl(new URL(uri))) + + server.stubFor( + get(urlPathEqualTo('/data')) + .willReturn(aResponse() + .withStatus(200) + .withHeader('Content-Type', 'application/json') + .withBody(JsonOutput.toJson([ + parent: [ + [ + child: ['a'] + ], + [ + child: ['a'] + ] + ] + ]))) + ) + } +} diff --git a/provider/junit5/src/test/resources/pacts/issue396.json b/provider/junit5/src/test/resources/pacts/issue396.json new file mode 100644 index 0000000000..2f92bc43ff --- /dev/null +++ b/provider/junit5/src/test/resources/pacts/issue396.json @@ -0,0 +1,54 @@ +{ + "provider": { + "name": "Issue396Service" + }, + "consumer": { + "name": "consumer" + }, + "interactions": [ + { + "description": "Get data", + "request": { + "method": "GET", + "path": "/data" + }, + "response": { + "status": 200, + "body": { + "parent": [ + { + "child": [ + "a" + ] + }, + { + "child": [ + "a" + ] + } + ] + }, + "matchingRules": { + "body": { + "$.parent": { + "matchers": [ + { + "match": "type", + "min": 2 + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "3.1.1" + } + } +}