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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() + "()"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
*/
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.*`
|
||||
*/
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()')
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"some" : "json"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user