Added support for Stub Runner and Pact Broker

fixes gh-191
commit 533f1d3d0e8f6dcd37677bfef555fc2ea86bc1b1
Author: Tim Ysewyn <Tim.Ysewyn@me.com>
Date:   Wed Mar 7 15:56:48 2018 +0100

    Upgraded pact-jvm-model to 3.5.13
This commit is contained in:
Marcin Grzejszczak
2018-04-02 22:50:40 +02:00
parent de529e222e
commit 6402e14f3c
82 changed files with 953 additions and 395 deletions

View File

@@ -0,0 +1,376 @@
/*
* Copyright 2013-2018 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
*
* http://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.stubrunner;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import au.com.dius.pact.model.Pact;
import au.com.dius.pact.model.PactSpecVersion;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junit.loader.PactBrokerLoader;
import au.com.dius.pact.provider.junit.loader.PactLoader;
import au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver;
import au.com.dius.pact.provider.junit.sysprops.ValueResolver;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
/**
* Allows downloading of Pact files from the Pact Broker.
*
* @author Marcin Grzejszczak
* @since 2.0.0
*/
public final class PactStubDownloaderBuilder implements StubDownloaderBuilder {
private static final List<String> ACCEPTABLE_PROTOCOLS = Collections
.singletonList("pact");
/**
* Does any of the accepted protocols matches the URL of the repository
* @param url - of the repository
*/
private static boolean isProtocolAccepted(String url) {
return ACCEPTABLE_PROTOCOLS.stream().anyMatch(url::startsWith);
}
@Override public StubDownloader build(StubRunnerOptions stubRunnerOptions) {
if (stubRunnerOptions.getStubsMode() == StubRunnerProperties.StubsMode.CLASSPATH ||
stubRunnerOptions.getStubRepositoryRoot() == null) {
return null;
}
Resource resource = stubRunnerOptions.getStubRepositoryRoot();
if (!(resource instanceof PactResource)) {
return null;
}
return new PactStubDownloader(stubRunnerOptions);
}
@Override public Resource resolve(String location, ResourceLoader resourceLoader) {
if (StringUtils.isEmpty(location) || !isProtocolAccepted(location)) {
return null;
}
return new PactResource(location);
}
}
class PactResource extends AbstractResource {
private final String rawLocation;
PactResource(String location) {
this.rawLocation = location;
}
@Override public String getDescription() {
return this.rawLocation;
}
@Override public InputStream getInputStream() {
return null;
}
@Override public URI getURI() {
return URI.create(this.rawLocation);
}
}
class PactStubDownloader implements StubDownloader {
private static final String TEMP_DIR_PREFIX = "pact";
private static final Log log = LogFactory.getLog(PactStubDownloader.class);
// Preloading class for the shutdown hook not to throw ClassNotFound
private static final Class CLAZZ = TemporaryFileStorage.class;
private static final String ARTIFICIAL_NAME_ENDING_WITH_GROOVY = "name.groovy";
private final StubRunnerOptions stubRunnerOptions;
private final boolean deleteStubsAfterTest;
private final ObjectMapper objectMapper;
private static final String PROVIDER_NAME_WITH_GROUP_ID = "pactbroker.provider-name-with-group-id";
PactStubDownloader(StubRunnerOptions stubRunnerOptions) {
this.stubRunnerOptions = stubRunnerOptions;
this.objectMapper = new ObjectMapper();
this.deleteStubsAfterTest = stubRunnerOptions.isDeleteStubsAfterTest();
registerShutdownHook();
}
@Override public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration stubConfiguration) {
String version = stubConfiguration.version;
final FromPropsThenFromSysEnv resolver = new FromPropsThenFromSysEnv(this.stubRunnerOptions);
List<String> tags = tags(version, resolver);
try {
PactLoader loader = pactBrokerLoader(resolver, tags);
String providerName = providerName(stubConfiguration);
List<Pact> pacts = loader.load(providerName);
if (pacts.isEmpty()) {
log.warn("No pact definitions found for provider [" + providerName + "]");
return null;
}
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.createTempDir(TEMP_DIR_PREFIX);
// make the groupid / artifactid folders
String coordinatesFolderName = stubConfiguration.getGroupId().replace(".", File.separator) +
File.separator + stubConfiguration.getArtifactId();
File contractsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "contracts");
File mappingsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "mappings");
boolean createdContractsDirs = contractsFolder.mkdirs();
boolean createdMappingsDirs = mappingsFolder.mkdirs();
if (!createdContractsDirs || !createdMappingsDirs) {
throw new IllegalStateException("Failed to create mandatory [contracts] or [mappings] folders under [" + coordinatesFolderName + "]");
}
storePacts(providerName, pacts, contractsFolder, mappingsFolder);
return new AbstractMap.SimpleEntry<>(stubConfiguration, tmpDirWhereStubsWillBeUnzipped);
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void storePacts(String providerName, List<Pact> pacts, File contractsFolder,
File mappingsFolder) {
for (int i = 0; i < pacts.size(); i++) {
String json = toJson(pacts.get(i).toMap(PactSpecVersion.V3));
File file = new File(contractsFolder, i + "_" +
providerName.replace(":", "_") + "_pact.json");
storeFile(file.toPath(), json.getBytes());
try {
storeMapping(mappingsFolder, file);
} catch (Exception e) {
log.warn("Exception occurred while trying to store the mapping", e);
}
}
}
private void storeMapping(File mappingsFolder, File file) {
Collection<Contract> contracts = new PactContractConverter()
.convertFrom(file);
if (log.isDebugEnabled()) {
log.debug("Converted pact file [" + file + "] to [" + contracts.size() + "] contracts");
}
StubGeneratorProvider provider = new StubGeneratorProvider();
Collection<StubGenerator> stubGenerators = provider
.converterForName(ARTIFICIAL_NAME_ENDING_WITH_GROOVY);
if (log.isDebugEnabled()) {
log.debug("Found following matching stub generators " + stubGenerators);
}
for (StubGenerator stubGenerator : stubGenerators) {
Map<Contract, String> map = stubGenerator
.convertContents(file.getName(),
new ContractMetadata(file.toPath(), false,
contracts.size(), null, contracts));
for (Map.Entry<Contract, String> entry : map.entrySet()) {
String value = entry.getValue();
File mapping = new File(mappingsFolder,
StringUtils.stripFilenameExtension(file.getName()) + "_" +
Math.abs(entry.getKey().hashCode()) + ".json");
storeFile(mapping.toPath(), value.getBytes());
}
}
}
private void storeFile(Path path, byte[] contents) {
try {
Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path.toString() + "]");
}
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private String providerName(StubConfiguration stubConfiguration) {
boolean providerNameWithGroupId = Boolean.parseBoolean(
StubRunnerPropertyUtils.getProperty(this.stubRunnerOptions.getProperties(),
PROVIDER_NAME_WITH_GROUP_ID));
if (providerNameWithGroupId) {
return stubConfiguration.getGroupId() + ":" + stubConfiguration.getArtifactId();
}
return stubConfiguration.getArtifactId();
}
@NotNull PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) throws IOException {
Resource repo = this.stubRunnerOptions.getStubRepositoryRoot();
String schemeSpecificPart = schemeSpecificPart(repo.getURI());
URI pactBrokerUrl = URI.create(schemeSpecificPart);
return new PactBrokerLoader(new PactBroker() {
@Override public Class<? extends Annotation> annotationType() {
return PactBroker.class;
}
@Override public String host() {
return resolver.resolveValue("pactbroker.host:" + pactBrokerUrl.getHost());
}
@Override public String port() {
return resolver.resolveValue("pactbroker.port:" + pactBrokerUrl.getPort());
}
@Override public String protocol() {
return resolver.resolveValue("pactbroker.protocol:" + pactBrokerUrl.getScheme());
}
@Override public String[] tags() {
return tags.toArray(new String[0]);
}
@Override public boolean failIfNoPactsFound() {
return true;
}
@Override public PactBrokerAuth authentication() {
return new PactBrokerAuth() {
@Override public Class<? extends Annotation> annotationType() {
return PactBrokerAuth.class;
}
@Override public String scheme() {
return resolver.resolveValue("pactbroker.auth.scheme:basic");
}
@Override public String username() {
return resolver.resolveValue("pactbroker.auth.username:");
}
@Override public String password() {
return resolver.resolveValue("pactbroker.auth.password:");
}
};
}
@Override public Class<? extends ValueResolver> valueResolver() {
return SystemPropertyResolver.class;
}
});
}
private String schemeSpecificPart(URI uri) {
String part = uri.getSchemeSpecificPart();
if (StringUtils.isEmpty(part)) {
return part;
}
return part.startsWith("//") ? part.substring(2) : part;
}
@NotNull private List<String> tags(String version, ValueResolver resolver) {
String defaultTag = StubConfiguration.DEFAULT_VERSION.equals(version)
? "latest" : version;
return new ArrayList<>(Arrays.asList(StringUtils
.commaDelimitedListToStringArray(
resolver.resolveValue("pactbroker.tags:" + defaultTag + ""))));
}
private String toJson(Map map) {
try {
return this.objectMapper.writeValueAsString(map);
}
catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(
() -> TemporaryFileStorage.cleanup(PactStubDownloader.this.deleteStubsAfterTest)));
}
}
class FromPropsThenFromSysEnv implements ValueResolver {
final SystemPropertyResolver resolver = new SystemPropertyResolver();
StubRunnerOptions options;
FromPropsThenFromSysEnv(StubRunnerOptions options) {
this.options = options;
}
@Override public String resolveValue(String expression) {
PropertyValueTuple tuple = new PropertyValueTuple(expression).invoke();
String propertyName = tuple.getPropertyName();
String property = StubRunnerPropertyUtils
.getProperty(this.options.getProperties(), propertyName);
if (StringUtils.hasText(property)) {
return property;
}
return this.resolver.resolveValue(expression);
}
@Override public boolean propertyDefined(String property) {
PropertyValueTuple tuple = new PropertyValueTuple(property).invoke();
String propertyName = tuple.getPropertyName();
boolean hasProperty = StubRunnerPropertyUtils
.hasProperty(this.options.getProperties(), propertyName);
if (hasProperty) {
return true;
}
return this.resolver.propertyDefined(property);
}
}
// taken from pact - au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver.PropertyValueTuple
class PropertyValueTuple {
private String propertyName;
PropertyValueTuple(String property) {
this.propertyName = property;
}
String getPropertyName() {
return this.propertyName;
}
PropertyValueTuple invoke() {
if (this.propertyName.contains(":")) {
String[] kv = org.apache.commons.lang3.StringUtils
.splitPreserveAllTokens(this.propertyName, ':');
this.propertyName = kv[0];
}
return this;
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2013-2018 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
*
* http://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.dsl.DslPart
import au.com.dius.pact.consumer.dsl.PactDslJsonArray
import au.com.dius.pact.consumer.dsl.PactDslJsonBody
import au.com.dius.pact.model.OptionalBody
import au.com.dius.pact.model.Request
import au.com.dius.pact.model.Response
import au.com.dius.pact.model.generators.Generator
import au.com.dius.pact.model.v3.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
import java.util.regex.Pattern
/**
* @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) {
if (!isRoot) {
p = p.object()
}
processMap(v as Map, p as PactDslJsonBody, dslPropertyValueExtractor)
if (!isRoot) {
p = p.closeObject()
}
} else if (v instanceof Collection) {
if (!isRoot) {
p = p.array()
}
processCollection(v as Collection, p as PactDslJsonArray, dslPropertyValueExtractor)
if (!isRoot) {
p = p.closeArray()
}
}
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 {
traverse(it, jsonArray, dslPropertyValueExtractor)
}
})
}
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 {
PactDslJsonBody current = jsonObject.object(k)
traverse(v, current, dslPropertyValueExtractor)
current.closeObject()
}
})
}
static def toSCCBody(Request request) {
def body = parseBody(request.body)
if (request.generators.isNotEmpty() && request.generators.categories.containsKey(au.com.dius.pact.model.generators.Category.BODY)) {
applyGenerators(body, request.generators.categories.get(au.com.dius.pact.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.model.generators.Category.BODY)) {
applyGenerators(body, response.generators.categories.get(au.com.dius.pact.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.model.generators.Category.BODY)) {
applyGenerators(body, message.generators.categories.get(au.com.dius.pact.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().parseText(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)
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.EqualsMatcher
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.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 {
private static final RegexPatterns regexPatterns = new RegexPatterns()
static Category matchingRulesForBody(BodyMatchers bodyMatchers) {
return matchingRulesFor("body", bodyMatchers)
}
private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) {
Category category = new Category(categoryName)
bodyMatchers.jsonPathMatchers().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.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}"
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.v3.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(Contract contract) {
MessagePactBuilder messagePactBuilder = MessagePactBuilder.consumer("Consumer")
.hasPactWith("Provider")
.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 }))
messagePactBuilder = messagePactBuilder.withContent(pactResponseBody)
}
if (message.headers) {
messagePactBuilder = messagePactBuilder.withMetadata(getMetadata(message.headers))
}
}
return messagePactBuilder.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()
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.MatchingRule
import au.com.dius.pact.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.RuleLogic
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.model.matchingrules.TypeMatcher
import au.com.dius.pact.model.v3.messaging.Message
import au.com.dius.pact.model.v3.messaging.MessagePact
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
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 = '$'
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.equals(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(number()))
break
case NumberTypeMatcher.NumberType.INTEGER:
jsonPath(key, byRegex(anInteger()))
break
case NumberTypeMatcher.NumberType.DECIMAL:
jsonPath(key, byRegex(aDouble()))
break
default:
throw new RuntimeException("Unsupported number type!")
}
}
}
}
}
}
}
}
if (!message.metaData.isEmpty()) {
headers {
message.metaData.each { String k, String v ->
if (k.equalsIgnoreCase("contentType")) {
messagingContentType(v)
} else {
header(k, v)
}
}
}
}
}
}
})
}
private String getTriggeredBy(Message message) {
return message.providerStates.first().name
.replace(':', ' ')
.replace(' ', '_')
.replace('(', '')
.replace(')', '')
.uncapitalize() + "()"
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.Pact
import au.com.dius.pact.model.PactReader
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.v3.messaging.MessagePact
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 RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator()
private MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator()
private RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator()
private MessagePactCreator messagePactCreator = new MessagePactCreator()
@Override
boolean isAccepted(File file) {
try {
PactReader.loadPact(file)
return true
} catch (Exception e) {
return false
}
}
@Override
Collection<Contract> convertFrom(File file) {
Pact pact = PactReader.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<>()
contracts.collect({ Contract contract ->
if (contract.request) {
pactContracts.add(requestResponsePactCreator.createFromContract(contract))
}
if (contract.input) {
pactContracts.add(messagePactCreator.createFromContract(contract))
}
})
return pactContracts
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2013-2018 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
*
* http://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.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.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.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.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(Contract contract) {
assertNoExecutionProperty(contract)
PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer("Consumer")
.hasPactWith("Provider")
PactDslRequestWithPath pactDslRequest = createPactDslRequestWithPath(contract, pactDslWithProvider)
PactDslResponse 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, 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.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 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 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
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.OptionalBody
import au.com.dius.pact.model.ProviderState
import au.com.dius.pact.model.Request
import au.com.dius.pact.model.RequestResponseInteraction
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.Response
import au.com.dius.pact.model.matchingrules.Category
import au.com.dius.pact.model.matchingrules.DateMatcher
import au.com.dius.pact.model.matchingrules.MatchingRule
import au.com.dius.pact.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.model.matchingrules.MaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinMaxTypeMatcher
import au.com.dius.pact.model.matchingrules.MinTypeMatcher
import au.com.dius.pact.model.matchingrules.NullMatcher
import au.com.dius.pact.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.model.matchingrules.RegexMatcher
import au.com.dius.pact.model.matchingrules.RuleLogic
import au.com.dius.pact.model.matchingrules.TimeMatcher
import au.com.dius.pact.model.matchingrules.TimestampMatcher
import au.com.dius.pact.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.DslProperty
import org.springframework.cloud.contract.spec.internal.NotToEscapePattern
import org.springframework.cloud.contract.spec.internal.RegexPatterns
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
import java.util.regex.Pattern
/**
* Creator of {@link Contract} instances
*
* @author Tim Ysewyn
* @since 2.0.0
*/
@CompileStatic
@PackageScope
class RequestResponseSCContractCreator {
private static final String FULL_BODY = '$'
private static final RegexPatterns regexPatterns = new RegexPatterns()
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 { k, v ->
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) {
header(k, new DslProperty((Object)Pattern.compile(rule.getRegex()), (Object)v))
} else {
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
}
} else {
header(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.equals(key)) {
JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(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, String v ->
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) {
header(k, new DslProperty(new DslProperty(v), new NotToEscapePattern(Pattern.compile(rule.getRegex()))))
} else {
throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported")
}
} else {
header(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
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.generators.Category
import au.com.dius.pact.model.generators.DateGenerator
import au.com.dius.pact.model.generators.DateTimeGenerator
import au.com.dius.pact.model.generators.Generator
import au.com.dius.pact.model.generators.Generators
import au.com.dius.pact.model.generators.RandomBooleanGenerator
import au.com.dius.pact.model.generators.RandomDecimalGenerator
import au.com.dius.pact.model.generators.RandomHexadecimalGenerator
import au.com.dius.pact.model.generators.RandomIntGenerator
import au.com.dius.pact.model.generators.RandomStringGenerator
import au.com.dius.pact.model.generators.RegexGenerator
import au.com.dius.pact.model.generators.TimeGenerator
import au.com.dius.pact.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.verifier.util.ContentUtils
import java.util.regex.Pattern
/**
* @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(null)
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 Pattern) {
switch (v.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(v.pattern()))
break
}
}
}
}

View File

@@ -0,0 +1,5 @@
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
org.springframework.cloud.contract.stubrunner.PactStubDownloaderBuilder

View File

@@ -0,0 +1,114 @@
package org.springframework.cloud.contract.stubrunner
import java.nio.file.Files
import au.com.dius.pact.model.Pact
import au.com.dius.pact.model.PactSource
import au.com.dius.pact.provider.junit.loader.PactLoader
import au.com.dius.pact.provider.junit.sysprops.ValueResolver
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import org.jetbrains.annotations.NotNull
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
/**
* @author Marcin Grzejszczak
*/
class PactStubDownloaderBuilderSpec extends Specification {
def "should retrieve pacts from broker"() throws IOException {
given:
Collection<Pact> pacts = new PactContractConverter().convertTo([Contract.make {
request {
url "/foo"
method GET()
}
response {
status OK()
}
},Contract.make {
request {
url "/bar"
method GET()
}
response {
status OK()
}
}])
StubRunnerOptions options = new StubRunnerOptionsBuilder()
.withProperties(props())
.build()
PactStubDownloader downloader = new PactStubDownloader(options) {
@NotNull @Override PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) {
return new PactLoader() {
@Override List<Pact> load(String providerName) {
return pacts
}
@Override PactSource getPactSource() {
return null
}
}
}
}
when:
Map.Entry<StubConfiguration, File> entry = downloader
.downloadAndUnpackStubJar(new StubConfiguration("com.example:bobby:+:classifier"))
then:
entry != null
entry.getValue().exists()
File contracts = new File(entry.getValue(), "com/example/bobby/contracts")
contracts.exists()
contracts.list() != null
File mappings = new File(entry.getValue(), "com/example/bobby/mappings")
mappings.exists()
mappings.list() != null
mappings.list().size() == 2
StubMapping.buildFrom(new String(Files.readAllBytes(mappings.listFiles()[0].toPath())))
StubMapping.buildFrom(new String(Files.readAllBytes(mappings.listFiles()[1].toPath())))
}
Map<String, String> props() {
Map<String, String> map = new HashMap<>()
// map.put("pactbroker.host", "localhost")
// map.put("pactbroker.port", String.valueOf(this.port))
// map.put("pactbroker.host", "test.pact.dius.com.au")
// map.put("pactbroker.port", "443")
// map.put("pactbroker.protocol", "https")
// map.put("pactbroker.auth.scheme", "Basic")
// map.put("pactbroker.auth.username", "dXfltyFMgNOFZAxr8io9wJ37iUpY42M")
// map.put("pactbroker.auth.password", "O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1")
return map
}
// @After
// void tearDown() {
// SnapshotRecordResult recording = WireMock.stopRecording()
// List<StubMapping> mappings = recording.getStubMappings()
// storeMappings(mappings)
// }
// private void recordFromBroker() {
// WireMock.startRecording(WireMock.recordSpec()
// .forTarget("https://test.pact.dius.com.au")
// .extractTextBodiesOver(9999999L)
// .extractBinaryBodiesOver(9999999L)
// .makeStubsPersistent(false))
// }
// private void storeMappings(List<StubMapping> mappings) {
// try {
// File proxiedStubs = new File("target/stubs")
// proxiedStubs.mkdirs()
// for (StubMapping mapping : mappings) {
// File stub = new File(proxiedStubs, "foo" + ".json")
// stub.createNewFile()
// Files.write(stub.toPath(), mapping.toString().getBytes())
// }
// } catch (Exception e) {
// throw new RuntimeException(e)
// }
// }
}

View File

@@ -0,0 +1,665 @@
/*
* Copyright 2013-2018 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
*
* http://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.model.Pact
import au.com.dius.pact.model.PactSpecVersion
import groovy.json.JsonOutput
import org.skyscreamer.jsonassert.JSONAssert
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import spock.lang.Issue
import spock.lang.Specification
import spock.lang.Subject
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class PactContractConverterSpec extends Specification {
File pactJson = new File(PactContractConverterSpec.getResource("/pact/pact.json").toURI())
File pact509Json = new File(PactContractConverterSpec.getResource("/pact/pact_509.json").toURI())
File pactv2Json = new File(PactContractConverterSpec.getResource("/pact/pact_v2.json").toURI())
File pactv3Json = new File(PactContractConverterSpec.getResource("/pact/pact_v3.json").toURI())
File pactv3MessagingJson = new File(PactContractConverterSpec.getResource("/pact/pact_v3_messaging.json").toURI())
File pactv3UnsupportedRuleLogicJson = new File(PactContractConverterSpec.getResource("/pact/pact_v3_unsupported_rule_logic.json").toURI())
@Subject PactContractConverter converter = new PactContractConverter()
def "should accept json files that are pact files"() {
expect:
converter.isAccepted(pactJson)
}
def "should reject json files that are pact files"() {
given:
File invalidPact = new File(PactContractConverterSpec.getResource("/pact/invalid_pact.json").toURI())
expect:
converter.isAccepted(invalidPact)
}
def "should convert from pact to contract"() {
given:
Contract expectedContract = Contract.make {
description("a retrieve Mallory request a user with username 'username' and password 'password' exists")
request {
method(GET())
url("/mallory") {
queryParameters {
parameter("name", "ron")
parameter("status", "good")
}
}
headers {
contentType(applicationJson())
}
body(id: "123", method: "create")
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
}
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "rddtGwwWMEhnkAPEmsyE",
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
maxOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
}
when:
Collection<Contract> contracts = converter.convertFrom(pactJson)
then:
contracts == [expectedContract]
}
@Issue("#509")
def "should convert from pact with matching rules to whole body to contract"() {
given:
Contract expectedContract = Contract.make {
description("a request to POST a person provider accepts a new person")
request {
method(POST())
url("/user-service/users")
headers {
contentType(applicationJson())
}
body(firstName: "Arthur", lastName: "Dent")
}
response {
status(201)
headers {
contentType(applicationJson())
}
body(id: 42, firstName: "Arthur", lastName: "Dent")
bodyMatchers {
jsonPath('''$.['id']''', byType())
jsonPath('''$.['lastName']''', byType())
jsonPath('''$.['firstName']''', byType())
}
}
}
when:
Collection<Contract> contracts = converter.convertFrom(pact509Json)
then:
contracts == [expectedContract]
}
def "should convert from contract to pact"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
description("a retrieve Mallory request")
request {
method(GET())
url("/mallory") {
queryParameters {
parameter("name", "ron")
parameter("status", "good")
}
}
headers {
contentType(applicationJson())
}
body(
id: 123,
method: $(stub(regex("[0][1][2]"))),
something: "foo"
)
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
jsonPath('$.something', byEquality())
}
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "rddtGwwWMEhnkAPEmsyE",
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
number: $(producer(regex("[0-9]{3}")), consumer(923)),
positiveInteger: 1234567890,
negativeInteger: -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
something: "foo",
userName: "AJQrokEGPAVdOHprQpKP",
nullValue: null]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(1)
maxOccurrence(5)
})
jsonPath('$[0][*].number', byRegex(number()))
jsonPath('$[0][*].positiveInteger', byRegex(anInteger()))
jsonPath('$[0][*].negativeInteger', byRegex(anInteger()))
jsonPath('$[0][*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].negativeDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].userName', byType())
jsonPath('$[0][*].something', byEquality())
jsonPath('$[0][*].nullValue', byNull())
}
}
}
]
String expectedJson = '''
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "a retrieve Mallory request",
"request": {
"method": "GET",
"path": "\\/mallory",
"query": {
"name": ["ron"],
"status": ["good"]
},
"headers": {
"Content-Type": "application\\/json"
},
"body": {
"id": 123,
"method": "012",
"something": "foo"
},
"matchingRules": {
"body": {
"$.id": {
"matchers": [{
"match": "regex",
"regex": "[0-9]{3}"
}]
},
"$.something": {
"matchers": [{
"match": "equality"
}]
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application\\/json"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"number": 923,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890,
"userName": "AJQrokEGPAVdOHprQpKP",
"something": "foo",
"nullValue": null
}
]
],
"matchingRules": {
"body": {
"$[0][*].email": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].id": {
"matchers": [{
"match": "regex",
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}]
},
"$[0]": {
"matchers": [{
"match": "type",
"min": 1,
"max": 5
}]
},
"$[0][*].number": {
"matchers": [{
"match": "number"
}]
},
"$[0][*].positiveInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].negativeInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].positiveDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].negativeDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].userName": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].something": {
"matchers": [{
"match": "equality"
}]
},
"$[0][*].nullValue": {
"matchers": [{
"match": "null"
}]
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}
'''
when:
Pact pact = converter.convertTo(inputContracts).get(0)
then:
String actual = JsonOutput.toJson(pact.toMap(PactSpecVersion.V3))
JSONAssert.assertEquals(expectedJson, actual, false)
}
def "should fail to convert from contract to pact when contract has execution property in request"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
request {
method(GET())
url("/mallory")
body(
id: $(c("foo"), p(execute("foo")))
)
}
response {
status(200)
}
}
]
when:
converter.convertTo(inputContracts)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("execution property")
}
def "should fail to convert from contract to pact when contract has execution property in response"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
request {
method(GET())
url("/mallory")
}
response {
status(200)
body(
id: $(c(execute("foo")), p("foo"))
)
}
}
]
when:
converter.convertTo(inputContracts)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("execution property")
}
def "should convert contracts from samples to pacts"() {
given:
Resource[] contractResources = new PathMatchingResourcePatternResolver().getResources("contracts/*.groovy")
Resource[] pactResources = new PathMatchingResourcePatternResolver().getResources("contracts/*.json")
Map<String, Collection<Contract>> contracts = contractResources.collectEntries { [(it.filename) : ContractVerifierDslConverter.convertAsCollection(new File("/"), it.file)] }
Map<String, String> jsonPacts = pactResources.collectEntries { [(it.filename) : it.file.text] }
when:
Map<String, Collection<Pact>> pacts = contracts.entrySet().collectEntries { [(it.key) : converter.convertTo(it.value)] }
then:
pacts.entrySet().each {
String convertedPactAsText = JsonOutput.toJson(it.value[0].toMap(PactSpecVersion.V3))
String pactFileName = it.key.replace("groovy", "json")
println "File name [${it.key}]"
JSONAssert.assertEquals(jsonPacts.get(pactFileName), convertedPactAsText, true)
}
}
def "should convert from pact v2 to two SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("get all users for max a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "rddtGwwWMEhnkAPEmsyE",
id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
maxOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
},
Contract.make {
description("get all users for min a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email: "DPvAfkCZpOBZWzKYiDMC",
id: "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
userName: "GIUlVKoiLdHLYNKGbcSy"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv2Json)
then:
contracts == expectedContracts
}
def "should convert from pact v3 to three SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("java test interaction with a DSL array body")
request {
method(GET())
url("/")
headers {
contentType(applicationJsonUtf8())
header("Some-Header", $(c(regex("[a-zA-Z]{9}")), p("someValue")))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
header("Some-Header", $(c("someValue"), p(regex("[a-zA-Z]{9}"))))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
body([
[
"dob": "07/19/2016",
"id": 8958464620,
"name": "Rogger the Dogger",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
],
[
"dob": "07/19/2016",
"id": 4143398442,
"name": "Cat in the Hat",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
]
])
bodyMatchers {
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[*].nullValue', byNull())
jsonPath('$[*].aNumber', byRegex(number()))
jsonPath('$[*].positiveInteger', byRegex(anInteger()))
jsonPath('$[*].negativeInteger', byRegex(anInteger()))
jsonPath('$[*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[*].negativeDecimalNumber', byRegex(aDouble()))
}
}
},
Contract.make {
description("test interaction with a array body with templates")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
}
body([
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
]
])
bodyMatchers {
jsonPath('$[2].name', byType())
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[2].id', byType())
jsonPath('$[1].name', byType())
jsonPath('$[0].name', byType())
jsonPath('$[0].dob', byDate())
}
}
},
Contract.make {
description("test interaction with an array like matcher")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
contentType(applicationJsonUtf8())
}
body([
"data": [
"array1": [[
"dob": "2016-07-19",
"id": 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
]],
"array2": [[
"address": "127.0.0.1",
"name": "jvxrzduZnwwxpFYrQnpd"
]],
"array3": [[
[
"itemCount": 652571349
]
]]
],
"id": 7183997828
])
bodyMatchers {
jsonPath('$.data.array3[0]', byType() {
maxOccurrence(5)
})
jsonPath('$.data.array1', byType() {
minOccurrence(0)
})
jsonPath('$.data.array2', byType() {
minOccurrence(1)
})
jsonPath('$.id', byType())
jsonPath('$.data.array2[*].name', byType())
jsonPath('$.data.array2[*].address', byRegex("(\\d{1,3}\\.)+\\d{1,3}"))
jsonPath('$.data.array1[*].name', byType())
jsonPath('$.data.array1[*].id', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3Json)
then:
contracts == expectedContracts
}
def "should convert from pact v3 messaging to one SC message contract"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
label 'message sent to activemq:output'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
body([
bookName: "foo"
])
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
bodyMatchers {
jsonPath('$.bookName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3MessagingJson)
then:
contracts == expectedContracts
}
def "should fail to convert a pact v3 contract with unsupported rule logic"() {
when:
converter.convertFrom(pactv3UnsupportedRuleLogicJson)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("Currently only the AND combination rule logic is supported")
}
}
// file creator
/*
pacts.entrySet().each {
new File("target/${it.key.replace("groovy", "json")}").text = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V3))
}
*/

View File

@@ -0,0 +1,41 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
body([
someInteger: $(c(anyInteger()), p(1234567890)),
someDecimal: $(c(anyDouble()), p(123.123)),
someHex: $(c(anyHex()), p('DEADC0DE')),
someAlphaNumeric: $(c(anyAlphaNumeric()), p('Some alpha numeric string with 1234567890')),
someUUID: $(c(anyUuid()), p('00000000-0000-0000-0000-000000000000')),
someDate: $(c(anyDate()), p('2018-03-26')),
someTime: $(c(anyTime()), p('13:37:00')),
someDateTime: $(c(anyDateTime()), p('2018-03-26 13:37:00')),
someBoolean: $(c(anyBoolean()), p('true')),
someRegex: $(c(regex('[0-9]{10}')), p(1234567890))
])
headers {
contentType('application/json')
}
}
response {
status OK()
body([
someInteger: $(c(1234567890), p(anyInteger())),
someDecimal: $(c(123.123), p(anyDouble())),
someHex: $(c('DEADC0DE'), p(anyHex())),
someAlphaNumeric: $(c('Some alpha numeric string with 1234567890'), p(anyAlphaNumeric())),
someUUID: $(c('00000000-0000-0000-0000-000000000000'), p(anyUuid())),
someDate: $(c('2018-03-26'), p(anyDate())),
someTime: $(c('13:37:00'), p(anyTime())),
someDateTime: $(c('2018-03-26 13:37:00'), p(anyDateTime())),
someBoolean: $(c('true'), p(anyBoolean())),
someRegex: $(c(1234567890), p(regex('[0-9]{10}')))
])
headers {
contentType('application/json')
}
}
}

View File

@@ -0,0 +1,280 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger":{
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal":{
"type": "RandomDecimal",
"digits": 10
},
"$.someHex":{
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric":{
"type": "RandomString",
"size": 10
},
"$.someUUID":{
"type": "Uuid"
},
"$.someDate":{
"type": "Date"
},
"$.someTime":{
"type": "Time"
},
"$.someDateTime":{
"type": "DateTime"
},
"$.someBoolean":{
"type": "RandomBoolean"
},
"$.someRegex":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header": {
"Content-Type":{
"matchers":[
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.someHex":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someAlphaNumeric":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someUUID":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDate":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDateTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someBoolean":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger":{
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal":{
"type": "RandomDecimal",
"digits": 10
},
"$.someHex":{
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric":{
"type": "RandomString",
"size": 10
},
"$.someUUID":{
"type": "Uuid"
},
"$.someDate":{
"type": "Date"
},
"$.someTime":{
"type": "Time"
},
"$.someDateTime":{
"type": "DateTime"
},
"$.someBoolean":{
"type": "RandomBoolean"
},
"$.someRegex":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.someHex":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someAlphaNumeric":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someUUID":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDate":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someDateTime":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.someBoolean":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,66 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
clientId: $(c(regex('[0-9]{10}')), p("8532032713")),
loanAmount: 99999
])
headers { // (5)
contentType('application/vnd.fraud.v1+json')
}
}
response { // (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus: "FRAUD",
rejectionReason: "Amount too high"
])
headers { // (9)
contentType('application/vnd.fraud.v1+json')
}
}
}
/*
Since we don't want to force on the user to hardcode values of fields that are dynamic
(timestamps, database ids etc.), one can parametrize those entries. If you wrap your field's
value in a `$(...)` or `value(...)` and provide a dynamic value of a field then
the concrete value will be generated for you. If you want to be really explicit about
which side gets which value you can do that by using the `value(consumer(...), producer(...))` notation.
That way what's present in the `consumer` section will end up in the produced stub. What's
there in the `producer` will end up in the autogenerated test. If you provide only the
regular expression side without the concrete value then Spring Cloud Contract will generate one for you.
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*`
*/

