Skip to content

Commit

Permalink
chore(compatibility-suite): Implement V4 matching rule and generator …
Browse files Browse the repository at this point in the history
…scenarios
  • Loading branch information
rholshausen committed Aug 11, 2023
1 parent 41e220d commit c37387f
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 11 deletions.
176 changes: 176 additions & 0 deletions compatibility-suite/src/test/groovy/steps/v4/Generators.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package steps.v4

import au.com.dius.pact.core.model.HttpRequest
import au.com.dius.pact.core.model.IRequest
import au.com.dius.pact.core.model.JsonUtils
import au.com.dius.pact.core.model.generators.GeneratorTestMode
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 steps.shared.SharedSteps.configureBody
import static steps.shared.SharedSteps.determineContentType

@SuppressWarnings('SpaceAfterOpeningBrace')
class Generators {
HttpRequest request
IRequest generatedRequest
Map<String, Object> context = [:]
GeneratorTestMode testMode = GeneratorTestMode.Provider
JsonValue originalJson
JsonValue generatedJson

@Given('a request configured with the following generators:')
void a_request_configured_with_the_following_generators(DataTable dataTable) {
request = new HttpRequest('GET', '/path/one')
def entry = dataTable.entries().first()
if (entry['body']) {
def part = configureBody(entry['body'], determineContentType(entry['body'], request.contentTypeHeader()))
request.body = part.body
request.headers.putAll(part.headers)
}
if (entry['generators']) {
JsonValue json
if (entry['generators'].startsWith('JSON:')) {
json = JsonParser.INSTANCE.parseString(entry['generators'][5..-1])
} else {
File contents = new File("pact-compatibility-suite/fixtures/${entry['generators']}")
contents.withInputStream {
json = JsonParser.INSTANCE.parseStream(it)
}
}
request.generators.categories.putAll(au.com.dius.pact.core.model.generators.Generators.fromJson(json).categories)
}
}

@Given('the generator test mode is set as {string}')
void the_generator_test_mode_is_set_as(String mode) {
testMode = mode == 'Consumer' ? GeneratorTestMode.Consumer : GeneratorTestMode.Provider
}

@When('the request is prepared for use')
void the_request_prepared_for_use() {
generatedRequest = request.generatedRequest(context, testMode)
originalJson = request.body.present ? JsonParser.INSTANCE.parseString(request.body.valueAsString()) : null
generatedJson = generatedRequest.body.present ?
JsonParser.INSTANCE.parseString(generatedRequest.body.valueAsString()) : null
}

@When('the request is prepared for use with a {string} context:')
void the_request_is_prepared_for_use_with_a_context(String type, DataTable dataTable) {
context[type] = JsonParser.parseString(dataTable.values().first()).asObject().entries
generatedRequest = request.generatedRequest(context, testMode)
originalJson = request.body.present ? JsonParser.INSTANCE.parseString(request.body.valueAsString()) : null
generatedJson = generatedRequest.body.present ?
JsonParser.INSTANCE.parseString(generatedRequest.body.valueAsString()) : null
}

@Then('the body value for {string} will have been replaced with a(n) {string}')
void the_body_value_for_will_have_been_replaced_with_a_value(String path, String type) {
def originalElement = JsonUtils.INSTANCE.fetchPath(originalJson, path)
def element = JsonUtils.INSTANCE.fetchPath(generatedJson, path)
assert originalElement != element
matchTypeOfElement(type, element)
}

static void matchTypeOfElement(String type, JsonValue element) {
switch (type) {
case 'integer' -> {
assert element.type() == 'Integer'
assert element.toString() ==~ /\d+/
}
case 'decimal number' -> {
assert element.type() == 'Decimal'
assert element.toString() ==~ /\d+\.\d+/
}
case 'hexadecimal number' -> {
assert element.type() == 'String'
assert element.toString() ==~ /[a-fA-F0-9]+/
}
case 'random string' -> {
assert element.type() == 'String'
}
case 'string from the regex' -> {
assert element.type() == 'String'
assert element.toString() ==~ /\d{1,8}/
}
case 'date' -> {
assert element.type() == 'String'
assert element.toString() ==~ /\d{4}-\d{2}-\d{2}/
}
case 'time' -> {
assert element.type() == 'String'
assert element.toString() ==~ /\d{2}:\d{2}:\d{2}/
}
case 'date-time' -> {
assert element.type() == 'String'
assert element.toString() ==~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,9}/
}
case 'UUID' -> {
assert element.type() == 'String'
UUID.fromString(element.toString())
}
case 'simple UUID' -> {
assert element.type() == 'String'
assert element.toString() ==~ /[0-9a-zA-Z]{32}/
}
case 'lower-case-hyphenated UUID' -> {
assert element.type() == 'String'
UUID.fromString(element.toString())
}
case 'upper-case-hyphenated UUID' -> {
assert element.type() == 'String'
UUID.fromString(element.toString())
}
case 'URN UUID' -> {
assert element.type() == 'String'
assert element.toString().startsWith('urn:uuid:')
UUID.fromString(element.toString().substring('urn:uuid:'.length()))
}
case 'boolean' -> {
assert element.type() == 'Boolean'
}
default -> throw new AssertionError("Invalid type: $type")
}
}

