@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import au.com.dius.pact.consumer.dsl.DslPart
|
||||
import au.com.dius.pact.consumer.dsl.PactDslJsonArray
|
||||
import au.com.dius.pact.consumer.dsl.PactDslJsonBody
|
||||
import au.com.dius.pact.core.model.OptionalBody
|
||||
import au.com.dius.pact.core.model.Request
|
||||
import au.com.dius.pact.core.model.Response
|
||||
import au.com.dius.pact.core.model.generators.Generator
|
||||
import au.com.dius.pact.core.model.messaging.Message
|
||||
import com.jayway.jsonpath.Configuration
|
||||
import com.jayway.jsonpath.internal.EvaluationContext
|
||||
import com.jayway.jsonpath.internal.Path
|
||||
import com.jayway.jsonpath.internal.PathRef
|
||||
import com.jayway.jsonpath.internal.path.PathCompiler
|
||||
import groovy.json.JsonException
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.Body
|
||||
import org.springframework.cloud.contract.spec.internal.ClientDslProperty
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty
|
||||
import org.springframework.cloud.contract.spec.internal.ServerDslProperty
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils
|
||||
|
||||
/**
|
||||
* @author Tim Ysewyn
|
||||
* @Since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class BodyConverter {
|
||||
|
||||
private static final JsonSlurper jsonSlurper = new JsonSlurper()
|
||||
|
||||
static DslPart toPactBody(Body body, Closure dslPropertyValueExtractor) {
|
||||
return traverse(body, null, dslPropertyValueExtractor)
|
||||
}
|
||||
|
||||
static DslPart toPactBody(DslProperty dslProperty, Closure dslPropertyValueExtractor) {
|
||||
return traverse(dslProperty, null, dslPropertyValueExtractor)
|
||||
}
|
||||
|
||||
private static DslPart traverse(Object value, DslPart parent, Closure dslPropertyValueExtractor) {
|
||||
boolean isRoot = parent == null
|
||||
Object v = value
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor(v)
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
|
||||
}
|
||||
if (v instanceof String) {
|
||||
v = v.trim()
|
||||
if (v.startsWith("{") && v.endsWith("}")) {
|
||||
try {
|
||||
v = jsonSlurper.parseText(v as String)
|
||||
}
|
||||
catch (JsonException ex) { /*it wasn't a JSON string after all...*/
|
||||
}
|
||||
}
|
||||
}
|
||||
DslPart p = isRoot ? createRootDslPart(v) : parent
|
||||
if (v instanceof Map) {
|
||||
processMap(v as Map, p as PactDslJsonBody, dslPropertyValueExtractor)
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
processCollection(v as Collection, p as PactDslJsonArray, dslPropertyValueExtractor)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
private static DslPart createRootDslPart(Object value) {
|
||||
return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody()
|
||||
}
|
||||
|
||||
private static void processCollection(Collection values, PactDslJsonArray jsonArray, Closure dslPropertyValueExtractor) {
|
||||
values.forEach({
|
||||
Object v = it
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor(v)
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
|
||||
}
|
||||
if (v == null) {
|
||||
jsonArray.nullValue()
|
||||
}
|
||||
else if (v instanceof String) {
|
||||
jsonArray.string(v)
|
||||
}
|
||||
else if (v instanceof Number) {
|
||||
jsonArray.number(v)
|
||||
}
|
||||
else if (v instanceof Map) {
|
||||
PactDslJsonBody current = jsonArray.object()
|
||||
traverse(v, current, dslPropertyValueExtractor)
|
||||
current.closeObject()
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
PactDslJsonArray current = jsonArray.array()
|
||||
traverse(v, current, dslPropertyValueExtractor)
|
||||
current.closeArray()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static void processMap(Map<String, Object> values, PactDslJsonBody jsonObject, Closure dslPropertyValueExtractor) {
|
||||
values.forEach({ String k, Object v ->
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor(v)
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue(v, dslPropertyValueExtractor)
|
||||
}
|
||||
if (v == null) {
|
||||
jsonObject.nullValue(k)
|
||||
}
|
||||
else if (v instanceof String) {
|
||||
jsonObject.stringType(k, v)
|
||||
}
|
||||
else if (v instanceof Number) {
|
||||
jsonObject.numberValue(k, v)
|
||||
}
|
||||
else if (v instanceof Map) {
|
||||
PactDslJsonBody current = jsonObject.object(k)
|
||||
traverse(v, current, dslPropertyValueExtractor)
|
||||
current.closeObject()
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
PactDslJsonArray current = jsonObject.array(k)
|
||||
traverse(v, current, dslPropertyValueExtractor)
|
||||
current.closeArray()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static def toSCCBody(Request request) {
|
||||
def body = parseBody(request.body)
|
||||
if (request.generators.isNotEmpty()
|
||||
&& request.generators.categories.
|
||||
containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body, request.generators.categories.
|
||||
get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
|
||||
return new DslProperty<Object>(new ClientDslProperty(pattern, generatedValue), currentValue)
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
static def toSCCBody(Response response) {
|
||||
def body = parseBody(response.body)
|
||||
if (response.generators.isNotEmpty()
|
||||
&& response.generators.categories.
|
||||
containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body, response.generators.categories.
|
||||
get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
|
||||
return new DslProperty<Object>(currentValue, new ServerDslProperty(pattern, generatedValue))
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
static def toSCCBody(Message message) {
|
||||
def body = parseBody(message.contents)
|
||||
if (message.generators.isNotEmpty()
|
||||
&& message.generators.categories.
|
||||
containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body, message.generators.categories.
|
||||
get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue ->
|
||||
return new DslProperty<Object>(new ClientDslProperty(pattern, generatedValue), currentValue)
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
private static def parseBody(OptionalBody optionalBody) {
|
||||
if (optionalBody.present) {
|
||||
return new JsonSlurper().parse(optionalBody.value)
|
||||
}
|
||||
else {
|
||||
return optionalBody.value
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyGenerators(def body, Map<String, Generator> generatorsPerPath, Closure<DslProperty> dslPropertyProvider) {
|
||||
Configuration configuration = Configuration.defaultConfiguration()
|
||||
generatorsPerPath.each { String path, Generator generator ->
|
||||
Path compiledPath = PathCompiler.compile(path)
|
||||
EvaluationContext evaluationContext = compiledPath.
|
||||
evaluate(body, body, configuration, true)
|
||||
evaluationContext.updateOperations().each { PathRef pathRef ->
|
||||
pathRef.convert({ Object currentValue, Configuration config ->
|
||||
return ValueGeneratorConverter.
|
||||
convert(generator) { Pattern pattern, Object generatedValue ->
|
||||
return dslPropertyProvider(currentValue, pattern, generatedValue)
|
||||
}
|
||||
}, configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import au.com.dius.pact.core.model.matchingrules.Category
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.EqualsMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers
|
||||
import org.springframework.cloud.contract.spec.internal.MatchingType
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns
|
||||
|
||||
/**
|
||||
* @author Tim Ysewyn
|
||||
* @Since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class MatchingRulesConverter {
|
||||
|
||||
static Category matchingRulesForBody(BodyMatchers bodyMatchers) {
|
||||
return matchingRulesFor("body", bodyMatchers)
|
||||
}
|
||||
|
||||
private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) {
|
||||
Category category = new Category(categoryName)
|
||||
bodyMatchers.matchers().forEach({ BodyMatcher it ->
|
||||
String key = getMatcherKey(it.path())
|
||||
MatchingType matchingType = it.matchingType()
|
||||
switch (matchingType) {
|
||||
case MatchingType.NULL:
|
||||
category.addRule(key, NullMatcher.INSTANCE)
|
||||
break
|
||||
case MatchingType.EQUALITY:
|
||||
category.addRule(key, EqualsMatcher.INSTANCE)
|
||||
break
|
||||
case MatchingType.TYPE:
|
||||
if (it.minTypeOccurrence() && it.maxTypeOccurrence()) {
|
||||
category.
|
||||
addRule(key, new MinMaxTypeMatcher(it.minTypeOccurrence(), it.
|
||||
maxTypeOccurrence()))
|
||||
}
|
||||
else if (it.minTypeOccurrence()) {
|
||||
category.addRule(key, new MinTypeMatcher(it.minTypeOccurrence()))
|
||||
}
|
||||
else if (it.maxTypeOccurrence()) {
|
||||
category.addRule(key, new MaxTypeMatcher(it.maxTypeOccurrence()))
|
||||
}
|
||||
else {
|
||||
category.addRule(key, TypeMatcher.INSTANCE)
|
||||
}
|
||||
break
|
||||
case MatchingType.DATE:
|
||||
category.addRule(key, new DateMatcher())
|
||||
break
|
||||
case MatchingType.TIME:
|
||||
category.addRule(key, new TimeMatcher())
|
||||
break
|
||||
case MatchingType.TIMESTAMP:
|
||||
category.addRule(key, new TimestampMatcher())
|
||||
break
|
||||
case MatchingType.REGEX:
|
||||
String pattern = it.value().toString()
|
||||
if (pattern == RegexPatterns.number().pattern()) {
|
||||
category.
|
||||
addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER))
|
||||
}
|
||||
else if (pattern == RegexPatterns.anInteger().pattern()) {
|
||||
category.
|
||||
addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))
|
||||
}
|
||||
else if (pattern == RegexPatterns.aDouble().pattern()) {
|
||||
category.
|
||||
addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL))
|
||||
}
|
||||
else {
|
||||
category.addRule(key, new RegexMatcher(pattern))
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return category
|
||||
}
|
||||
|
||||
private static String getMatcherKey(String path) {
|
||||
return "${path.startsWith('$') ? path.substring(1) : path}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import au.com.dius.pact.consumer.MessagePactBuilder
|
||||
import au.com.dius.pact.consumer.dsl.DslPart
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty
|
||||
import org.springframework.cloud.contract.spec.internal.Header
|
||||
import org.springframework.cloud.contract.spec.internal.Headers
|
||||
import org.springframework.cloud.contract.spec.internal.Input
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils
|
||||
|
||||
/**
|
||||
* Creator of {@link MessagePact} instances
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class MessagePactCreator {
|
||||
|
||||
private static final Closure clientValueExtractor = { DslProperty property -> property.clientValue }
|
||||
|
||||
MessagePact createFromContract(List<Contract> contracts) {
|
||||
if (contracts.empty) {
|
||||
return null
|
||||
}
|
||||
Names names = NamingUtil.name(contracts.get(0))
|
||||
MessagePactBuilder pactBuilder = MessagePactBuilder.consumer(names.consumer)
|
||||
.hasPactWith(names.producer)
|
||||
contracts.each { Contract contract ->
|
||||
pactBuilder = pactBuilder
|
||||
.given(getGiven(contract.input))
|
||||
.expectsToReceive(getOutcome(contract))
|
||||
if (contract.outputMessage) {
|
||||
OutputMessage message = contract.outputMessage
|
||||
if (message.body) {
|
||||
DslPart pactResponseBody = BodyConverter.
|
||||
toPactBody(message.body, clientValueExtractor)
|
||||
if (message.bodyMatchers) {
|
||||
pactResponseBody.setMatchers(MatchingRulesConverter.
|
||||
matchingRulesForBody(message.bodyMatchers))
|
||||
}
|
||||
pactResponseBody.setGenerators(ValueGeneratorConverter.
|
||||
extract(message, { DslProperty dslProperty -> dslProperty.serverValue }))
|
||||
pactBuilder = pactBuilder.withContent(pactResponseBody)
|
||||
}
|
||||
if (message.headers) {
|
||||
pactBuilder = pactBuilder.withMetadata(getMetadata(message.headers))
|
||||
}
|
||||
}
|
||||
}
|
||||
return pactBuilder.toPact()
|
||||
}
|
||||
|
||||
private String getGiven(Input input) {
|
||||
if (input.triggeredBy) {
|
||||
return input.triggeredBy.executionCommand
|
||||
}
|
||||
else if (input.messageFrom) {
|
||||
return "received message from " + clientValueExtractor.call(input.messageFrom)
|
||||
}
|
||||
else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutcome(Contract contract) {
|
||||
if (contract.outputMessage) {
|
||||
OutputMessage message = contract.outputMessage
|
||||
return "message sent to " + clientValueExtractor.call(message.sentTo)
|
||||
}
|
||||
else {
|
||||
return "assert that " + contract.input.assertThat.executionCommand
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> getMetadata(Headers headers) {
|
||||
return headers.entries.collectEntries({ Header header ->
|
||||
return [(header.name): extractValue(header)]
|
||||
})
|
||||
}
|
||||
|
||||
private String extractValue(Object value) {
|
||||
Object v = value
|
||||
if (v instanceof DslProperty) {
|
||||
v = clientValueExtractor.call(v)
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue(v, clientValueExtractor)
|
||||
}
|
||||
if (v instanceof String) {
|
||||
return v
|
||||
}
|
||||
else {
|
||||
return v.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import au.com.dius.pact.core.model.matchingrules.Category
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRule
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.RuleLogic
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
|
||||
import au.com.dius.pact.core.model.messaging.Message
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
|
||||
|
||||
/**
|
||||
* Creator of {@link Contract} instances
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class MessagingSCContractCreator {
|
||||
|
||||
private static final String FULL_BODY = '$'
|
||||
private static final String DESTINATION_KEY = "sentTo"
|
||||
private static final List<String> NON_HEADER_META_DATA = [DESTINATION_KEY]
|
||||
|
||||
Collection<Contract> convertFrom(MessagePact pact) {
|
||||
return pact.messages.collect({ Message message ->
|
||||
Contract.make {
|
||||
label("$message.description")
|
||||
if (!message.providerStates.isEmpty()) {
|
||||
input {
|
||||
triggeredBy(getTriggeredBy(message))
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
if (message.contents.present) {
|
||||
body(BodyConverter.toSCCBody(message))
|
||||
Category bodyRules = message.matchingRules.
|
||||
rulesForCategory('body')
|
||||
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
|
||||
bodyMatchers {
|
||||
bodyRules.matchingRules.
|
||||
each { String key, MatchingRuleGroup ruleGroup ->
|
||||
if (ruleGroup.ruleLogic != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
|
||||
}
|
||||
if (FULL_BODY == key) {
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter.
|
||||
transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.contents.value)
|
||||
jsonPaths.each {
|
||||
jsonPath(it.keyBeforeChecking(), byType())
|
||||
}
|
||||
}
|
||||
else {
|
||||
ruleGroup.rules.each { MatchingRule rule ->
|
||||
if (rule instanceof NullMatcher) {
|
||||
jsonPath(key, byNull())
|
||||
}
|
||||
else if (rule instanceof RegexMatcher) {
|
||||
jsonPath(key, byRegex(rule.regex))
|
||||
}
|
||||
else if (rule instanceof DateMatcher) {
|
||||
jsonPath(key, byDate())
|
||||
}
|
||||
else if (rule instanceof TimeMatcher) {
|
||||
jsonPath(key, byTime())
|
||||
}
|
||||
else if (rule instanceof TimestampMatcher) {
|
||||
jsonPath(key, byTimestamp())
|
||||
}
|
||||
else if (rule instanceof MinTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
minOccurrence((rule as MinTypeMatcher).min)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof MinMaxTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
minOccurrence((rule as MinMaxTypeMatcher).min)
|
||||
maxOccurrence((rule as MinMaxTypeMatcher).max)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof MaxTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
maxOccurrence((rule as MaxTypeMatcher).max)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof TypeMatcher) {
|
||||
jsonPath(key, byType())
|
||||
}
|
||||
else if (rule instanceof NumberTypeMatcher) {
|
||||
switch (rule.numberType) {
|
||||
case NumberTypeMatcher.NumberType.NUMBER:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.number()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.INTEGER:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.anInteger()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.DECIMAL:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.aDouble()))
|
||||
break
|
||||
default:
|
||||
throw new RuntimeException("Unsupported number type!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!message.metaData.isEmpty()) {
|
||||
headers {
|
||||
message.metaData.each { String k, Object val ->
|
||||
String v = val.toString()
|
||||
if (k.equalsIgnoreCase("contentType")) {
|
||||
messagingContentType(v)
|
||||
}
|
||||
else if (!NON_HEADER_META_DATA.contains(k)) {
|
||||
header(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String dest = findDestination(message)
|
||||
if (dest) {
|
||||
sentTo(dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private String getTriggeredBy(Message message) {
|
||||
return message.providerStates.first().name
|
||||
.replace(':', ' ')
|
||||
.replace(' ', '_')
|
||||
.replace('(', '')
|
||||
.replace(')', '')
|
||||
.uncapitalize() + "()"
|
||||
}
|
||||
|
||||
private String findDestination(Message message) {
|
||||
return message.metaData.get(DESTINATION_KEY) ?: ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
* @since
|
||||
*/
|
||||
@PackageScope
|
||||
@CompileStatic
|
||||
final class NamingUtil {
|
||||
|
||||
// consumer___producer___testname
|
||||
private static final String SEPARATOR = "___"
|
||||
|
||||
protected static Names name(Contract contract) {
|
||||
String contractName = contract.name
|
||||
if (!contractName || !contractName.contains(SEPARATOR)) {
|
||||
return new Names(["Consumer", "Provider", ""] as String[])
|
||||
}
|
||||
return new Names(contractName.split(SEPARATOR))
|
||||
}
|
||||
}
|
||||
|
||||
@PackageScope
|
||||
@CompileStatic
|
||||
class Names {
|
||||
final String consumer
|
||||
final String producer
|
||||
final String test
|
||||
|
||||
Names(String[] strings) {
|
||||
this.consumer = strings[0]
|
||||
this.producer = strings[1]
|
||||
this.test = strings.length >= 2 ? strings[2] : ""
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
String toString() {
|
||||
return this.consumer + "_" + this.producer
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import au.com.dius.pact.core.model.DefaultPactReader
|
||||
import au.com.dius.pact.core.model.Pact
|
||||
import au.com.dius.pact.core.model.PactSpecVersion
|
||||
import au.com.dius.pact.core.model.RequestResponsePact
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.ContractConverter
|
||||
/**
|
||||
* Converter of JSON PACT file
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@CompileStatic
|
||||
class PactContractConverter implements ContractConverter<Collection<Pact>> {
|
||||
|
||||
private final RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator()
|
||||
private final MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator()
|
||||
private final RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator()
|
||||
private final MessagePactCreator messagePactCreator = new MessagePactCreator()
|
||||
|
||||
@Override
|
||||
boolean isAccepted(File file) {
|
||||
try {
|
||||
DefaultPactReader.INSTANCE.loadPact(file)
|
||||
return true
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<Contract> convertFrom(File file) {
|
||||
Pact pact = DefaultPactReader.INSTANCE.loadPact(file)
|
||||
if (pact instanceof RequestResponsePact) {
|
||||
return requestResponseSCContractCreator.
|
||||
convertFrom(pact as RequestResponsePact)
|
||||
}
|
||||
if (pact instanceof MessagePact) {
|
||||
return messagingSCContractCreator.convertFrom(pact as MessagePact)
|
||||
}
|
||||
throw new UnsupportedOperationException("We currently don't support pact contracts of type" + pact.class.simpleName)
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<Pact> convertTo(Collection<Contract> contracts) {
|
||||
List<Pact> pactContracts = new ArrayList<>()
|
||||
Map<String, List<Contract>> groupedContracts = contracts.
|
||||
groupBy { NamingUtil.name(it).toString() }
|
||||
for (List<Contract> list : groupedContracts.values()) {
|
||||
List<Contract> httpOnly = list.findAll { it.request }
|
||||
List<Contract> messagingOnly = list.findAll { it.input }
|
||||
RequestResponsePact responsePact = requestResponsePactCreator.
|
||||
createFromContract(httpOnly)
|
||||
if (responsePact) {
|
||||
pactContracts.add(responsePact)
|
||||
}
|
||||
MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly)
|
||||
if (messagePact) {
|
||||
pactContracts.add(messagePact)
|
||||
}
|
||||
}
|
||||
return pactContracts
|
||||
}
|
||||
|
||||
@Override
|
||||
Map<String, byte[]> store(Collection<Pact> contracts) {
|
||||
return contracts.collectEntries {
|
||||
return [(name(it)): JsonOutput.
|
||||
prettyPrint(JsonOutput.toJson(it.toMap(PactSpecVersion.V3))).bytes]
|
||||
}
|
||||
}
|
||||
|
||||
protected String name(Pact contract) {
|
||||
return contract.consumer.name + "_" + contract.provider.name + "_" + String.
|
||||
valueOf(Math.abs(contract.hashCode())) + ".json"
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import au.com.dius.pact.consumer.ConsumerPactBuilder
|
||||
import au.com.dius.pact.consumer.dsl.DslPart
|
||||
import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath
|
||||
import au.com.dius.pact.consumer.dsl.PactDslResponse
|
||||
import au.com.dius.pact.consumer.dsl.PactDslWithProvider
|
||||
import au.com.dius.pact.core.model.RequestResponsePact
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.internal.Body
|
||||
import org.springframework.cloud.contract.spec.internal.Cookies
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty
|
||||
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
|
||||
import org.springframework.cloud.contract.spec.internal.Header
|
||||
import org.springframework.cloud.contract.spec.internal.QueryParameters
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty
|
||||
import org.springframework.cloud.contract.spec.internal.Request
|
||||
import org.springframework.cloud.contract.spec.internal.Response
|
||||
|
||||
/**
|
||||
* Creator of {@link RequestResponsePact} instances
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class RequestResponsePactCreator {
|
||||
|
||||
RequestResponsePact createFromContract(List<Contract> contracts) {
|
||||
if (contracts.empty) {
|
||||
return null
|
||||
}
|
||||
Names names = NamingUtil.name(contracts.get(0))
|
||||
PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder
|
||||
.consumer(names.consumer).hasPactWith(names.producer)
|
||||
PactDslResponse pactDslResponse = null
|
||||
contracts.each { Contract contract ->
|
||||
assertNoExecutionProperty(contract)
|
||||
PactDslRequestWithPath pactDslRequest = pactDslResponse ?
|
||||
createPactDslRequestWithPath(contract, pactDslResponse) :
|
||||
createPactDslRequestWithPath(contract, pactDslWithProvider)
|
||||
pactDslResponse = createPactDslResponse(contract, pactDslRequest)
|
||||
}
|
||||
return pactDslResponse.toPact()
|
||||
}
|
||||
|
||||
private void assertNoExecutionProperty(Contract contract) {
|
||||
assertNoExecutionPropertyInBody(contract.request.body,
|
||||
{ DslProperty dslProperty -> dslProperty.serverValue })
|
||||
assertNoExecutionPropertyInBody(contract.response.body,
|
||||
{ DslProperty dslProperty -> dslProperty.clientValue })
|
||||
}
|
||||
|
||||
private void assertNoExecutionPropertyInBody(Body body, Closure dslPropertyValueExtractor) {
|
||||
traverseValues(body, dslPropertyValueExtractor, {
|
||||
if (it instanceof ExecutionProperty) {
|
||||
throw new UnsupportedOperationException("We can't convert a contract that has execution property")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private void traverseValues(def value, Closure dslPropertyValueExtractor, Closure closure) {
|
||||
if (value instanceof DslProperty) {
|
||||
traverseValues(
|
||||
dslPropertyValueExtractor(value), dslPropertyValueExtractor, closure)
|
||||
}
|
||||
else if (value instanceof Map) {
|
||||
value.values().
|
||||
forEach({ traverseValues(it, dslPropertyValueExtractor, closure) })
|
||||
}
|
||||
else if (value instanceof Collection) {
|
||||
value.forEach({ traverseValues(it, dslPropertyValueExtractor, closure) })
|
||||
}
|
||||
else {
|
||||
closure(value)
|
||||
}
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) {
|
||||
Request request = contract.request
|
||||
PactDslRequestWithPath pactDslRequest = pactDslResponse
|
||||
.uponReceiving(contract.description ?: "")
|
||||
.path(url(request))
|
||||
.method(request.method.serverValue.toString())
|
||||
String query = query(request)
|
||||
if (query) {
|
||||
pactDslRequest = pactDslRequest.encodedQuery(query)
|
||||
}
|
||||
if (request.headers) {
|
||||
request.headers.entries.each { Header header ->
|
||||
pactDslRequest = processHeader(pactDslRequest, header)
|
||||
}
|
||||
}
|
||||
if (request.cookies) {
|
||||
pactDslRequest = processCookies(pactDslRequest, request.cookies)
|
||||
}
|
||||
if (request.body) {
|
||||
DslPart pactRequestBody = BodyConverter.
|
||||
toPactBody(request.body, { DslProperty property -> property.serverValue })
|
||||
if (request.bodyMatchers) {
|
||||
pactRequestBody.setMatchers(MatchingRulesConverter.
|
||||
matchingRulesForBody(request.bodyMatchers))
|
||||
}
|
||||
pactRequestBody.setGenerators(ValueGeneratorConverter.
|
||||
extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue }))
|
||||
pactDslRequest = pactDslRequest.body(pactRequestBody)
|
||||
}
|
||||
return pactDslRequest
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslWithProvider pactDslWithProvider) {
|
||||
Request request = contract.request
|
||||
PactDslRequestWithPath pactDslRequest = pactDslWithProvider
|
||||
.uponReceiving(contract.description ?: "")
|
||||
.path(url(request))
|
||||
.method(request.method.serverValue.toString())
|
||||
String query = query(request)
|
||||
if (query) {
|
||||
pactDslRequest = pactDslRequest.encodedQuery(query)
|
||||
}
|
||||
if (request.headers) {
|
||||
request.headers.entries.each { Header header ->
|
||||
pactDslRequest = processHeader(pactDslRequest, header)
|
||||
}
|
||||
}
|
||||
if (request.body) {
|
||||
DslPart pactRequestBody = BodyConverter.
|
||||
toPactBody(request.body, { DslProperty property -> property.serverValue })
|
||||
if (request.bodyMatchers) {
|
||||
pactRequestBody.setMatchers(MatchingRulesConverter.
|
||||
matchingRulesForBody(request.bodyMatchers))
|
||||
}
|
||||
pactRequestBody.setGenerators(ValueGeneratorConverter.
|
||||
extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue }))
|
||||
pactDslRequest = pactDslRequest.body(pactRequestBody)
|
||||
}
|
||||
return pactDslRequest
|
||||
}
|
||||
|
||||
private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) {
|
||||
Response response = contract.response
|
||||
PactDslResponse pactDslResponse = pactDslRequest.willRespondWith()
|
||||
.status(response.status.clientValue as Integer)
|
||||
if (response.headers) {
|
||||
response.headers.entries.each { Header header ->
|
||||
pactDslResponse = processHeader(pactDslResponse, header)
|
||||
}
|
||||
}
|
||||
if (response.cookies) {
|
||||
pactDslResponse = processCookies(pactDslResponse, response.cookies)
|
||||
}
|
||||
if (response.body) {
|
||||
DslPart pactResponseBody = BodyConverter.
|
||||
toPactBody(response.body, { DslProperty property -> property.clientValue })
|
||||
if (response.bodyMatchers) {
|
||||
pactResponseBody.setMatchers(MatchingRulesConverter.
|
||||
matchingRulesForBody(response.bodyMatchers))
|
||||
}
|
||||
pactResponseBody.setGenerators(ValueGeneratorConverter.
|
||||
extract(response.body, { DslProperty dslProperty -> dslProperty.serverValue }))
|
||||
pactDslResponse = pactDslResponse.body(pactResponseBody)
|
||||
}
|
||||
return pactDslResponse
|
||||
}
|
||||
|
||||
private String url(Request request) {
|
||||
if (request.urlPath) {
|
||||
return request.urlPath.serverValue.toString()
|
||||
}
|
||||
else if (request.url) {
|
||||
return request.url.serverValue.toString()
|
||||
}
|
||||
throw new IllegalStateException("No url provided")
|
||||
}
|
||||
|
||||
private String query(Request request) {
|
||||
String query = null
|
||||
QueryParameters params = queryParams(request)
|
||||
if (params) {
|
||||
query = ""
|
||||
params.parameters.eachWithIndex { param, index ->
|
||||
query += param.name + '=' + param.serverValue
|
||||
if (index + 1 < params.parameters.size()) {
|
||||
query += '&'
|
||||
}
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
private QueryParameters queryParams(Request request) {
|
||||
if (request.urlPath) {
|
||||
return request.urlPath.queryParameters
|
||||
}
|
||||
else if (request.url) {
|
||||
return request.url.queryParameters
|
||||
}
|
||||
throw new IllegalStateException("No url provided")
|
||||
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) {
|
||||
if (header.isSingleValue()) {
|
||||
String value = getDslPropertyServerValue(header).toString()
|
||||
return pactDslRequest.headers(header.name, value)
|
||||
}
|
||||
else {
|
||||
String regex = getDslPropertyClientValue(header).toString()
|
||||
String example = getDslPropertyServerValue(header).toString()
|
||||
return pactDslRequest.matchHeader(header.name, regex, example)
|
||||
}
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath processCookies(PactDslRequestWithPath pactDslRequest, Cookies cookies) {
|
||||
Map<String, Object> stubSideCookies = cookies.asStubSideMap()
|
||||
Collection<RegexProperty> regexProperties = stubSideCookies.values().findAll { it instanceof Pattern || it instanceof RegexProperty }.collect { new RegexProperty(it)}
|
||||
if (!regexProperties.empty) {
|
||||
String regex = regexProperties.collect { it.pattern() }.join("|")
|
||||
return pactDslRequest.matchHeader("Cookie", regex, testSideCookieExample(cookies))
|
||||
}
|
||||
else {
|
||||
return pactDslRequest.headers("Cookie", testSideCookieExample(cookies))
|
||||
}
|
||||
}
|
||||
|
||||
private String testSideCookieExample(Cookies cookies) {
|
||||
return cookies.asTestSideMap().collect { it.key + "=" + it.value.toString() }.join(";")
|
||||
}
|
||||
|
||||
private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) {
|
||||
if (header.isSingleValue()) {
|
||||
String value = getDslPropertyClientValue(header).toString()
|
||||
return pactDslResponse.headers([(header.name): value])
|
||||
}
|
||||
else {
|
||||
String regex = getDslPropertyServerValue(header).toString()
|
||||
String example = getDslPropertyClientValue(header).toString()
|
||||
return pactDslResponse.matchHeader(header.name, regex, example)
|
||||
}
|
||||
}
|
||||
|
||||
private PactDslResponse processCookies(PactDslResponse pactDslResponse, Cookies cookies) {
|
||||
Map<String, Object> testSideCookies = cookies.asTestSideMap()
|
||||
Collection<RegexProperty> regexProperties = testSideCookies.values().findAll { it instanceof Pattern || it instanceof RegexProperty }.collect { new RegexProperty(it)}
|
||||
if (!regexProperties.empty) {
|
||||
String regex = regexProperties.collect { it.pattern() }.join("|")
|
||||
return pactDslResponse.matchHeader("Cookie", regex, stubSideCookieExample(cookies))
|
||||
}
|
||||
else {
|
||||
return pactDslResponse.headers(["Cookie": stubSideCookieExample(cookies)])
|
||||
}
|
||||
}
|
||||
|
||||
private String stubSideCookieExample(Cookies cookies) {
|
||||
return cookies.asStubSideMap().collect { it.key + "=" + it.value.toString() }.join(";")
|
||||
}
|
||||
|
||||
private Object getDslPropertyClientValue(Object o) {
|
||||
Object value = o
|
||||
if (value instanceof DslProperty) {
|
||||
value = getDslPropertyClientValue(value.getClientValue())
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private Object getDslPropertyServerValue(Object o) {
|
||||
Object value = o
|
||||
if (value instanceof DslProperty) {
|
||||
value = getDslPropertyServerValue(value.getServerValue())
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import au.com.dius.pact.core.model.OptionalBody
|
||||
import au.com.dius.pact.core.model.ProviderState
|
||||
import au.com.dius.pact.core.model.Request
|
||||
import au.com.dius.pact.core.model.RequestResponseInteraction
|
||||
import au.com.dius.pact.core.model.RequestResponsePact
|
||||
import au.com.dius.pact.core.model.Response
|
||||
import au.com.dius.pact.core.model.matchingrules.Category
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRule
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.RuleLogic
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
|
||||
/**
|
||||
* Creator of {@link Contract} instances
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class RequestResponseSCContractCreator {
|
||||
|
||||
private static final String FULL_BODY = '$'
|
||||
|
||||
Collection<Contract> convertFrom(RequestResponsePact pact) {
|
||||
return pact.interactions.collect { RequestResponseInteraction interaction ->
|
||||
Contract.make {
|
||||
description(buildDescription(interaction))
|
||||
request {
|
||||
Request request = interaction.request
|
||||
method(request.method)
|
||||
if (request.query) {
|
||||
url(request.path) {
|
||||
queryParameters {
|
||||
request.query.each { String key, List<String> value ->
|
||||
value.each { String singleValue ->
|
||||
parameter(key, singleValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
url(request.path)
|
||||
}
|
||||
if (request.headers) {
|
||||
Category headerRules = request.matchingRules.
|
||||
rulesForCategory('header')
|
||||
headers {
|
||||
request.headers.each { String k, List<String> v ->
|
||||
if (k.equalsIgnoreCase("Cookie")) {
|
||||
return
|
||||
}
|
||||
if (headerRules.matchingRules.containsKey(k)) {
|
||||
MatchingRuleGroup ruleGroup = headerRules.matchingRules.
|
||||
get(k)
|
||||
if (ruleGroup.rules.size() > 1) {
|
||||
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
|
||||
}
|
||||
MatchingRule rule = ruleGroup.rules[0]
|
||||
if (rule instanceof RegexMatcher) {
|
||||
v.each({
|
||||
header(k, $(c(regex(((RegexMatcher) rule).getRegex())),
|
||||
p(it)))
|
||||
})
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
|
||||
}
|
||||
}
|
||||
else {
|
||||
v.each({
|
||||
header(k, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request.headers.containsKey("Cookie")) {
|
||||
Category headerRules = request.matchingRules.
|
||||
rulesForCategory('header')
|
||||
String[] splitHeader = request.headers.get("Cookie").first().split(";")
|
||||
Map<String, String> foundCookies = splitHeader.collectEntries {
|
||||
String[] keyValue = it.split("=")
|
||||
return [(keyValue[0]): keyValue[1]]
|
||||
}
|
||||
cookies {
|
||||
foundCookies.each { k, v ->
|
||||
if (headerRules.matchingRules.containsKey("Cookie")) {
|
||||
MatchingRuleGroup ruleGroup = headerRules.matchingRules.
|
||||
get("Cookie")
|
||||
if (ruleGroup.rules.size() > 1) {
|
||||
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
|
||||
}
|
||||
MatchingRule rule = ruleGroup.rules[0]
|
||||
if (rule instanceof RegexMatcher) {
|
||||
v.each({
|
||||
cookie(k, $(c(regex(((RegexMatcher) rule).getRegex())),
|
||||
p(it)))
|
||||
})
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
|
||||
}
|
||||
}
|
||||
else {
|
||||
cookie(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request.body.state == OptionalBody.State.PRESENT) {
|
||||
def parsedBody = BodyConverter.toSCCBody(request)
|
||||
if (parsedBody instanceof Map) {
|
||||
body(parsedBody as Map)
|
||||
}
|
||||
else if (parsedBody instanceof List) {
|
||||
body(parsedBody as List)
|
||||
}
|
||||
else {
|
||||
body(parsedBody.toString())
|
||||
}
|
||||
}
|
||||
Category bodyRules = request.matchingRules.rulesForCategory('body')
|
||||
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
|
||||
bodyMatchers {
|
||||
bodyRules.matchingRules.
|
||||
each { String key, MatchingRuleGroup ruleGroup ->
|
||||
if (ruleGroup.ruleLogic != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
|
||||
}
|
||||
|
||||
ruleGroup.rules.each { MatchingRule rule ->
|
||||
if (rule instanceof RegexMatcher) {
|
||||
jsonPath(key, byRegex(rule.regex))
|
||||
}
|
||||
else if (rule instanceof DateMatcher) {
|
||||
jsonPath(key, byDate())
|
||||
}
|
||||
else if (rule instanceof TimeMatcher) {
|
||||
jsonPath(key, byTime())
|
||||
}
|
||||
else if (rule instanceof TimestampMatcher) {
|
||||
jsonPath(key, byTimestamp())
|
||||
}
|
||||
else if (rule instanceof NumberTypeMatcher) {
|
||||
switch (rule.numberType) {
|
||||
case NumberTypeMatcher.NumberType.NUMBER:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.number()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.INTEGER:
|
||||
jsonPath(key, byRegex(RegexPatterns.
|
||||
anInteger()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.DECIMAL:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.aDouble()))
|
||||
break
|
||||
default:
|
||||
throw new RuntimeException("Unsupported number type!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
Response response = interaction.response
|
||||
status(response.status)
|
||||
if (response.body.present) {
|
||||
def parsedBody = BodyConverter.toSCCBody(response)
|
||||
if (parsedBody instanceof Map) {
|
||||
body(parsedBody as Map)
|
||||
}
|
||||
else if (parsedBody instanceof List) {
|
||||
body(parsedBody as List)
|
||||
}
|
||||
else {
|
||||
body(parsedBody.toString())
|
||||
}
|
||||
}
|
||||
Category bodyRules = response.matchingRules.rulesForCategory('body')
|
||||
if (bodyRules && !bodyRules.matchingRules.isEmpty()) {
|
||||
bodyMatchers {
|
||||
bodyRules.matchingRules.
|
||||
each { String key, MatchingRuleGroup ruleGroup ->
|
||||
if (ruleGroup.ruleLogic != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported")
|
||||
}
|
||||
|
||||
if (FULL_BODY == key) {
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter.
|
||||
transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(response.body.value instanceof byte[] ? new String(response.body.value) : response.body.value)
|
||||
jsonPaths.each {
|
||||
jsonPath(it.keyBeforeChecking(), byType())
|
||||
}
|
||||
}
|
||||
else {
|
||||
ruleGroup.rules.each { MatchingRule rule ->
|
||||
if (rule instanceof NullMatcher) {
|
||||
jsonPath(key, byNull())
|
||||
}
|
||||
else if (rule instanceof RegexMatcher) {
|
||||
jsonPath(key, byRegex(rule.regex))
|
||||
}
|
||||
else if (rule instanceof DateMatcher) {
|
||||
jsonPath(key, byDate())
|
||||
}
|
||||
else if (rule instanceof TimeMatcher) {
|
||||
jsonPath(key, byTime())
|
||||
}
|
||||
else if (rule instanceof TimestampMatcher) {
|
||||
jsonPath(key, byTimestamp())
|
||||
}
|
||||
else if (rule instanceof MinTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
minOccurrence((rule as MinTypeMatcher).min)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof MinMaxTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
minOccurrence((rule as MinMaxTypeMatcher).min)
|
||||
maxOccurrence((rule as MinMaxTypeMatcher).max)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof MaxTypeMatcher) {
|
||||
jsonPath(key, byType() {
|
||||
maxOccurrence((rule as MaxTypeMatcher).max)
|
||||
})
|
||||
}
|
||||
else if (rule instanceof TypeMatcher) {
|
||||
jsonPath(key, byType())
|
||||
}
|
||||
else if (rule instanceof NumberTypeMatcher) {
|
||||
switch (rule.numberType) {
|
||||
case NumberTypeMatcher.NumberType.NUMBER:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.
|
||||
number()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.INTEGER:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.
|
||||
anInteger()))
|
||||
break
|
||||
case NumberTypeMatcher.NumberType.DECIMAL:
|
||||
jsonPath(key,
|
||||
byRegex(RegexPatterns.
|
||||
aDouble()))
|
||||
break
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported number type!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.headers) {
|
||||
Category headerRules = response.matchingRules.
|
||||
rulesForCategory('header')
|
||||
headers {
|
||||
response.headers.forEach({ String k, List<String> v ->
|
||||
if (k.equalsIgnoreCase("Cookie")) {
|
||||
return
|
||||
}
|
||||
if (headerRules.matchingRules.containsKey(k)) {
|
||||
MatchingRuleGroup ruleGroup = headerRules.matchingRules.
|
||||
get(k)
|
||||
if (ruleGroup.rules.size() > 1) {
|
||||
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
|
||||
}
|
||||
MatchingRule rule = ruleGroup.rules[0]
|
||||
if (rule instanceof RegexMatcher) {
|
||||
v.each({
|
||||
header(k, $(p(regex(Pattern.compile(
|
||||
((RegexMatcher) rule).getRegex()))),
|
||||
c(it)))
|
||||
})
|
||||
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
|
||||
}
|
||||
}
|
||||
else {
|
||||
v.each({
|
||||
header(k, it)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (response.headers.containsKey("Cookie")) {
|
||||
Category headerRules = response.matchingRules.
|
||||
rulesForCategory('header')
|
||||
String[] splitHeader = response.headers.get("Cookie").first().split(";")
|
||||
Map<String, String> foundCookies = splitHeader.collectEntries {
|
||||
String[] keyValue = it.split("=")
|
||||
return [(keyValue[0]): keyValue[1]]
|
||||
}
|
||||
cookies {
|
||||
foundCookies.each { k, v ->
|
||||
if (headerRules.matchingRules.containsKey("Cookie")) {
|
||||
MatchingRuleGroup ruleGroup = headerRules.matchingRules.
|
||||
get("Cookie")
|
||||
if (ruleGroup.rules.size() > 1) {
|
||||
throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported")
|
||||
}
|
||||
MatchingRule rule = ruleGroup.rules[0]
|
||||
if (rule instanceof RegexMatcher) {
|
||||
v.each({
|
||||
cookie(k, $(p(regex(Pattern.compile(
|
||||
((RegexMatcher) rule).getRegex()))),
|
||||
c(it)))
|
||||
})
|
||||
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
|
||||
}
|
||||
}
|
||||
else {
|
||||
cookie(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildDescription(RequestResponseInteraction interaction) {
|
||||
String description = "$interaction.description"
|
||||
interaction.providerStates.forEach({ ProviderState it ->
|
||||
description += " $it.name"
|
||||
if (!it.params.isEmpty()) {
|
||||
Map<String, Object> params = it.params
|
||||
description += "("
|
||||
params.forEach({ String k, Object v ->
|
||||
description += k + ": " + v.toString()
|
||||
if (params.keySet().last() != k) {
|
||||
description += ", "
|
||||
}
|
||||
})
|
||||
description += ")"
|
||||
}
|
||||
})
|
||||
return description
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import au.com.dius.pact.core.model.generators.Category
|
||||
import au.com.dius.pact.core.model.generators.DateGenerator
|
||||
import au.com.dius.pact.core.model.generators.DateTimeGenerator
|
||||
import au.com.dius.pact.core.model.generators.Generator
|
||||
import au.com.dius.pact.core.model.generators.Generators
|
||||
import au.com.dius.pact.core.model.generators.RandomBooleanGenerator
|
||||
import au.com.dius.pact.core.model.generators.RandomDecimalGenerator
|
||||
import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator
|
||||
import au.com.dius.pact.core.model.generators.RandomIntGenerator
|
||||
import au.com.dius.pact.core.model.generators.RandomStringGenerator
|
||||
import au.com.dius.pact.core.model.generators.RegexGenerator
|
||||
import au.com.dius.pact.core.model.generators.TimeGenerator
|
||||
import au.com.dius.pact.core.model.generators.UuidGenerator
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.Body
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils
|
||||
|
||||
/**
|
||||
* @author Tim Ysewyn
|
||||
* @Since 2.0.0
|
||||
*/
|
||||
@CompileStatic
|
||||
@PackageScope
|
||||
class ValueGeneratorConverter {
|
||||
|
||||
private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN)
|
||||
private static final String INTEGER_PATTERN = '-?(\\d+)'
|
||||
private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN)
|
||||
private static final String DECIMAL_PATTERN = '-?(\\d*\\.\\d+)'
|
||||
private static final Pattern HEX = Pattern.compile(HEX_PATTERN)
|
||||
private static final String HEX_PATTERN = '[a-fA-F0-9]+'
|
||||
private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN)
|
||||
private static final String ALPHA_NUMERIC_PATTERN = '[a-zA-Z0-9]+'
|
||||
private static final Pattern UUID = Pattern.compile(UUID_PATTERN)
|
||||
private static final String UUID_PATTERN = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
|
||||
private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN)
|
||||
private static final String ANY_DATE_PATTERN = '(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])'
|
||||
private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN)
|
||||
private static final String ANY_TIME_PATTERN = '(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'
|
||||
private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN)
|
||||
private static final String ANY_DATE_TIME_PATTERN = '([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'
|
||||
private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN)
|
||||
private static final String TRUE_OR_FALSE_PATTERN = /(true|false)/
|
||||
|
||||
static DslProperty convert(Generator generator, Closure<DslProperty> dslPropertyProvider) {
|
||||
Pattern pattern
|
||||
if (generator instanceof RandomIntGenerator) {
|
||||
pattern = INTEGER
|
||||
}
|
||||
else if (generator instanceof RandomDecimalGenerator) {
|
||||
pattern = DECIMAL
|
||||
}
|
||||
else if (generator instanceof RandomHexadecimalGenerator) {
|
||||
pattern = HEX
|
||||
}
|
||||
else if (generator instanceof RandomStringGenerator) {
|
||||
pattern = ALPHA_NUMERIC
|
||||
}
|
||||
else if (generator instanceof RegexGenerator) {
|
||||
pattern = Pattern.compile(generator.regex)
|
||||
}
|
||||
else if (generator instanceof UuidGenerator) {
|
||||
pattern = UUID
|
||||
}
|
||||
else if (generator instanceof DateGenerator) {
|
||||
pattern = getDateTimePattern(generator.format, ANY_DATE)
|
||||
}
|
||||
else if (generator instanceof TimeGenerator) {
|
||||
pattern = getDateTimePattern(generator.format, ANY_TIME)
|
||||
}
|
||||
else if (generator instanceof DateTimeGenerator) {
|
||||
pattern = getDateTimePattern(generator.format, ANY_DATE_TIME)
|
||||
}
|
||||
else if (generator instanceof RandomBooleanGenerator) {
|
||||
pattern = TRUE_OR_FALSE
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw new UnsupportedOperationException("We currently don't support a generator of type " + generator.class.simpleName)
|
||||
}
|
||||
else {
|
||||
Object generatedValue = generator.generate([:])
|
||||
return dslPropertyProvider(pattern, generatedValue)
|
||||
}
|
||||
}
|
||||
|
||||
private static Pattern getDateTimePattern(String format, Pattern defaultPattern) {
|
||||
return format ? Pattern.compile(format) : defaultPattern
|
||||
}
|
||||
|
||||
static Generators extract(Body body, Closure dslPropertyValueProvider) {
|
||||
Generators generators = new Generators()
|
||||
traverse(body, dslPropertyValueProvider, '', generators, Category.BODY)
|
||||
return generators
|
||||
}
|
||||
|
||||
static Generators extract(OutputMessage message, Closure dslPropertyValueProvider) {
|
||||
Generators generators = new Generators()
|
||||
traverse(message.body, dslPropertyValueProvider, '', generators, Category.BODY)
|
||||
return generators
|
||||
}
|
||||
|
||||
private static void traverse(Object value, Closure dslPropertyValueProvider, String path, Generators generators, Category category) {
|
||||
Object v = value
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueProvider(v)
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue(v, dslPropertyValueProvider)
|
||||
}
|
||||
if (v instanceof Map) {
|
||||
v.each { Map.Entry entry ->
|
||||
traverse(entry.value, dslPropertyValueProvider, path + "." + entry.key, generators, category)
|
||||
}
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
v.eachWithIndex { def entry, int index ->
|
||||
traverse(entry, dslPropertyValueProvider, path + "[" + index + "]", generators, category)
|
||||
}
|
||||
}
|
||||
else if (v instanceof DslProperty) {
|
||||
traverse(v, dslPropertyValueProvider, path, generators, category)
|
||||
}
|
||||
else if (v instanceof RegexProperty || v instanceof Pattern) {
|
||||
RegexProperty regexProperty = new RegexProperty(v)
|
||||
switch (regexProperty.pattern()) {
|
||||
case INTEGER_PATTERN:
|
||||
generators.
|
||||
addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE))
|
||||
break
|
||||
case DECIMAL_PATTERN:
|
||||
generators.
|
||||
addGenerator(category, path, new RandomDecimalGenerator(10))
|
||||
break
|
||||
case HEX_PATTERN:
|
||||
generators.
|
||||
addGenerator(category, path, new RandomHexadecimalGenerator(10))
|
||||
break
|
||||
case ALPHA_NUMERIC_PATTERN:
|
||||
generators.addGenerator(category, path, new RandomStringGenerator(10))
|
||||
break
|
||||
case UUID_PATTERN:
|
||||
generators.addGenerator(category, path, UuidGenerator.INSTANCE)
|
||||
break
|
||||
case ANY_DATE_PATTERN:
|
||||
generators.addGenerator(category, path, new DateGenerator())
|
||||
break
|
||||
case ANY_TIME_PATTERN:
|
||||
generators.addGenerator(category, path, new TimeGenerator())
|
||||
break
|
||||
case ANY_DATE_TIME_PATTERN:
|
||||
generators.addGenerator(category, path, new DateTimeGenerator())
|
||||
break
|
||||
case TRUE_OR_FALSE_PATTERN:
|
||||
generators.
|
||||
addGenerator(category, path, RandomBooleanGenerator.INSTANCE)
|
||||
break
|
||||
default:
|
||||
generators.
|
||||
addGenerator(category, path, new RegexGenerator(regexProperty.
|
||||
pattern()))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import au.com.dius.pact.consumer.dsl.DslPart;
|
||||
import au.com.dius.pact.consumer.dsl.PactDslJsonArray;
|
||||
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
|
||||
import au.com.dius.pact.core.model.OptionalBody;
|
||||
import au.com.dius.pact.core.model.Request;
|
||||
import au.com.dius.pact.core.model.Response;
|
||||
import au.com.dius.pact.core.model.generators.Generator;
|
||||
import au.com.dius.pact.core.model.messaging.Message;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.internal.EvaluationContext;
|
||||
import com.jayway.jsonpath.internal.Path;
|
||||
import com.jayway.jsonpath.internal.path.PathCompiler;
|
||||
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
|
||||
import groovy.lang.GString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.ServerDslProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
|
||||
/**
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
final class BodyConverter {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper();
|
||||
|
||||
private BodyConverter() {
|
||||
|
||||
}
|
||||
|
||||
static DslPart toPactBody(DslProperty<?> dslProperty, Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
|
||||
return traverse(dslProperty, null, dslPropertyValueExtractor);
|
||||
}
|
||||
|
||||
private static DslPart traverse(Object value, DslPart parent,
|
||||
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
|
||||
boolean isRoot = parent == null;
|
||||
Object v = value;
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
|
||||
}
|
||||
if (v instanceof String) {
|
||||
String stringValue = ((String) v).trim();
|
||||
if (StringUtils.startsWith(stringValue, "{") && StringUtils.endsWith(stringValue, "}")) {
|
||||
try {
|
||||
v = OBJECT_MAPPER.readValue(stringValue, Object.class);
|
||||
}
|
||||
catch (JsonProcessingException ex) { /*
|
||||
* it wasn't a JSON string after
|
||||
* all...
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
DslPart p = isRoot ? createRootDslPart(v) : parent;
|
||||
if (v instanceof Map) {
|
||||
processMap((Map) v, (PactDslJsonBody) p, dslPropertyValueExtractor);
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
processCollection((Collection) v, (PactDslJsonArray) p, dslPropertyValueExtractor);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private static DslPart createRootDslPart(Object value) {
|
||||
return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody();
|
||||
}
|
||||
|
||||
private static void processCollection(Collection values, PactDslJsonArray jsonArray,
|
||||
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
|
||||
values.forEach(v -> {
|
||||
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
|
||||
}
|
||||
if (v == null) {
|
||||
jsonArray.nullValue();
|
||||
}
|
||||
else if (v instanceof String) {
|
||||
jsonArray.string((String) v);
|
||||
}
|
||||
else if (v instanceof Number) {
|
||||
jsonArray.number((Number) v);
|
||||
}
|
||||
else if (v instanceof Map) {
|
||||
PactDslJsonBody current = jsonArray.object();
|
||||
traverse(v, current, dslPropertyValueExtractor);
|
||||
current.closeObject();
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
PactDslJsonArray current = jsonArray.array();
|
||||
traverse(v, current, dslPropertyValueExtractor);
|
||||
current.closeArray();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void processMap(Map<String, Object> values, PactDslJsonBody jsonObject,
|
||||
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
|
||||
values.forEach((k, v) -> {
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
|
||||
}
|
||||
if (v == null) {
|
||||
jsonObject.nullValue(k);
|
||||
}
|
||||
else if (v instanceof String) {
|
||||
jsonObject.stringType(k, (String) v);
|
||||
}
|
||||
else if (v instanceof Number) {
|
||||
jsonObject.numberValue(k, (Number) v);
|
||||
}
|
||||
else if (v instanceof Map) {
|
||||
PactDslJsonBody current = jsonObject.object(k);
|
||||
traverse(v, current, dslPropertyValueExtractor);
|
||||
current.closeObject();
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
PactDslJsonArray current = jsonObject.array(k);
|
||||
traverse(v, current, dslPropertyValueExtractor);
|
||||
current.closeArray();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Object toSCCBody(Request request) {
|
||||
Object body = parseBody(request.getBody());
|
||||
if (request.getGenerators().isNotEmpty() && request.getGenerators().getCategories()
|
||||
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body,
|
||||
request.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
|
||||
currentValue -> pattern -> generatedValue -> new DslProperty<>(
|
||||
new ClientDslProperty(pattern, generatedValue), currentValue));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static Object toSCCBody(Response response) {
|
||||
Object body = parseBody(response.getBody());
|
||||
if (response.getGenerators().isNotEmpty() && response.getGenerators().getCategories()
|
||||
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body,
|
||||
response.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
|
||||
currentValue -> pattern -> generatedValue -> new DslProperty<>(currentValue,
|
||||
new ServerDslProperty(pattern, generatedValue)));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static Object toSCCBody(Message message) {
|
||||
Object body = parseBody(message.getContents());
|
||||
if (message.getGenerators().isNotEmpty() && message.getGenerators().getCategories()
|
||||
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
|
||||
applyGenerators(body,
|
||||
message.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
|
||||
currentValue -> pattern -> generatedValue -> new DslProperty<>(
|
||||
new ClientDslProperty(pattern, generatedValue), currentValue));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private static Object parseBody(OptionalBody optionalBody) {
|
||||
if (optionalBody.isPresent()) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(optionalBody.getValue(), Object.class);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Body could not be read", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return optionalBody.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyGenerators(Object body, Map<String, Generator> generatorsPerPath,
|
||||
Function<Pattern, Function<Object, Function<Object, DslProperty<Object>>>> dslPropertyProvider) {
|
||||
Configuration configuration = Configuration.builder().jsonProvider(new JacksonJsonProvider(OBJECT_MAPPER))
|
||||
.build();
|
||||
generatorsPerPath.forEach((path, generator) -> {
|
||||
Path compiledPath = PathCompiler.compile(path);
|
||||
EvaluationContext evaluationContext = compiledPath.evaluate(body, body, configuration, true);
|
||||
evaluationContext.updateOperations().forEach(pathRef -> {
|
||||
pathRef.convert(((currentValue, config) -> ValueGeneratorConverter.convert(generator,
|
||||
(pattern, generatedValue) -> dslPropertyProvider.apply(pattern).apply(generatedValue)
|
||||
.apply(currentValue))),
|
||||
configuration);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import au.com.dius.pact.core.model.matchingrules.Category;
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.EqualsMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.MatchingType;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
|
||||
|
||||
/**
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
final class MatchingRulesConverter {
|
||||
|
||||
private MatchingRulesConverter() {
|
||||
}
|
||||
|
||||
static Category matchingRulesForBody(BodyMatchers bodyMatchers) {
|
||||
return matchingRulesFor("body", bodyMatchers);
|
||||
}
|
||||
|
||||
private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) {
|
||||
Category category = new Category(categoryName);
|
||||
bodyMatchers.matchers().forEach((b) -> {
|
||||
String key = getMatcherKey(b.path());
|
||||
MatchingType matchingType = b.matchingType();
|
||||
switch (matchingType) {
|
||||
case NULL:
|
||||
category.addRule(key, NullMatcher.INSTANCE);
|
||||
break;
|
||||
case EQUALITY:
|
||||
category.addRule(key, EqualsMatcher.INSTANCE);
|
||||
break;
|
||||
case TYPE:
|
||||
if (b.minTypeOccurrence() != null && b.maxTypeOccurrence() != null) {
|
||||
category.addRule(key, new MinMaxTypeMatcher(b.minTypeOccurrence(), b.maxTypeOccurrence()));
|
||||
}
|
||||
else if (b.minTypeOccurrence() != null) {
|
||||
category.addRule(key, new MinTypeMatcher(b.minTypeOccurrence()));
|
||||
}
|
||||
else if (b.maxTypeOccurrence() != null) {
|
||||
category.addRule(key, new MaxTypeMatcher(b.maxTypeOccurrence()));
|
||||
}
|
||||
else {
|
||||
category.addRule(key, TypeMatcher.INSTANCE);
|
||||
}
|
||||
break;
|
||||
case DATE:
|
||||
category.addRule(key, new DateMatcher());
|
||||
break;
|
||||
case TIME:
|
||||
category.addRule(key, new TimeMatcher());
|
||||
break;
|
||||
case TIMESTAMP:
|
||||
category.addRule(key, new TimestampMatcher());
|
||||
break;
|
||||
case REGEX:
|
||||
String pattern = b.value().toString();
|
||||
if (pattern.equals(RegexPatterns.number().pattern())) {
|
||||
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER));
|
||||
}
|
||||
else if (pattern.equals(RegexPatterns.anInteger().pattern())) {
|
||||
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER));
|
||||
}
|
||||
else if (pattern.equals(RegexPatterns.aDouble().pattern())) {
|
||||
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL));
|
||||
}
|
||||
else {
|
||||
category.addRule(key, new RegexMatcher(pattern));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return category;
|
||||
}
|
||||
|
||||
private static String getMatcherKey(String path) {
|
||||
return path.startsWith("$") ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import au.com.dius.pact.consumer.MessagePactBuilder;
|
||||
import au.com.dius.pact.consumer.dsl.DslPart;
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact;
|
||||
import groovy.lang.GString;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.Headers;
|
||||
import org.springframework.cloud.contract.spec.internal.Input;
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
|
||||
/**
|
||||
* Creator of {@link MessagePact} instances.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class MessagePactCreator {
|
||||
|
||||
private static final Function<DslProperty<?>, Object> clientValueExtractor = DslProperty::getClientValue;
|
||||
|
||||
MessagePact createFromContract(List<Contract> contracts) {
|
||||
if (CollectionUtils.isEmpty(contracts)) {
|
||||
return null;
|
||||
}
|
||||
Names names = NamingUtil.name(contracts.get(0));
|
||||
MessagePactBuilder pactBuilder = MessagePactBuilder.consumer(names.getConsumer())
|
||||
.hasPactWith(names.getProducer());
|
||||
|
||||
for (Contract contract : contracts) {
|
||||
pactBuilder = pactBuilder.given(getGiven(contract.getInput())).expectsToReceive(getOutcome(contract));
|
||||
if (contract.getOutputMessage() != null) {
|
||||
OutputMessage message = contract.getOutputMessage();
|
||||
if (message.getBody() != null) {
|
||||
DslPart pactResponseBody = BodyConverter.toPactBody(message.getBody(), clientValueExtractor);
|
||||
if (message.getBodyMatchers() != null) {
|
||||
pactResponseBody
|
||||
.setMatchers(MatchingRulesConverter.matchingRulesForBody(message.getBodyMatchers()));
|
||||
}
|
||||
pactResponseBody
|
||||
.setGenerators(ValueGeneratorConverter.extract(message, DslProperty::getServerValue));
|
||||
pactBuilder = pactBuilder.withContent(pactResponseBody);
|
||||
}
|
||||
if (message.getHeaders() != null) {
|
||||
pactBuilder = pactBuilder.withMetadata(getMetadata(message.getHeaders()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return pactBuilder.toPact();
|
||||
}
|
||||
|
||||
private String getGiven(Input input) {
|
||||
if (input.getTriggeredBy() != null) {
|
||||
return input.getTriggeredBy().getExecutionCommand();
|
||||
}
|
||||
else if (input.getMessageFrom() != null) {
|
||||
return "received message from " + clientValueExtractor.apply(input.getMessageFrom());
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutcome(Contract contract) {
|
||||
if (contract.getOutputMessage() != null) {
|
||||
OutputMessage message = contract.getOutputMessage();
|
||||
return "message sent to " + clientValueExtractor.apply(message.getSentTo());
|
||||
}
|
||||
else {
|
||||
return "assert that " + contract.getInput().getAssertThat().getExecutionCommand();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> getMetadata(Headers headers) {
|
||||
return headers.getEntries().stream().collect(Collectors.toMap(Header::getName, this::extractValue));
|
||||
}
|
||||
|
||||
private String extractValue(Object value) {
|
||||
Object v = value;
|
||||
if (v instanceof DslProperty) {
|
||||
v = clientValueExtractor.apply((DslProperty) v);
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue((GString) v, clientValueExtractor);
|
||||
}
|
||||
if (v instanceof String) {
|
||||
return (String) v;
|
||||
}
|
||||
else {
|
||||
return v.toString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import au.com.dius.pact.core.model.matchingrules.Category;
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRule;
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.RuleLogic;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
|
||||
import au.com.dius.pact.core.model.messaging.Message;
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.collections4.MapUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.Headers;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
|
||||
import org.springframework.cloud.contract.spec.internal.ResponseBodyMatchers;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
|
||||
/**
|
||||
* Creator of {@link Contract} instances.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class MessagingSCContractCreator {
|
||||
|
||||
private static final String FULL_BODY = "$";
|
||||
|
||||
private static final String DESTINATION_KEY = "sentTo";
|
||||
|
||||
private static final List<String> NON_HEADER_META_DATA = Collections.singletonList(DESTINATION_KEY);
|
||||
|
||||
Collection<Contract> convertFrom(MessagePact pact) {
|
||||
return pact.getMessages().stream().map(message -> Contract.make(contract -> {
|
||||
contract.label(message.getDescription());
|
||||
if (CollectionUtils.isNotEmpty(message.getProviderStates())) {
|
||||
contract.input(i -> i.triggeredBy(this.getTriggeredBy(message)));
|
||||
}
|
||||
|
||||
contract.outputMessage((outputMessage) -> {
|
||||
if (message.getContents().isPresent()) {
|
||||
outputMessage.body(BodyConverter.toSCCBody(message));
|
||||
Category bodyRules = message.getMatchingRules().rulesForCategory("body");
|
||||
if (bodyRules != null && MapUtils.isNotEmpty(bodyRules.getMatchingRules())) {
|
||||
outputMessage.bodyMatchers((responseBodyMatchers) -> outputMessageBodyMatchers(message,
|
||||
bodyRules, responseBodyMatchers));
|
||||
}
|
||||
}
|
||||
if (MapUtils.isNotEmpty(message.getMetaData())) {
|
||||
outputMessage.headers((headers) -> outputMessageHeaders(message, headers));
|
||||
}
|
||||
String dest = findDestination(message);
|
||||
if (StringUtils.isNotBlank(dest)) {
|
||||
outputMessage.sentTo(dest);
|
||||
}
|
||||
});
|
||||
})).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void outputMessageHeaders(Message message, Headers headers) {
|
||||
message.getMetaData().forEach((key, value) -> {
|
||||
String matchingRuleGroup = value.toString();
|
||||
if (key.equalsIgnoreCase("contentType")) {
|
||||
headers.messagingContentType(matchingRuleGroup);
|
||||
}
|
||||
else if (!NON_HEADER_META_DATA.contains(key)) {
|
||||
headers.header(key, matchingRuleGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void outputMessageBodyMatchers(Message message, Category bodyRules,
|
||||
ResponseBodyMatchers responseBodyMatchers) {
|
||||
bodyRules.getMatchingRules().forEach((matchingRuleKey, matchingRuleGroup) -> {
|
||||
if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported");
|
||||
}
|
||||
if (FULL_BODY.equals(matchingRuleKey)) {
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.getContents().getValue());
|
||||
jsonPaths.forEach((j) -> {
|
||||
responseBodyMatchers.jsonPath(j.keyBeforeChecking(), responseBodyMatchers.byType());
|
||||
});
|
||||
}
|
||||
else {
|
||||
matchingRuleGroup.getRules().forEach((rule) -> {
|
||||
applyJsonPathToResponseBodyMatchers(rule, matchingRuleKey, responseBodyMatchers);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getTriggeredBy(Message message) {
|
||||
String triggeredBy = message.getProviderStates().get(0).getName().replaceAll(":", " ").replaceAll(" ", "_")
|
||||
.replaceAll("\\(", "").replaceAll("\\)", "");
|
||||
return StringUtils.uncapitalize(triggeredBy) + "()";
|
||||
}
|
||||
|
||||
private String findDestination(Message message) {
|
||||
return message.getMetaData().get(DESTINATION_KEY) != null
|
||||
? message.getMetaData().get(DESTINATION_KEY).toString() : "";
|
||||
}
|
||||
|
||||
void applyJsonPathToResponseBodyMatchers(MatchingRule matchingRule, String key,
|
||||
ResponseBodyMatchers responseBodyMatchers) {
|
||||
if (matchingRule instanceof NullMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byNull());
|
||||
}
|
||||
else if (matchingRule instanceof RegexMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
|
||||
}
|
||||
else if (matchingRule instanceof DateMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byDate());
|
||||
}
|
||||
else if (matchingRule instanceof TimeMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTime());
|
||||
}
|
||||
else if (matchingRule instanceof TimestampMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTimestamp());
|
||||
}
|
||||
else if (matchingRule instanceof MinTypeMatcher) {
|
||||
responseBodyMatchers.jsonPath(key,
|
||||
responseBodyMatchers.byType((b) -> b.minOccurrence(((MinTypeMatcher) matchingRule).getMin())));
|
||||
}
|
||||
else if (matchingRule instanceof MinMaxTypeMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> {
|
||||
c.minOccurrence(((MinMaxTypeMatcher) matchingRule).getMin());
|
||||
c.maxOccurrence(((MinMaxTypeMatcher) matchingRule).getMax());
|
||||
}));
|
||||
}
|
||||
else if (matchingRule instanceof MaxTypeMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> {
|
||||
c.maxOccurrence(((MaxTypeMatcher) matchingRule).getMax());
|
||||
}));
|
||||
}
|
||||
else if (matchingRule instanceof TypeMatcher) {
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType());
|
||||
}
|
||||
else if (matchingRule instanceof NumberTypeMatcher) {
|
||||
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
|
||||
case NUMBER:
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.number()));
|
||||
break;
|
||||
case INTEGER:
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.anInteger()));
|
||||
break;
|
||||
case DECIMAL:
|
||||
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.aDouble()));
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported number type!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Stessy Delcroix
|
||||
* @since
|
||||
*/
|
||||
class Names {
|
||||
|
||||
private final String consumer;
|
||||
|
||||
private final String producer;
|
||||
|
||||
private final String test;
|
||||
|
||||
Names(String[] strings) {
|
||||
this.consumer = strings[0];
|
||||
this.producer = strings[1];
|
||||
this.test = strings.length >= 2 ? strings[2] : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.consumer + "_" + this.producer;
|
||||
}
|
||||
|
||||
String getConsumer() {
|
||||
return consumer;
|
||||
}
|
||||
|
||||
String getProducer() {
|
||||
return producer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Stessy Delcroix
|
||||
* @since
|
||||
*/
|
||||
final class NamingUtil {
|
||||
|
||||
// consumer___producer___testname
|
||||
private static final String SEPARATOR = "___";
|
||||
|
||||
private NamingUtil() {
|
||||
}
|
||||
|
||||
static Names name(Contract contract) {
|
||||
String contractName = contract.getName();
|
||||
if (contractName == null || !contractName.contains(SEPARATOR)) {
|
||||
return new Names(new String[] { "Consumer", "Provider", "" });
|
||||
}
|
||||
return new Names(contractName.split(SEPARATOR));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* {@link ObjectMapper} singleton instance creation.
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum ObjectMapperFactory {
|
||||
|
||||
/**
|
||||
* Singleton instance constant.
|
||||
*/
|
||||
INSTANCE;
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
static {
|
||||
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
|
||||
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true);
|
||||
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
|
||||
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
|
||||
OBJECT_MAPPER.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
|
||||
}
|
||||
|
||||
public ObjectMapper getMapper() {
|
||||
return OBJECT_MAPPER;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import au.com.dius.pact.core.model.DefaultPactReader;
|
||||
import au.com.dius.pact.core.model.Pact;
|
||||
import au.com.dius.pact.core.model.PactSpecVersion;
|
||||
import au.com.dius.pact.core.model.RequestResponsePact;
|
||||
import au.com.dius.pact.core.model.messaging.MessagePact;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.util.DefaultIndenter;
|
||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||
import com.fasterxml.jackson.core.util.Separators;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.ContractConverter;
|
||||
|
||||
/**
|
||||
* Converter of JSON PACT file.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class PactContractConverter implements ContractConverter<Collection<Pact<?>>> {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper();
|
||||
|
||||
private final RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator();
|
||||
|
||||
private final MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator();
|
||||
|
||||
private final RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator();
|
||||
|
||||
private final MessagePactCreator messagePactCreator = new MessagePactCreator();
|
||||
|
||||
@Override
|
||||
public boolean isAccepted(File file) {
|
||||
try {
|
||||
DefaultPactReader.INSTANCE.loadPact(file);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Contract> convertFrom(File file) {
|
||||
Pact<?> pact = DefaultPactReader.INSTANCE.loadPact(file);
|
||||
if (pact instanceof RequestResponsePact) {
|
||||
return requestResponseSCContractCreator.convertFrom((RequestResponsePact) pact);
|
||||
}
|
||||
if (pact instanceof MessagePact) {
|
||||
return messagingSCContractCreator.convertFrom((MessagePact) pact);
|
||||
}
|
||||
throw new UnsupportedOperationException(
|
||||
"We currently don't support pact contracts of type" + pact.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pact<?>> convertTo(Collection<Contract> contracts) {
|
||||
List<Pact<?>> pactContracts = new ArrayList<>();
|
||||
Map<String, List<Contract>> groupedContracts = contracts.stream()
|
||||
.collect(Collectors.groupingBy(c -> NamingUtil.name(c).toString()));
|
||||
for (List<Contract> list : groupedContracts.values()) {
|
||||
List<Contract> httpOnly = list.stream().filter(c -> c.getRequest() != null).collect(Collectors.toList());
|
||||
List<Contract> messagingOnly = list.stream().filter(c -> c.getInput() != null).collect(Collectors.toList());
|
||||
RequestResponsePact responsePact = requestResponsePactCreator.createFromContract(httpOnly);
|
||||
if (responsePact != null) {
|
||||
pactContracts.add(responsePact);
|
||||
}
|
||||
MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly);
|
||||
if (messagePact != null) {
|
||||
pactContracts.add(messagePact);
|
||||
}
|
||||
}
|
||||
return pactContracts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, byte[]> store(Collection<Pact<?>> contracts) {
|
||||
return contracts.stream().collect(Collectors.toMap(this::name, c -> {
|
||||
try {
|
||||
return this.buildPrettyPrint(OBJECT_MAPPER.writeValueAsString(c.toMap(PactSpecVersion.V3))).getBytes();
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("The pact contract is not a valid map", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected String name(Pact<?> contract) {
|
||||
return contract.getConsumer().getName() + "_" + contract.getProvider().getName() + "_"
|
||||
+ Math.abs(contract.hashCode()) + ".json";
|
||||
}
|
||||
|
||||
private String buildPrettyPrint(String contract) {
|
||||
try {
|
||||
Object intermediateObjectForPrettyPrinting = OBJECT_MAPPER.reader().readValue(contract, Object.class);
|
||||
DefaultIndenter customIndenter = new DefaultIndenter(" ", "\n");
|
||||
return OBJECT_MAPPER
|
||||
.writer(new CustomPrettyPrinter().withArrayIndenter(customIndenter)
|
||||
.withObjectIndenter(customIndenter))
|
||||
.writeValueAsString(intermediateObjectForPrettyPrinting);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("WireMock response body could not be pretty printed");
|
||||
}
|
||||
}
|
||||
|
||||
private static class CustomPrettyPrinter extends DefaultPrettyPrinter {
|
||||
|
||||
@Override
|
||||
public CustomPrettyPrinter createInstance() {
|
||||
return new CustomPrettyPrinter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultPrettyPrinter withSeparators(Separators separators) {
|
||||
_separators = separators;
|
||||
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import au.com.dius.pact.consumer.ConsumerPactBuilder;
|
||||
import au.com.dius.pact.consumer.dsl.DslPart;
|
||||
import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath;
|
||||
import au.com.dius.pact.consumer.dsl.PactDslResponse;
|
||||
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
|
||||
import au.com.dius.pact.core.model.RequestResponsePact;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.Body;
|
||||
import org.springframework.cloud.contract.spec.internal.Cookies;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.ExecutionProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.QueryParameters;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Request;
|
||||
import org.springframework.cloud.contract.spec.internal.Response;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
/**
|
||||
* Creator of {@link RequestResponsePact} instances.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class RequestResponsePactCreator {
|
||||
|
||||
RequestResponsePact createFromContract(List<Contract> contracts) {
|
||||
if (CollectionUtils.isEmpty(contracts)) {
|
||||
return null;
|
||||
}
|
||||
Names names = NamingUtil.name(contracts.get(0));
|
||||
PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer(names.getConsumer())
|
||||
.hasPactWith(names.getProducer());
|
||||
PactDslResponse pactDslResponse = null;
|
||||
for (Contract contract : contracts) {
|
||||
assertNoExecutionProperty(contract);
|
||||
PactDslRequestWithPath pactDslRequest = pactDslResponse != null
|
||||
? createPactDslRequestWithPath(contract, pactDslResponse)
|
||||
: createPactDslRequestWithPath(contract, pactDslWithProvider);
|
||||
pactDslResponse = createPactDslResponse(contract, pactDslRequest);
|
||||
}
|
||||
return pactDslResponse.toPact();
|
||||
}
|
||||
|
||||
private void assertNoExecutionProperty(Contract contract) {
|
||||
assertNoExecutionPropertyInBody(contract.getRequest().getBody(), DslProperty::getServerValue);
|
||||
assertNoExecutionPropertyInBody(contract.getResponse().getBody(), DslProperty::getClientValue);
|
||||
}
|
||||
|
||||
private void assertNoExecutionPropertyInBody(Body body,
|
||||
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
|
||||
traverseValues(body, dslPropertyValueExtractor, (Object object) -> {
|
||||
if (object instanceof ExecutionProperty) {
|
||||
throw new UnsupportedOperationException("We can't convert a contract that has execution property");
|
||||
}
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
private void traverseValues(Object value, Function<DslProperty<?>, Object> dslPropertyValueExtractor,
|
||||
Function<Object, Object> function) {
|
||||
if (value instanceof DslProperty) {
|
||||
traverseValues(dslPropertyValueExtractor.apply((DslProperty<?>) value), dslPropertyValueExtractor,
|
||||
function);
|
||||
}
|
||||
else if (value instanceof Map) {
|
||||
((Map) value).values().forEach(v -> traverseValues(v, dslPropertyValueExtractor, function));
|
||||
}
|
||||
else if (value instanceof Collection) {
|
||||
((Collection<?>) value).forEach(v -> traverseValues(v, dslPropertyValueExtractor, function));
|
||||
}
|
||||
else {
|
||||
function.apply(value);
|
||||
}
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) {
|
||||
Request request = contract.getRequest();
|
||||
PactDslRequestWithPath pactDslRequest = pactDslResponse
|
||||
.uponReceiving(StringUtils.isNotBlank(contract.getDescription()) ? contract.getDescription() : "")
|
||||
.path(url(request)).method(request.getMethod().getServerValue().toString());
|
||||
String query = query(request);
|
||||
if (StringUtils.isNotBlank(query)) {
|
||||
pactDslRequest = pactDslRequest.encodedQuery(query);
|
||||
}
|
||||
final PactDslRequestWithPath finalPactDslRequest = pactDslRequest;
|
||||
if (request.getHeaders() != null) {
|
||||
request.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslRequest, h));
|
||||
}
|
||||
if (request.getCookies() != null) {
|
||||
pactDslRequest = processCookies(finalPactDslRequest, request.getCookies());
|
||||
}
|
||||
if (request.getBody() != null) {
|
||||
DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getClientValue);
|
||||
if (request.getBodyMatchers() != null) {
|
||||
pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers()));
|
||||
}
|
||||
pactRequestBody
|
||||
.setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue));
|
||||
pactDslRequest = pactDslRequest.body(pactRequestBody);
|
||||
}
|
||||
return pactDslRequest;
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract,
|
||||
PactDslWithProvider pactDslWithProvider) {
|
||||
Request request = contract.getRequest();
|
||||
PactDslRequestWithPath pactDslRequest = pactDslWithProvider
|
||||
.uponReceiving(StringUtils.isNotBlank(contract.getDescription()) ? contract.getDescription() : "")
|
||||
.path(url(request)).method(request.getMethod().getServerValue().toString());
|
||||
String query = query(request);
|
||||
if (StringUtils.isNotBlank(query)) {
|
||||
pactDslRequest = pactDslRequest.encodedQuery(query);
|
||||
}
|
||||
final PactDslRequestWithPath finalPactDslRequest = pactDslRequest;
|
||||
if (request.getHeaders() != null) {
|
||||
request.getHeaders().getEntries().forEach(h -> {
|
||||
processHeader(finalPactDslRequest, h);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.getBody() != null) {
|
||||
DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getServerValue);
|
||||
if (request.getBodyMatchers() != null) {
|
||||
pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers()));
|
||||
}
|
||||
pactRequestBody
|
||||
.setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue));
|
||||
pactDslRequest = pactDslRequest.body(pactRequestBody);
|
||||
}
|
||||
return pactDslRequest;
|
||||
}
|
||||
|
||||
private String url(Request request) {
|
||||
if (request.getUrlPath() != null) {
|
||||
return request.getUrlPath().getServerValue().toString();
|
||||
}
|
||||
else if (request.getUrl() != null) {
|
||||
return request.getUrl().getServerValue().toString();
|
||||
}
|
||||
throw new IllegalStateException("No url provided");
|
||||
}
|
||||
|
||||
private String query(Request request) {
|
||||
final StringBuilder query = new StringBuilder();
|
||||
QueryParameters params = queryParams(request);
|
||||
if (params != null) {
|
||||
AtomicInteger index = new AtomicInteger();
|
||||
params.getParameters().forEach(p -> {
|
||||
query.append(p.getName()).append('=').append(p.getServerValue());
|
||||
if (index.incrementAndGet() < params.getParameters().size()) {
|
||||
query.append('&');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
private QueryParameters queryParams(Request request) {
|
||||
if (request.getUrlPath() != null) {
|
||||
return request.getUrlPath().getQueryParameters();
|
||||
}
|
||||
else if (request.getUrl() != null) {
|
||||
return request.getUrl().getQueryParameters();
|
||||
}
|
||||
throw new IllegalStateException("No url provided");
|
||||
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) {
|
||||
if (header.isSingleValue()) {
|
||||
String value = getDslPropertyServerValue(header).toString();
|
||||
return pactDslRequest.headers(header.getName(), value);
|
||||
}
|
||||
else {
|
||||
String regex = getDslPropertyClientValue(header).toString();
|
||||
String example = getDslPropertyServerValue(header).toString();
|
||||
return pactDslRequest.matchHeader(header.getName(), regex, example);
|
||||
}
|
||||
}
|
||||
|
||||
private String stubSideCookieExample(Cookies cookies) {
|
||||
return cookies.asStubSideMap().entrySet().stream().map(Object::toString).collect(joining(";"));
|
||||
}
|
||||
|
||||
private Object getDslPropertyClientValue(Object o) {
|
||||
Object value = o;
|
||||
if (value instanceof DslProperty) {
|
||||
value = getDslPropertyClientValue(((DslProperty) value).getClientValue());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private Object getDslPropertyServerValue(Object o) {
|
||||
Object value = o;
|
||||
if (value instanceof DslProperty) {
|
||||
value = getDslPropertyServerValue(((DslProperty) value).getServerValue());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private PactDslRequestWithPath processCookies(PactDslRequestWithPath pactDslRequest, Cookies cookies) {
|
||||
Map<String, Object> stubSideCookies = cookies.asStubSideMap();
|
||||
Collection<RegexProperty> regexProperties = stubSideCookies.values().stream()
|
||||
.filter(r -> r instanceof Pattern || r instanceof RegexProperty).map(RegexProperty::new)
|
||||
.collect(Collectors.toList());
|
||||
if (!regexProperties.isEmpty()) {
|
||||
String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|"));
|
||||
return pactDslRequest.matchHeader("Cookie", regex, testSideCookieExample(cookies));
|
||||
}
|
||||
else {
|
||||
return pactDslRequest.headers("Cookie", testSideCookieExample(cookies));
|
||||
}
|
||||
}
|
||||
|
||||
private String testSideCookieExample(Cookies cookies) {
|
||||
return cookies.asTestSideMap().entrySet().stream().map(Object::toString).collect(joining(";"));
|
||||
}
|
||||
|
||||
private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) {
|
||||
Response response = contract.getResponse();
|
||||
PactDslResponse pactDslResponse = pactDslRequest.willRespondWith()
|
||||
.status((Integer) response.getStatus().getClientValue());
|
||||
|
||||
PactDslResponse finalPactDslResponse = pactDslResponse;
|
||||
if (response.getHeaders() != null) {
|
||||
response.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslResponse, h));
|
||||
}
|
||||
|
||||
if (response.getCookies() != null) {
|
||||
pactDslResponse = processCookies(pactDslResponse, response.getCookies());
|
||||
}
|
||||
if (response.getBody() != null) {
|
||||
DslPart pactResponseBody = BodyConverter.toPactBody(response.getBody(), DslProperty::getClientValue);
|
||||
if (response.getBodyMatchers() != null) {
|
||||
pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(response.getBodyMatchers()));
|
||||
}
|
||||
pactResponseBody
|
||||
.setGenerators(ValueGeneratorConverter.extract(response.getBody(), DslProperty::getServerValue));
|
||||
pactDslResponse = pactDslResponse.body(pactResponseBody);
|
||||
}
|
||||
return pactDslResponse;
|
||||
}
|
||||
|
||||
private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) {
|
||||
if (header.isSingleValue()) {
|
||||
String value = getDslPropertyClientValue(header).toString();
|
||||
return pactDslResponse.headers(Collections.singletonMap(header.getName(), value));
|
||||
}
|
||||
else {
|
||||
String regex = getDslPropertyServerValue(header).toString();
|
||||
String example = getDslPropertyClientValue(header).toString();
|
||||
return pactDslResponse.matchHeader(header.getName(), regex, example);
|
||||
}
|
||||
}
|
||||
|
||||
private PactDslResponse processCookies(PactDslResponse pactDslResponse, Cookies cookies) {
|
||||
Map<String, Object> testSideCookies = cookies.asTestSideMap();
|
||||
Collection<RegexProperty> regexProperties = testSideCookies.values().stream()
|
||||
.filter(p -> p instanceof Pattern || p instanceof RegexProperty).map(RegexProperty::new)
|
||||
.collect(Collectors.toList());
|
||||
if (!regexProperties.isEmpty()) {
|
||||
String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|"));
|
||||
return pactDslResponse.matchHeader("Cookie", regex, stubSideCookieExample(cookies));
|
||||
}
|
||||
else {
|
||||
return pactDslResponse.headers(Collections.singletonMap("Cookie", stubSideCookieExample(cookies)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import au.com.dius.pact.core.model.OptionalBody;
|
||||
import au.com.dius.pact.core.model.ProviderState;
|
||||
import au.com.dius.pact.core.model.Request;
|
||||
import au.com.dius.pact.core.model.RequestResponseInteraction;
|
||||
import au.com.dius.pact.core.model.RequestResponsePact;
|
||||
import au.com.dius.pact.core.model.Response;
|
||||
import au.com.dius.pact.core.model.matchingrules.Category;
|
||||
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRule;
|
||||
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup;
|
||||
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.RuleLogic;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
|
||||
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
|
||||
import org.apache.commons.collections4.MapUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
|
||||
/**
|
||||
* Creator of {@link Contract} instances.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class RequestResponseSCContractCreator {
|
||||
|
||||
private static final String FULL_BODY = "$";
|
||||
|
||||
Collection<Contract> convertFrom(RequestResponsePact pact) {
|
||||
return pact.getInteractions().stream().map(interaction -> Contract.make(contract -> {
|
||||
mapContractDescription(interaction, contract);
|
||||
mapContractRequest(interaction, contract);
|
||||
mapContractResponse(interaction, contract);
|
||||
})).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void mapContractDescription(RequestResponseInteraction interaction, Contract contract) {
|
||||
contract.description(buildDescription(interaction));
|
||||
}
|
||||
|
||||
private void mapContractRequest(RequestResponseInteraction interaction, Contract contract) {
|
||||
contract.request((contractRequest) -> {
|
||||
Request pactRequest = interaction.getRequest();
|
||||
contractRequest.method(pactRequest.getMethod());
|
||||
mapRequestUrl(contractRequest, pactRequest);
|
||||
|
||||
if (MapUtils.isNotEmpty(pactRequest.getHeaders())) {
|
||||
mapRequestHeaders(contractRequest, pactRequest);
|
||||
}
|
||||
if (pactRequest.getHeaders().containsKey("Cookie")) {
|
||||
mapRequestCookies(contractRequest, pactRequest);
|
||||
}
|
||||
if (pactRequest.getBody().getState() == OptionalBody.State.PRESENT) {
|
||||
mapRequestBody(contractRequest, pactRequest);
|
||||
}
|
||||
Category bodyRules = pactRequest.getMatchingRules().rulesForCategory("body");
|
||||
if (MapUtils.isNotEmpty(bodyRules.getMatchingRules())) {
|
||||
mapRequestBodyRules(contractRequest, bodyRules);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void mapContractResponse(RequestResponseInteraction interaction, Contract contract) {
|
||||
contract.response((contractResponse) -> {
|
||||
Response pactResponse = interaction.getResponse();
|
||||
contractResponse.status(pactResponse.getStatus());
|
||||
if (pactResponse.getBody().isPresent()) {
|
||||
mapResponseBody(contractResponse, pactResponse);
|
||||
}
|
||||
|
||||
Category bodyRules = pactResponse.getMatchingRules().rulesForCategory("body");
|
||||
if (MapUtils.isNotEmpty(bodyRules.getMatchingRules())) {
|
||||
mapResponseBodyRules(contractResponse, pactResponse, bodyRules);
|
||||
}
|
||||
if (MapUtils.isNotEmpty(pactResponse.getHeaders())) {
|
||||
mapResponseHeaders(contractResponse, pactResponse);
|
||||
}
|
||||
|
||||
if (pactResponse.getHeaders().containsKey("Cookie")) {
|
||||
mapResponseCookies(contractResponse, pactResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void mapResponseBodyRules(org.springframework.cloud.contract.spec.internal.Response contractResponse,
|
||||
Response pactResponse, Category bodyRules) {
|
||||
contractResponse.bodyMatchers((bodyMatchers) -> {
|
||||
bodyRules.getMatchingRules().forEach((key, matchindRuleGroup) -> {
|
||||
if (matchindRuleGroup.getRuleLogic() != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the AND combination rule logic is supported");
|
||||
}
|
||||
if (FULL_BODY.equals(key)) {
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(
|
||||
new String(pactResponse.getBody().getValue()));
|
||||
jsonPaths.forEach(
|
||||
(jsonPath) -> bodyMatchers.jsonPath(jsonPath.keyBeforeChecking(), bodyMatchers.byType()));
|
||||
}
|
||||
else {
|
||||
matchindRuleGroup.getRules().forEach((matchingRule) -> {
|
||||
if (matchingRule instanceof NullMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byNull());
|
||||
}
|
||||
else if (matchingRule instanceof RegexMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
|
||||
}
|
||||
else if (matchingRule instanceof DateMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byDate());
|
||||
}
|
||||
else if (matchingRule instanceof TimeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byTime());
|
||||
}
|
||||
else if (matchingRule instanceof TimestampMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp());
|
||||
}
|
||||
else if (matchingRule instanceof MinTypeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder
|
||||
.minOccurrence((((MinTypeMatcher) matchingRule).getMin()))));
|
||||
}
|
||||
else if (matchingRule instanceof MinMaxTypeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> {
|
||||
valueHolder.minOccurrence((((MinMaxTypeMatcher) matchingRule).getMin()));
|
||||
valueHolder.maxOccurrence((((MinMaxTypeMatcher) matchingRule).getMax()));
|
||||
}));
|
||||
}
|
||||
else if (matchingRule instanceof MaxTypeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder
|
||||
.maxOccurrence((((MaxTypeMatcher) matchingRule).getMax()))));
|
||||
}
|
||||
else if (matchingRule instanceof TypeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byType());
|
||||
}
|
||||
else if (matchingRule instanceof NumberTypeMatcher) {
|
||||
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
|
||||
case NUMBER:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number()));
|
||||
break;
|
||||
case INTEGER:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger()));
|
||||
break;
|
||||
case DECIMAL:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble()));
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported number type!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void mapResponseBody(org.springframework.cloud.contract.spec.internal.Response contractResponse,
|
||||
Response pactResponse) {
|
||||
Object parsedBody = BodyConverter.toSCCBody(pactResponse);
|
||||
if (parsedBody instanceof Map) {
|
||||
contractResponse.body((Map) parsedBody);
|
||||
}
|
||||
else if (parsedBody instanceof List) {
|
||||
contractResponse.body((List) parsedBody);
|
||||
}
|
||||
else {
|
||||
contractResponse.body(parsedBody.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void mapRequestBodyRules(org.springframework.cloud.contract.spec.internal.Request contractRequest,
|
||||
Category bodyRules) {
|
||||
contractRequest.bodyMatchers((bodyMatchers) -> {
|
||||
bodyRules.getMatchingRules().forEach((key, matchingRuleGroup) -> {
|
||||
if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the AND combination rule logic is supported");
|
||||
}
|
||||
|
||||
matchingRuleGroup.getRules().forEach((matchingRule) -> {
|
||||
if (matchingRule instanceof RegexMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
|
||||
}
|
||||
else if (matchingRule instanceof DateMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byDate());
|
||||
}
|
||||
else if (matchingRule instanceof TimeMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byTime());
|
||||
}
|
||||
else if (matchingRule instanceof TimestampMatcher) {
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp());
|
||||
}
|
||||
else if (matchingRule instanceof NumberTypeMatcher) {
|
||||
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
|
||||
case NUMBER:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number()));
|
||||
break;
|
||||
case INTEGER:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger()));
|
||||
break;
|
||||
case DECIMAL:
|
||||
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble()));
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported number type!");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void mapRequestBody(org.springframework.cloud.contract.spec.internal.Request contractRequest,
|
||||
Request pactRequest) {
|
||||
Object parsedBody = BodyConverter.toSCCBody(pactRequest);
|
||||
if (parsedBody instanceof Map) {
|
||||
contractRequest.body((Map) parsedBody);
|
||||
}
|
||||
else if (parsedBody instanceof List) {
|
||||
contractRequest.body((List) parsedBody);
|
||||
}
|
||||
else {
|
||||
contractRequest.body(parsedBody.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void mapRequestCookies(org.springframework.cloud.contract.spec.internal.Request contractRequest,
|
||||
Request pactRequest) {
|
||||
Category headerRules = pactRequest.getMatchingRules().rulesForCategory("header");
|
||||
String[] splitCookiesHeader = pactRequest.getHeaders().get("Cookie").get(0).split(";");
|
||||
Map<String, String> foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("="))
|
||||
.collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0],
|
||||
splittedCookieHeader -> splittedCookieHeader[1]));
|
||||
|
||||
contractRequest.cookies((cookies) -> foundCookies.forEach((key, value) -> {
|
||||
if (headerRules.getMatchingRules().containsKey("Cookie")) {
|
||||
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie");
|
||||
if (matchingRuleGroup.getRules().size() > 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only 1 rule at a time for a header is supported");
|
||||
}
|
||||
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
|
||||
if (matchingRule instanceof RegexMatcher) {
|
||||
cookies.cookie(key,
|
||||
contractRequest.$(
|
||||
contractRequest.c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())),
|
||||
contractRequest.p(value)));
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the header matcher of type regex is supported");
|
||||
}
|
||||
}
|
||||
else {
|
||||
cookies.cookie(key, value);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void mapResponseCookies(org.springframework.cloud.contract.spec.internal.Response contractResponse,
|
||||
Response pactResponse) {
|
||||
Category headerRules = pactResponse.getMatchingRules().rulesForCategory("header");
|
||||
String[] splitCookiesHeader = pactResponse.getHeaders().get("Cookie").get(0).split(";");
|
||||
Map<String, String> foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("="))
|
||||
.collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0],
|
||||
splittedCookieHeader -> splittedCookieHeader[1]));
|
||||
|
||||
contractResponse.cookies((cookies) -> foundCookies.forEach((key, value) -> {
|
||||
if (headerRules.getMatchingRules().containsKey("Cookie")) {
|
||||
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie");
|
||||
if (matchingRuleGroup.getRules().size() > 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only 1 rule at a time for a header is supported");
|
||||
}
|
||||
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
|
||||
if (matchingRule instanceof RegexMatcher) {
|
||||
cookies.cookie(key, contractResponse.$(
|
||||
contractResponse.p(
|
||||
contractResponse.regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))),
|
||||
contractResponse.c(value)));
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the header matcher of type regex is supported");
|
||||
}
|
||||
}
|
||||
else {
|
||||
cookies.cookie(key, value);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void mapRequestUrl(org.springframework.cloud.contract.spec.internal.Request contractRequest,
|
||||
Request pactRequest) {
|
||||
if (MapUtils.isNotEmpty(pactRequest.getQuery())) {
|
||||
contractRequest.url(pactRequest.getPath(),
|
||||
(url) -> url.queryParameters((queryParameters) -> pactRequest.getQuery().forEach((key,
|
||||
values) -> values.forEach((singleValue) -> queryParameters.parameter(key, singleValue)))));
|
||||
}
|
||||
else {
|
||||
contractRequest.url(pactRequest.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private void mapRequestHeaders(org.springframework.cloud.contract.spec.internal.Request contractRequest,
|
||||
Request pactRequest) {
|
||||
Category headerRules = pactRequest.getMatchingRules().rulesForCategory("header");
|
||||
contractRequest.headers((headers) -> pactRequest.getHeaders().forEach((key, values) -> {
|
||||
if (key.equalsIgnoreCase("Cookie")) {
|
||||
return;
|
||||
}
|
||||
if (headerRules.getMatchingRules().containsKey(key)) {
|
||||
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key);
|
||||
if (matchingRuleGroup.getRules().size() > 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only 1 rule at a time for a header is supported");
|
||||
}
|
||||
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
|
||||
if (matchingRule instanceof RegexMatcher) {
|
||||
values.forEach((value) -> {
|
||||
headers.header(key,
|
||||
contractRequest.$(
|
||||
contractRequest
|
||||
.c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())),
|
||||
contractRequest.p(value)));
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the header matcher of type regex is supported");
|
||||
}
|
||||
}
|
||||
else {
|
||||
values.forEach((value) -> headers.header(key, value));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void mapResponseHeaders(org.springframework.cloud.contract.spec.internal.Response contractResponse,
|
||||
Response pactResponse) {
|
||||
Category headerRules = pactResponse.getMatchingRules().rulesForCategory("header");
|
||||
contractResponse.headers((headers) -> pactResponse.getHeaders().forEach((key, values) -> {
|
||||
if (key.equalsIgnoreCase("Cookie")) {
|
||||
return;
|
||||
}
|
||||
if (headerRules.getMatchingRules().containsKey(key)) {
|
||||
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key);
|
||||
if (matchingRuleGroup.getRules().size() > 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only 1 rule at a time for a header is supported");
|
||||
}
|
||||
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
|
||||
if (matchingRule instanceof RegexMatcher) {
|
||||
values.forEach((value) -> {
|
||||
headers.header(key,
|
||||
contractResponse.$(
|
||||
contractResponse.p(contractResponse
|
||||
.regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))),
|
||||
contractResponse.c(value)));
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Currently only the header matcher of type regex is supported");
|
||||
}
|
||||
}
|
||||
else {
|
||||
values.forEach((value) -> headers.header(key, value));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private String buildDescription(RequestResponseInteraction interaction) {
|
||||
StringBuilder description = new StringBuilder(interaction.getDescription());
|
||||
List<ProviderState> providerStates = interaction.getProviderStates();
|
||||
for (ProviderState providerState : providerStates) {
|
||||
description.append(" ").append(providerState.getName());
|
||||
Map<String, Object> params = providerState.getParams();
|
||||
if (MapUtils.isNotEmpty(params)) {
|
||||
description.append("(");
|
||||
params.forEach((k, v) -> {
|
||||
description.append(k).append(": ").append(v.toString()).append(", ");
|
||||
});
|
||||
description.delete(description.length() - 2, description.length());
|
||||
description.append(")");
|
||||
}
|
||||
}
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.spec.pact;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import au.com.dius.pact.core.model.generators.Category;
|
||||
import au.com.dius.pact.core.model.generators.DateGenerator;
|
||||
import au.com.dius.pact.core.model.generators.DateTimeGenerator;
|
||||
import au.com.dius.pact.core.model.generators.Generator;
|
||||
import au.com.dius.pact.core.model.generators.Generators;
|
||||
import au.com.dius.pact.core.model.generators.RandomBooleanGenerator;
|
||||
import au.com.dius.pact.core.model.generators.RandomDecimalGenerator;
|
||||
import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator;
|
||||
import au.com.dius.pact.core.model.generators.RandomIntGenerator;
|
||||
import au.com.dius.pact.core.model.generators.RandomStringGenerator;
|
||||
import au.com.dius.pact.core.model.generators.RegexGenerator;
|
||||
import au.com.dius.pact.core.model.generators.TimeGenerator;
|
||||
import au.com.dius.pact.core.model.generators.UuidGenerator;
|
||||
import groovy.lang.GString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.Body;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
|
||||
/**
|
||||
* Convert a value for a given {@link Generator}.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
* @author Stessy Delcroix
|
||||
* @since 2.0.0
|
||||
*/
|
||||
final class ValueGeneratorConverter {
|
||||
|
||||
private static final String INTEGER_PATTERN = "-?(\\d+)";
|
||||
|
||||
private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN);
|
||||
|
||||
private static final String DECIMAL_PATTERN = "-?(\\d*\\.\\d+)";
|
||||
|
||||
private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN);
|
||||
|
||||
private static final String HEX_PATTERN = "[a-fA-F0-9]+";
|
||||
|
||||
private static final Pattern HEX = Pattern.compile(HEX_PATTERN);
|
||||
|
||||
private static final String ALPHA_NUMERIC_PATTERN = "[a-zA-Z0-9]+";
|
||||
|
||||
private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN);
|
||||
|
||||
private static final String UUID_PATTERN = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
|
||||
|
||||
private static final Pattern UUID = Pattern.compile(UUID_PATTERN);
|
||||
|
||||
private static final String ANY_DATE_PATTERN = "(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])";
|
||||
|
||||
private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN);
|
||||
|
||||
private static final String ANY_TIME_PATTERN = "(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])";
|
||||
|
||||
private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN);
|
||||
|
||||
private static final String ANY_DATE_TIME_PATTERN = "([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])";
|
||||
|
||||
private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN);
|
||||
|
||||
private static final String TRUE_OR_FALSE_PATTERN = "(true|false)";
|
||||
|
||||
private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN);
|
||||
|
||||
private ValueGeneratorConverter() {
|
||||
|
||||
}
|
||||
|
||||
static DslProperty<Object> convert(Generator generator,
|
||||
BiFunction<Pattern, Object, DslProperty<Object>> dslPropertyProvider) {
|
||||
Pattern pattern = null;
|
||||
if (generator instanceof RandomIntGenerator) {
|
||||
pattern = INTEGER;
|
||||
}
|
||||
else if (generator instanceof RandomDecimalGenerator) {
|
||||
pattern = DECIMAL;
|
||||
}
|
||||
else if (generator instanceof RandomHexadecimalGenerator) {
|
||||
pattern = HEX;
|
||||
}
|
||||
else if (generator instanceof RandomStringGenerator) {
|
||||
pattern = ALPHA_NUMERIC;
|
||||
}
|
||||
else if (generator instanceof RegexGenerator) {
|
||||
pattern = Pattern.compile(((RegexGenerator) generator).getRegex());
|
||||
}
|
||||
else if (generator instanceof UuidGenerator) {
|
||||
pattern = UUID;
|
||||
}
|
||||
else if (generator instanceof DateGenerator) {
|
||||
pattern = getDateTimePattern(((DateGenerator) generator).getFormat(), ANY_DATE);
|
||||
}
|
||||
else if (generator instanceof TimeGenerator) {
|
||||
pattern = getDateTimePattern(((TimeGenerator) generator).getFormat(), ANY_TIME);
|
||||
}
|
||||
else if (generator instanceof DateTimeGenerator) {
|
||||
pattern = getDateTimePattern(((DateTimeGenerator) generator).getFormat(), ANY_DATE_TIME);
|
||||
}
|
||||
else if (generator instanceof RandomBooleanGenerator) {
|
||||
pattern = TRUE_OR_FALSE;
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"We currently don't support a generator of type " + generator.getClass().getSimpleName());
|
||||
}
|
||||
else {
|
||||
Object generatedValue = generator.generate(new HashMap<>());
|
||||
return dslPropertyProvider.apply(pattern, generatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static Pattern getDateTimePattern(String format, Pattern defaultPattern) {
|
||||
return StringUtils.isNotBlank(format) ? Pattern.compile(format) : defaultPattern;
|
||||
}
|
||||
|
||||
static Generators extract(Body body, Function<DslProperty<?>, Object> dslPropertyValueProvider) {
|
||||
Generators generators = new Generators();
|
||||
traverse(body, dslPropertyValueProvider, "", generators, Category.BODY);
|
||||
return generators;
|
||||
}
|
||||
|
||||
static Generators extract(OutputMessage message, Function<DslProperty<?>, Object> dslPropertyValueProvider) {
|
||||
Generators generators = new Generators();
|
||||
traverse(message.getBody(), dslPropertyValueProvider, "", generators, Category.BODY);
|
||||
return generators;
|
||||
}
|
||||
|
||||
private static void traverse(Object value, Function<DslProperty<?>, Object> dslPropertyValueProvider, String path,
|
||||
Generators generators, Category category) {
|
||||
Object v = value;
|
||||
if (v instanceof DslProperty) {
|
||||
v = dslPropertyValueProvider.apply((DslProperty<?>) v);
|
||||
}
|
||||
if (v instanceof GString) {
|
||||
v = ContentUtils.extractValue((GString) v, dslPropertyValueProvider);
|
||||
}
|
||||
if (v instanceof Map) {
|
||||
((Map) v).forEach(
|
||||
(key, val) -> traverse(val, dslPropertyValueProvider, path + "." + key, generators, category));
|
||||
}
|
||||
else if (v instanceof Collection) {
|
||||
AtomicInteger index = new AtomicInteger();
|
||||
((Collection<?>) v).forEach(val -> traverse(val, dslPropertyValueProvider,
|
||||
path + "[" + index.getAndIncrement() + "]", generators, category));
|
||||
}
|
||||
else if (v instanceof DslProperty) {
|
||||
traverse(v, dslPropertyValueProvider, path, generators, category);
|
||||
}
|
||||
else if (v instanceof RegexProperty || v instanceof Pattern) {
|
||||
RegexProperty regexProperty = new RegexProperty(v);
|
||||
switch (regexProperty.pattern()) {
|
||||
case INTEGER_PATTERN:
|
||||
generators.addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE));
|
||||
break;
|
||||
case DECIMAL_PATTERN:
|
||||
generators.addGenerator(category, path, new RandomDecimalGenerator(10));
|
||||
break;
|
||||
case HEX_PATTERN:
|
||||
generators.addGenerator(category, path, new RandomHexadecimalGenerator(10));
|
||||
break;
|
||||
case ALPHA_NUMERIC_PATTERN:
|
||||
generators.addGenerator(category, path, new RandomStringGenerator(10));
|
||||
break;
|
||||
case UUID_PATTERN:
|
||||
generators.addGenerator(category, path, UuidGenerator.INSTANCE);
|
||||
break;
|
||||
case ANY_DATE_PATTERN:
|
||||
generators.addGenerator(category, path, new DateGenerator());
|
||||
break;
|
||||
case ANY_TIME_PATTERN:
|
||||
generators.addGenerator(category, path, new TimeGenerator());
|
||||
break;
|
||||
case ANY_DATE_TIME_PATTERN:
|
||||
generators.addGenerator(category, path, new DateTimeGenerator());
|
||||
break;
|
||||
case TRUE_OR_FALSE_PATTERN:
|
||||
generators.addGenerator(category, path, RandomBooleanGenerator.INSTANCE);
|
||||
break;
|
||||
default:
|
||||
generators.addGenerator(category, path, new RegexGenerator(regexProperty.pattern()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class MethodBuilderSpec extends Specification {
|
||||
}
|
||||
}
|
||||
}
|
||||
File stubFile = new File("invalid-method:name.groovy")
|
||||
File stubFile = new File("invalid-method;name.groovy")
|
||||
|
||||
ContractMetadata metadata = new ContractMetadata(stubFile.toPath(), false, 0, null, contractDsl)
|
||||
SingleContractMetadata singleContractMetadata = new SingleContractMetadata(contractDsl, metadata)
|
||||
|
||||
Reference in New Issue
Block a user