View File

@@ -0,0 +1,104 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "8532032713",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.clientId":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.fraudCheckStatus":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
},
"$.rejectionReason":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,30 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/fraudcheck'
body("""
{
"clientId":"${value(consumer(regex('[0-9]{10}')), producer('1234567890'))}",
"loanAmount":123.123
}
"""
)
headers {
contentType("application/vnd.fraud.v1+json")
}
}
response {
status OK()
body(
fraudCheckStatus: "OK",
rejectionReason: $(consumer(null), producer(execute('assertThatRejectionReasonIsNull($it)')))
)
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}

View File

@@ -0,0 +1,96 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 123.123
},
"generators": {
"body": {
"$.clientId":{
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.clientId":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"fraudCheckStatus": "OK",
"rejectionReason": null
},
"matchingRules":{
"header":{
"Content-Type":{
"matchers":[
{
"match":"regex",
"regex":"application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine":"AND"
}
},
"body":{
"$.fraudCheckStatus":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,37 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
request {
name "should count all frauds"
method GET()
url '/frauds'
}
response {
status OK()
body([
count: 200
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
},
Contract.make {
request {
method GET()
url '/drunks'
}
response {
status OK()
body([
count: 100
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}
]

View File

@@ -0,0 +1,48 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/frauds"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"count": 200
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -0,0 +1,20 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
]

View File

@@ -0,0 +1,45 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to activemq:output",
"metaData":{
"BOOK-NAME":"foo",
"contentType":"application/json"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"bookReturnedTriggered()"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,27 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
]

View File

@@ -0,0 +1,44 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to jms:output",
"metaData":{
"BOOK-NAME":"foo"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"received message from jms:input"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,19 @@
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
]

View File

@@ -0,0 +1,29 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"assert that bookWasDeleted()",
"metaData":{
},
"providerStates":[
{
"name":"received message from jms:delete"
}
]
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,59 @@
{
"provider": {
"name": "Alice Service"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "a retrieve Mallory request",
"provider_state": "a user with username 'username' and password 'password' exists",
"request": {
"method": "GET",
"path": "/mallory",
"query": "name=ron&status=good",
"body" : {"id": "123", "method": "create"},
"headers": {
"Content-Type": "application/json"
},
"matchingRules": {
"$.body.id": {
"match": "regex",
"regex": "[0-9]{3}"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"userName": "AJQrokEGPAVdOHprQpKP"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"max": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
}
}
]
}

View File

@@ -0,0 +1,46 @@
{
"consumer": {
"name": "ui"
},
"provider": {
"name": "userservice"
},
"interactions": [
{
"description": "a request to POST a person",
"providerState": "provider accepts a new person",
"request": {
"method": "POST",
"path": "/user-service/users",
"headers": {
"Content-Type": "application/json"
},
"body": {
"firstName": "Arthur",
"lastName": "Dent"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"id": 42,
"firstName": "Arthur",
"lastName": "Dent"
},
"matchingRules": {
"$.body": {
"match": "type"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}

View File

@@ -0,0 +1,94 @@
{
"provider": {
"name": "266_provider"
},
"consumer": {
"name": "test_consumer"
},
"interactions": [
{
"description": "get all users for max",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"userName": "AJQrokEGPAVdOHprQpKP"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"max": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
},
{
"description": "get all users for min",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
[
{
"email": "DPvAfkCZpOBZWzKYiDMC",
"id": "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
"userName": "GIUlVKoiLdHLYNKGbcSy"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"min": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -0,0 +1,278 @@
{
"provider": {
"name": "test_provider_array"
},
"consumer": {
"name": "test_consumer_array"
},
"interactions": [
{
"description": "java test interaction with a DSL array body",
"request": {
"method": "GET",
"path": "/",
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
]
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"body": [
{
"dob": "07/19/2016",
"id": 8958464620,
"name": "Rogger the Dogger",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
},
{
"dob": "07/19/2016",
"id": 4143398442,
"name": "Cat in the Hat",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
}
],
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{ "match" : "regex", "regex" : "[a-zA-Z]{9}" }
]
}
},
"body": {
"$[0].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].id": {
"matchers": [
{ "match": "type" }
]
},
"$[*].nullValue": {
"matchers": [
{ "match": "null" }
]
},
"$[*].aNumber": {
"matchers": [
{ "match": "number" }
]
},
"$[*].positiveInteger": {
"matchers": [
{ "match": "integer" }
]
},
"$[*].negativeInteger": {
"matchers": [
{ "match": "integer" }
]
},
"$[*].positiveDecimalNumber": {
"matchers": [
{ "match": "decimal" }
]
},
"$[*].negativeDecimalNumber": {
"matchers": [
{ "match": "decimal" }
]
}
}
}
}
},
{
"description": "test interaction with a array body with templates",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
}
],
"matchingRules": {
"body": {
"$[2].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].id": {
"matchers": [
{ "match": "type" }
]
},
"$[2].id": {
"matchers": [
{ "match": "type" }
]
},
"$[1].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].name": {
"matchers": [
{ "match": "type" }
]
},
"$[0].dob": {
"matchers": [
{ "date": "yyyy-MM-dd" }
]
}
}
}
}
},
{
"description": "test interaction with an array like matcher",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": {
"data": {
"array1": [
{
"dob": "2016-07-19",
"id": 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
}
],
"array2": [
{
"address": "127.0.0.1",
"name": "jvxrzduZnwwxpFYrQnpd"
}
],
"array3": [
[
{
"itemCount": 652571349
}
]
]
},
"id": 7183997828
},
"matchingRules": {
"body": {
"$.data.array3[0]": {
"matchers": [
{ "max": 5, "match": "type" }
]
},
"$.data.array1": {
"matchers": [
{ "min": 0, "match": "type" }
]
},
"$.data.array2": {
"matchers": [
{ "min": 1, "match": "type" }
]
},
"$.id": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array2[*].name": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array2[*].address": {
"matchers": [
{ "regex": "(\\d{1,3}\\.)+\\d{1,3}" }
]
},
"$.data.array1[*].name": {
"matchers": [
{ "match": "type" }
]
},
"$.data.array1[*].id": {
"matchers": [
{ "match": "type" }
]
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"consumer":{
"name":"Consumer"
},
"provider":{
"name":"Provider"
},
"messages":[
{
"description":"message sent to activemq:output",
"metaData":{
"BOOK-NAME":"foo",
"contentType":"application/json"
},
"contents":{
"bookName":"foo"
},
"providerStates":[
{
"name":"bookReturnedTriggered()"
}
],
"matchingRules":{
"body":{
"$.bookName":{
"matchers":[
{
"match":"type"
}
],
"combine":"AND"
}
}
}
}
],
"metadata":{
"pact-specification":{
"version":"3.0.0"
},
"pact-jvm":{
"version":"3.5.13"
}
}
}

View File

@@ -0,0 +1,47 @@
{
"provider": {
"name": "test_unsupported_rule_logic"
},
"consumer": {
"name": "test_unsupported_rule_logic"
},
"interactions": [
{
"description": "test unsupported rule logic",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": [
{
"optionalField": 1234567890
}
],
"matchingRules": {
"body": {
"$[*].optionalField": {
"matchers": [
{ "match": "integer" },
{ "match": "null" }
],
"combine": "OR"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}