@Then('the body value for {string} will have been replaced with {string}')
void the_body_value_for_will_have_been_replaced_with_value(String path, String value) {
def originalElement = JsonUtils.INSTANCE.fetchPath(originalJson, path)
def element = JsonUtils.INSTANCE.fetchPath(generatedJson, path)
assert originalElement != element
assert element.type() == 'String'
assert element.toString() == value
}

@Then('the request {string} will be set as {string}')
void the_request_will_be_set_as(String part, String value) {
switch (part) {
case 'path' -> {
assert generatedRequest.path == value
}
default -> throw new AssertionError("Invalid HTTP part: $part")
}
}

@Then('the request {string} will match {string}')
void the_request_will_match(String part, String regex) {
switch (part) {
case 'path' -> {
assert generatedRequest.path ==~ regex
}
case ~/^header.*/ -> {
def header = (part =~ /\[(.*)]/)[0][1]
assert generatedRequest.headers[header].every { it ==~ regex }
}
case ~/^queryParameter.*/ -> {
def name = (part =~ /\[(.*)]/)[0][1]
assert generatedRequest.query[name].every { it ==~ regex }
}
default -> throw new AssertionError("Invalid HTTP part: $part")
}
}
}
16 changes: 13 additions & 3 deletions compatibility-suite/src/test/groovy/steps/v4/HttpMatching.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ class HttpMatching {
expectedRequest = new HttpRequest()
def entry = dataTable.entries().first()
if (entry['body']) {
def part = configureBody(entry['body'], determineContentType(entry['body'], expectedRequest.contentTypeHeader()))
def part
if (entry['content type']) {
part = configureBody(entry['body'], entry['content type'])
} else {
part = configureBody(entry['body'], determineContentType(entry['body'], expectedRequest.contentTypeHeader()))
}
expectedRequest.body = part.body
expectedRequest.headers.putAll(part.headers)
}
Expand All @@ -111,8 +116,13 @@ class HttpMatching {
receivedRequests << new HttpRequest()
def entry = dataTable.entries().first()
if (entry['body']) {
def part = configureBody(entry['body'], determineContentType(entry['body'],
receivedRequests[0].contentTypeHeader()))
def part
if (entry['content type']) {
part = configureBody(entry['body'], entry['content type'])
} else {
part = configureBody(entry['body'], determineContentType(entry['body'],
receivedRequests[0].contentTypeHeader()))
}
receivedRequests[0].body = part.body
receivedRequests[0].headers.putAll(part.headers)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ object JsonContentMatcher : ContentMatcher, KLogging() {
val expectedEntries = expectedValues.entries
val actualEntries = actualValues.entries
if (context.matcherDefined(path)) {
logger.debug { "compareMaps: matcher defined for path $path" }
for (matcher in context.selectBestMatcher(path).rules) {
result.addAll(Matchers.compareMaps(path, matcher, expectedEntries, actualEntries, context, generateDiff) {
p, expected, actual -> compare(p, expected ?: JsonValue.Null, actual ?: JsonValue.Null, context)
p, expected, actual, ctx -> compare(p, expected ?: JsonValue.Null, actual ?: JsonValue.Null, ctx)
})
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fun <M : Mismatch> domatch(
mismatchFn: MismatchFactory<M>,
cascaded: Boolean
): List<M> {
logger.debug { "Matching value at $path with $matcher" }
logger.debug { "Matching value $actual at $path with $matcher" }
return when (matcher) {
is RegexMatcher -> matchRegex(matcher.regex, path, expected, actual, mismatchFn)
is TypeMatcher -> matchType(path, expected, actual, mismatchFn, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,55 @@ object Matchers : KLogging() {
actualEntries: Map<String, T>,
context: MatchingContext,
generateDiff: () -> String,
callback: (List<String>, T?, T?) -> List<BodyItemMatchResult>
callback: (List<String>, T?, T?, MatchingContext) -> List<BodyItemMatchResult>
): List<BodyItemMatchResult> {
val result = mutableListOf<BodyItemMatchResult>()
if (matcher is ValuesMatcher || matcher is EachValueMatcher) {
logger.debug { "Matcher is ValuesMatcher or EachValueMatcher, checking just the values" }
val subContext = if (matcher is EachValueMatcher) {
val associatedRules = matcher.definition.rules.mapNotNull {
when (it) {
is Either.A -> it.value
is Either.B -> {
result.add(
BodyItemMatchResult(
constructPath(path),
listOf(
BodyMismatch(
expectedEntries, actualEntries,
"Found an un-resolved reference ${it.value.name}", constructPath(path), generateDiff()
)
)
)
)
null
}
}
}
val matcherPath = constructPath(path) + ".*"
MatchingContext(
MatchingRuleCategory("body", mutableMapOf(
matcherPath to MatchingRuleGroup(associatedRules.toMutableList())
)),
context.allowUnexpectedKeys,
context.pluginConfiguration
)
} else {
context
}
actualEntries.entries.forEach { (key, value) ->
if (expectedEntries.containsKey(key)) {
result.addAll(callback(path + key, expectedEntries[key]!!, value))
result.addAll(callback(path + key, expectedEntries[key]!!, value, subContext))
} else {
result.addAll(callback(path + key, expectedEntries.values.firstOrNull(), value))
result.addAll(callback(path + key, expectedEntries.values.firstOrNull(), value, subContext))
}
}
} else {
result.addAll(context.matchKeys(path, expectedEntries, actualEntries, generateDiff))
if (matcher !is EachKeyMatcher) {
expectedEntries.entries.forEach { (key, value) ->
if (actualEntries.containsKey(key)) {
result.addAll(callback(path + key, value, actualEntries[key]))
result.addAll(callback(path + key, value, actualEntries[key], context))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ data class MatchingContext @JvmOverloads constructor(

val result = mutableListOf<BodyItemMatchResult>()

if (!directMatcherDefined(path, listOf(EachValueMatcher::class.java, ValuesMatcher::class.java))) {
if (!directMatcherDefined(path, listOf(EachKeyMatcher::class.java, EachValueMatcher::class.java, ValuesMatcher::class.java))) {
if (allowUnexpectedKeys && missingKeys.isNotEmpty()) {
result.add(
BodyItemMatchResult(
Expand Down Expand Up @@ -226,7 +226,10 @@ object Matching : KLogging() {
listOf(BodyItemMatchResult("$", domatch(rootMatcher, listOf("$"), expected.body.orEmpty(),
actual.body.orEmpty(), BodyMismatchFactory))))
expectedContentType.getBaseType() == actualContentType.getBaseType() -> {
val matcher = MatchingConfig.lookupContentMatcher(actualContentType.getBaseType())
var matcher = MatchingConfig.lookupContentMatcher(actualContentType.getBaseType())
if (matcher == null) {
matcher = MatchingConfig.lookupContentMatcher(actualContentType.getSupertype().toString())
}
if (matcher != null) {
logger.debug { "Found a matcher for $actualContentType -> $matcher" }
matcher.matchBody(expected.body, actual.body, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ContentTypeSpec extends Specification {
'application/x-thrift' || true
'application/x-other' || false
'application/graphql' || true
'application/vnd.siren+json' || true

contentType = new ContentType(value)
}
Expand Down Expand Up @@ -113,6 +114,7 @@ class ContentTypeSpec extends Specification {
'application/json' || false
'application/hal+json' || false
'application/HAL+JSON' || false
'application/vnd.siren+json' || false
'application/xml' || false
'application/atom+xml' || false
'application/octet-stream' || true
Expand Down Expand Up @@ -142,6 +144,7 @@ class ContentTypeSpec extends Specification {
'application/json' || 'application/javascript'
'application/hal+json' || 'application/json'
'application/HAL+JSON' || 'application/json'
'application/vnd.siren+json' || 'application/json'
'application/xml' || 'text/plain'
'application/atom+xml' || 'application/xml'
'application/octet-stream' || null
Expand Down

0 comments on commit c37387f

Please sign in to comment.