Messaging polyglot support (#1472)
added support for AMQP, KAFKA and standalone options
This commit is contained in:
committed by
GitHub
parent
c5d3456d3a
commit
a915cf102b
12
docker/spring-cloud-contract-docker/build_adocs.sh
Executable file
12
docker/spring-cloud-contract-docker/build_adocs.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
CURRENT_DIR="$( pwd )"
|
||||
ADOC_OUTPUT_DIR="${CURRENT_DIR}/target/adoc/"
|
||||
pushd project
|
||||
mkdir -p "${ADOC_OUTPUT_DIR}"
|
||||
./gradlew dumpAllProps
|
||||
cp "$( pwd )/build/props.adoc" "${ADOC_OUTPUT_DIR}/"
|
||||
cp "$( pwd )/build/appProps.adoc" "${ADOC_OUTPUT_DIR}/"
|
||||
popd
|
||||
@@ -8,15 +8,20 @@ GRADLE_WRAPPER_DIR="${HOME}/.gradle/wrapper/dists/${GRADLE_BIN_DIR}"
|
||||
CURRENT_DIR="$( pwd )"
|
||||
GRADLE_OUTPUT_DIR="${CURRENT_DIR}/target/gradle_dependencies/"
|
||||
pushd project
|
||||
rm -rf .gradle
|
||||
./gradlew wrapper --gradle-version "${WRAPPER_VERSION}"
|
||||
./gradlew clean resolveDependencies build -g "${GRADLE_OUTPUT_DIR}" -x copyOutput || echo "Expected to fail the build"
|
||||
if [ -d "${GRADLE_WRAPPER_DIR}" ]; then
|
||||
echo "Copying Gradle Wrapper version [${WRAPPER_VERSION}]"
|
||||
mkdir -p "${GRADLE_OUTPUT_DIR}/wrapper/dists/"
|
||||
cp -r "${GRADLE_WRAPPER_DIR}" "${GRADLE_OUTPUT_DIR}/wrapper/dists/"
|
||||
else
|
||||
echo "Gradle Wrapper [${GRADLE_WRAPPER_DIR}] not found. Will not copy it"
|
||||
fi
|
||||
rm -rf build
|
||||
rm -rf .gradle
|
||||
./gradlew wrapper --gradle-version "${WRAPPER_VERSION}"
|
||||
./gradlew clean resolveDependencies build -g "${GRADLE_OUTPUT_DIR}" -x copyOutput || echo "Expected to fail the build"
|
||||
if [ -d "${GRADLE_WRAPPER_DIR}" ]; then
|
||||
echo "Copying Gradle Wrapper version [${WRAPPER_VERSION}]"
|
||||
mkdir -p "${GRADLE_OUTPUT_DIR}/wrapper/dists/"
|
||||
cp -r "${GRADLE_WRAPPER_DIR}" "${GRADLE_OUTPUT_DIR}/wrapper/dists/"
|
||||
else
|
||||
echo "Gradle Wrapper [${GRADLE_WRAPPER_DIR}] not found. Will not copy it"
|
||||
fi
|
||||
popd
|
||||
|
||||
./build_adocs.sh
|
||||
|
||||
pushd project
|
||||
rm -rf build
|
||||
popd
|
||||
17
docker/spring-cloud-contract-docker/project/README.adoc
Normal file
17
docker/spring-cloud-contract-docker/project/README.adoc
Normal file
@@ -0,0 +1,17 @@
|
||||
# Spring Cloud Contract Verifier Docker Project
|
||||
|
||||
## Developer tips
|
||||
|
||||
In order to use a new environment variable inside the Gradle script, you need to add it to the `envVars` map together with a description and a default value. Otherwise, any attempt to resolve such an environment variable will lead to an exception being thrown.
|
||||
|
||||
If you're referencing any environment variables from inside the Java code please follow the following convention.
|
||||
|
||||
```java
|
||||
/**
|
||||
* Some description.
|
||||
**/
|
||||
@Value("${ENV_VAR_NAME:defaultValue}")
|
||||
String envVar;
|
||||
```
|
||||
|
||||
If you provide Javadocs, we will automatically parse any `@Value` annotated fields and build a table of environment variables with description and default values.
|
||||
@@ -1,3 +1,5 @@
|
||||
import contracts.DocsFromSources
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@@ -10,13 +12,39 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "io.spring.gradle:dependency-management-plugin:1.0.8.RELEASE"
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
|
||||
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifierVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
group = getProp("PROJECT_GROUP") ?: 'com.example'
|
||||
version = getProp("PROJECT_VERSION") ?: '0.0.1-SNAPSHOT'
|
||||
Map<String, EnvVar> envVars = [
|
||||
PROJECT_GROUP: new EnvVar("Your project's group ID", "com.example"),
|
||||
PROJECT_VERSION: new EnvVar("Your project's version", "0.0.1-SNAPSHOT"),
|
||||
PROJECT_NAME: new EnvVar("Your project's artifact id", "example"),
|
||||
STANDALONE_PROTOCOL: new EnvVar("For standalone version, which additional protocol should be added", ""),
|
||||
PRODUCER_STUBS_CLASSIFIER: new EnvVar("Archive classifier used for generated producer stubs", "stubs"),
|
||||
FAIL_ON_NO_CONTRACTS: new EnvVar("Should the build fail if there are no contracts present?", false),
|
||||
REPO_WITH_BINARIES_URL: new EnvVar("URL of your Artifact Manager (defaults to the default URL of https://jfrog.com/artifactory/[Artifactory] when running locally)", "http://localhost:8081/artifactory/libs-release-local"),
|
||||
REPO_WITH_BINARIES_USERNAME: new EnvVar("(optional) Username when the Artifact Manager is secured", "admin"),
|
||||
REPO_WITH_BINARIES_PASSWORD: new EnvVar("(optional) Password when the Artifact Manager is secured", "password"),
|
||||
PUBLISH_ARTIFACTS: new EnvVar("If set to `true`, publishes the artifact to binary storage", "true"),
|
||||
PUBLISH_ARTIFACTS_OFFLINE: new EnvVar("If set to `true`, publishes the artifacts to local m2", "false"),
|
||||
EXTERNAL_CONTRACTS_GROUP_ID: new EnvVar("Group ID of the project with contracts", "com.example"),
|
||||
EXTERNAL_CONTRACTS_ARTIFACT_ID: new EnvVar("Artifact ID of the project with contracts", ""),
|
||||
EXTERNAL_CONTRACTS_CLASSIFIER: new EnvVar("Classifier of the project with contracts", ""),
|
||||
EXTERNAL_CONTRACTS_VERSION: new EnvVar("Version of the project with contracts. Defautls to an equivalent of picking the latest", "+"),
|
||||
EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL: new EnvVar("URL of your Artifact Manager. It defaults to the value of `REPO_WITH_BINARIES_URL` environment variable and if that is not set, it defaults to `http://localhost:8081/artifactory/libs-release-local`", ""),
|
||||
EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_USERNAME: new EnvVar("(optional) Username if the `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL` requires authentication. It defaults to `REPO_WITH_BINARIES_USERNAME`. If that is not set, it defaults to `admin", ""),
|
||||
EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_PASSWORD: new EnvVar("(optional) Password if the `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL` requires authentication. It defaults to `REPO_WITH_BINARIES_PASSWORD, If that is not set, it defaults to `password", ""),
|
||||
EXTERNAL_CONTRACTS_PATH: new EnvVar("Path to contracts for the given project, inside the project with contracts. Defaults to slash-separated `EXTERNAL_CONTRACTS_GROUP_ID` concatenated with `/` and `EXTERNAL_CONTRACTS_ARTIFACT_ID. For example,\n" +
|
||||
"for group id `cat-server-side.dog` and artifact ID `fish`, would result in `cat/dog/fish` for the contracts path.", ""),
|
||||
EXTERNAL_CONTRACTS_WORK_OFFLINE: new EnvVar("If set to `true`, retrieves the artifact with contracts from the container's `.m2`. Mount your local `.m2` as a volume available at the container's `/root/.m2` path", "false"),
|
||||
PUBLISH_STUBS_TO_SCM: new EnvVar("If set to `true` will run the task to publish stubs to scm", false),
|
||||
MESSAGING_TYPE: new EnvVar("Type of messaging. Can be either [rabbit] or [kafka].", ""),
|
||||
]
|
||||
|
||||
group = getProp(envVars, "PROJECT_GROUP") ?: 'com.example'
|
||||
version = getProp(envVars, "PROJECT_VERSION") ?: '0.0.1-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@@ -29,21 +57,69 @@ repositories {
|
||||
}
|
||||
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
apply plugin: "io.spring.dependency-management"
|
||||
apply plugin: "org.springframework.boot"
|
||||
apply plugin: 'spring-cloud-contract'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}"
|
||||
bootJar.enabled = false
|
||||
|
||||
class EnvVar {
|
||||
final Object defaultValue
|
||||
final String description
|
||||
|
||||
EnvVar(String description, Object defaultValue) {
|
||||
this.defaultValue = defaultValue
|
||||
this.description = description
|
||||
}
|
||||
|
||||
boolean equals(o) {
|
||||
if (this.is(o)) {
|
||||
return true
|
||||
}
|
||||
if (getClass() != o.class) {
|
||||
return false
|
||||
}
|
||||
EnvVar envVar = (EnvVar) o
|
||||
if (defaultValue != envVar.defaultValue) {
|
||||
return false
|
||||
}
|
||||
if (description != envVar.description) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
int hashCode() {
|
||||
int result
|
||||
result = (defaultValue != null ? defaultValue.hashCode() : 0)
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(platform("org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}"))
|
||||
testImplementation(platform("org.apache.camel.springboot:camel-spring-boot-dependencies:${camelVersion}"))
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-web")
|
||||
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
|
||||
testImplementation("org.springframework.amqp:spring-rabbit")
|
||||
testImplementation("org.apache.camel.springboot:camel-spring-boot-starter")
|
||||
testImplementation("org.apache.camel.springboot:camel-kafka-starter")
|
||||
testImplementation("org.apache.camel.springboot:camel-rabbitmq-starter")
|
||||
if (getProp(envVars, "STANDALONE_PROTOCOL")) {
|
||||
testImplementation("org.apache.camel.springboot:camel-${getProp(envVars, "STANDALONE_PROTOCOL")}-starter")
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
if (getProp(envVars, "MESSAGING_TYPE") != "") {
|
||||
systemProperty("spring.profiles.active", "messagingtype")
|
||||
} else if (getProp(envVars, "STANDALONE_PROTOCOL") != "") {
|
||||
systemProperty("spring.profiles.active", "standalone")
|
||||
}
|
||||
testLogging {
|
||||
exceptionFormat = 'full'
|
||||
afterSuite { desc, result ->
|
||||
@@ -58,32 +134,33 @@ test {
|
||||
}
|
||||
|
||||
contracts {
|
||||
baseClassForTests = "contracts.RestBase"
|
||||
baseClassForTests = "contracts.ContractTestsBase"
|
||||
testMode = "EXPLICIT"
|
||||
stubsSuffix = getProp("PRODUCER_STUBS_CLASSIFIER") ?: "stubs"
|
||||
if (getProp("EXTERNAL_CONTRACTS_ARTIFACT_ID")) {
|
||||
stubsSuffix = getProp(envVars, "PRODUCER_STUBS_CLASSIFIER") ?: "stubs"
|
||||
failOnNoContracts = getProp(envVars, "FAIL_ON_NO_CONTRACTS") ?: false
|
||||
if (getProp(envVars, "EXTERNAL_CONTRACTS_ARTIFACT_ID")) {
|
||||
logger.
|
||||
lifecycle("Will use an artifact with contracts [${getProp("EXTERNAL_CONTRACTS_GROUP_ID")}:${getProp("EXTERNAL_CONTRACTS_ARTIFACT_ID")}]")
|
||||
lifecycle("Will use an artifact with contracts [${getProp(envVars, "EXTERNAL_CONTRACTS_GROUP_ID")}:${getProp(envVars, "EXTERNAL_CONTRACTS_ARTIFACT_ID")}]")
|
||||
// tests - contracts from an artifact
|
||||
contractsPath = getProp("EXTERNAL_CONTRACTS_PATH") ?: ""
|
||||
if (Boolean.parseBoolean(getProp("EXTERNAL_CONTRACTS_WORK_OFFLINE")) == false) {
|
||||
contractsPath = getProp(envVars, "EXTERNAL_CONTRACTS_PATH") ?: ""
|
||||
if (!Boolean.parseBoolean(getProp(envVars, "EXTERNAL_CONTRACTS_WORK_OFFLINE").toString())) {
|
||||
contractRepository {
|
||||
repositoryUrl = getProp('EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL') ?:
|
||||
getProp('REPO_WITH_BINARIES_URL') ?: 'http://localhost:8081/artifactory/libs-release-local'
|
||||
username = getProp('EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_USERNAME') ?:
|
||||
getProp('REPO_WITH_BINARIES_USERNAME') ?: 'admin'
|
||||
password = getProp('EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_PASSWORD') ?:
|
||||
getProp('REPO_WITH_BINARIES_PASSWORD') ?: 'password'
|
||||
repositoryUrl = getProp(envVars, 'EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL') ?:
|
||||
getProp(envVars, 'REPO_WITH_BINARIES_URL') ?: 'http://localhost:8081/artifactory/libs-release-local'
|
||||
username = getProp(envVars, 'EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_USERNAME') ?:
|
||||
getProp(envVars, 'REPO_WITH_BINARIES_USERNAME') ?: 'admin'
|
||||
password = getProp(envVars, 'EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_PASSWORD') ?:
|
||||
getProp(envVars, 'REPO_WITH_BINARIES_PASSWORD') ?: 'password'
|
||||
}
|
||||
}
|
||||
contractDependency {
|
||||
groupId = getProp("EXTERNAL_CONTRACTS_GROUP_ID") ?: "com.example"
|
||||
artifactId = getProp("EXTERNAL_CONTRACTS_ARTIFACT_ID")
|
||||
delegate.classifier = getProp("EXTERNAL_CONTRACTS_CLASSIFIER") ?: ""
|
||||
delegate.version = getProp("EXTERNAL_CONTRACTS_VERSION") ?: "+"
|
||||
groupId = getProp(envVars, "EXTERNAL_CONTRACTS_GROUP_ID") ?: "com.example"
|
||||
artifactId = getProp(envVars, "EXTERNAL_CONTRACTS_ARTIFACT_ID")
|
||||
delegate.classifier = getProp(envVars, "EXTERNAL_CONTRACTS_CLASSIFIER") ?: ""
|
||||
delegate.version = getProp(envVars, "EXTERNAL_CONTRACTS_VERSION") ?: "+"
|
||||
}
|
||||
contractsMode = Boolean.
|
||||
parseBoolean(getProp("EXTERNAL_CONTRACTS_WORK_OFFLINE")) ? "LOCAL" : "REMOTE"
|
||||
parseBoolean(getProp(envVars, "EXTERNAL_CONTRACTS_WORK_OFFLINE").toString()) ? "LOCAL" : "REMOTE"
|
||||
}
|
||||
else {
|
||||
logger.lifecycle("Will use contracts from the mounted [/contracts] folder")
|
||||
@@ -107,16 +184,15 @@ task copyOutput(type: Copy) {
|
||||
|
||||
test {
|
||||
finalizedBy("copyOutput")
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
url getProp('REPO_WITH_BINARIES_URL') ?: 'http://localhost:8081/artifactory/libs-release-local'
|
||||
url getProp(envVars, 'REPO_WITH_BINARIES_URL') ?: 'http://localhost:8081/artifactory/libs-release-local'
|
||||
credentials {
|
||||
username getProp('REPO_WITH_BINARIES_USERNAME') ?: 'admin'
|
||||
password getProp('REPO_WITH_BINARIES_PASSWORD') ?: 'password'
|
||||
username getProp(envVars, 'REPO_WITH_BINARIES_USERNAME').toString() ?: 'admin'
|
||||
password getProp(envVars, 'REPO_WITH_BINARIES_PASSWORD').toString() ?: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,8 +201,8 @@ publishing {
|
||||
}
|
||||
|
||||
// explicitly disable artifacts publication
|
||||
boolean publishEnabled = Boolean.parseBoolean(getProp("PUBLISH_ARTIFACTS") ?: "true")
|
||||
boolean publishOffline = Boolean.parseBoolean(getProp("PUBLISH_ARTIFACTS_OFFLINE") ?: "false")
|
||||
boolean publishEnabled = Boolean.parseBoolean(getProp(envVars, "PUBLISH_ARTIFACTS").toString() ?: "true")
|
||||
boolean publishOffline = Boolean.parseBoolean(getProp(envVars, "PUBLISH_ARTIFACTS_OFFLINE").toString() ?: "false")
|
||||
publish.setEnabled(publishEnabled)
|
||||
publishToMavenLocal.setEnabled(publishOffline)
|
||||
|
||||
@@ -138,14 +214,17 @@ gradle.taskGraph.whenReady { graph ->
|
||||
findAll { it.name.startsWith("publish") && it.name.endsWith("ToMavenLocal") }*.setEnabled(publishOffline)
|
||||
}
|
||||
|
||||
if (Boolean.parseBoolean(getProp("PUBLISH_STUBS_TO_SCM"))) {
|
||||
if (Boolean.parseBoolean(getProp(envVars, "PUBLISH_STUBS_TO_SCM").toString())) {
|
||||
publish.dependsOn("publishStubsToScm")
|
||||
}
|
||||
|
||||
String getProp(String propName) {
|
||||
Object getProp(Map<String, EnvVar> envVars, String propName) {
|
||||
if (!envVars.containsKey(propName)) {
|
||||
throw new IllegalStateException("You've referenced a property with name [${propName}] but it's not in the list of accepatble props ${envVars.keySet()}")
|
||||
}
|
||||
return hasProperty(propName) ?
|
||||
(getProperty(propName) ?: System.properties[propName]) : System.properties[propName] ?:
|
||||
System.getenv(propName)
|
||||
System.getenv(propName) ?: envVars.get(propName)?.defaultValue
|
||||
}
|
||||
|
||||
task resolveDependencies {
|
||||
@@ -157,8 +236,27 @@ task resolveDependencies {
|
||||
config.files
|
||||
}
|
||||
catch (e) {
|
||||
project.logger.info e.message // some cannot be resolved, silentlyish skip them
|
||||
project.logger.info e.message // some cannot be resolved, silently skip them
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task dumpAllProps() {
|
||||
doLast {
|
||||
// TODO: Parse the java code for env vars
|
||||
File output = new File("build", "props.adoc")
|
||||
if (!output.exists()) {
|
||||
output.parentFile.mkdirs()
|
||||
output.createNewFile()
|
||||
}
|
||||
String table = """\
|
||||
.Docker environment variables
|
||||
|===
|
||||
|Name | Description | Default
|
||||
"""
|
||||
output.text = table + envVars.sort().collect { '|' + it.key + '|' + it.value.description + '|' + it.value.defaultValue }.join("\n") + "\n|==="
|
||||
|
||||
new DocsFromSources(project).buildApplicationEnvVars()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
maven { url "https://repo.spring.io/release" }
|
||||
}
|
||||
|
||||
ext {
|
||||
roasterVersion = "2.21.2.Final"
|
||||
junit5Version = "5.6.2"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jboss.forge.roaster:roaster-api:${roasterVersion}"
|
||||
implementation "org.jboss.forge.roaster:roaster-jdt:${roasterVersion}"
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.jboss.forge.roaster.Roaster;
|
||||
import org.jboss.forge.roaster.model.JavaUnit;
|
||||
import org.jboss.forge.roaster.model.source.AnnotationSource;
|
||||
import org.jboss.forge.roaster.model.source.FieldSource;
|
||||
import org.jboss.forge.roaster.model.source.JavaClassSource;
|
||||
import org.jboss.forge.roaster.model.source.JavaDocSource;
|
||||
|
||||
public class DocsFromSources {
|
||||
|
||||
private static final String ADOC_HEADER =
|
||||
".Docker environment variables - read at runtime\n"
|
||||
+ "|===\n"
|
||||
+ "|Name | Description | Default\n";
|
||||
|
||||
private final Project project;
|
||||
|
||||
public DocsFromSources(Project project) {
|
||||
this.project = project;
|
||||
}
|
||||
|
||||
public void buildApplicationEnvVars() {
|
||||
Path path = new File(rootDir(), sourcePath()).toPath();
|
||||
List<EnvVar> envVars = new ArrayList<>();
|
||||
FileVisitor<Path> fv = new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
if (!file.toString().endsWith(".java")) {
|
||||
info("Skipping [" + file.toString() + "]");
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
JavaUnit unit = Roaster.parseUnit(Files.newInputStream(file));
|
||||
JavaClassSource myClass = unit.getGoverningType();
|
||||
info("Checking [" + myClass.getName() + "]");
|
||||
List<FieldSource<JavaClassSource>> fields = myClass.getFields();
|
||||
for (FieldSource<JavaClassSource> field : fields) {
|
||||
List<AnnotationSource<JavaClassSource>> annotations = field.getAnnotations();
|
||||
for (AnnotationSource<JavaClassSource> annotation : annotations) {
|
||||
if ("org.springframework.beans.factory.annotation.Value".equals(annotation.getQualifiedName())) {
|
||||
info("Field [" + field.getName() + "] has @Value annotation");
|
||||
JavaDocSource<FieldSource<JavaClassSource>> javaDoc = field.getJavaDoc();
|
||||
String description = javaDoc.getFullText();
|
||||
// ${foo:asd}
|
||||
String annotationValue = annotation.getStringValue();
|
||||
// foo:asd
|
||||
annotationValue = annotationValue.substring(2, annotationValue.length() - 1);
|
||||
String[] parsed = annotationValue.split(":");
|
||||
String defaultValue = "";
|
||||
String name = parsed[0];
|
||||
if (parsed.length == 2) {
|
||||
defaultValue = parsed[1];
|
||||
}
|
||||
envVars.add(new EnvVar(name, description, defaultValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
info("Found [" + envVars.size() + "] env var field entries");
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
Files.walkFileTree(path, fv);
|
||||
Path output = new File(rootDir(), "build/appProps.adoc").toPath();
|
||||
StringBuilder stringBuilder = new StringBuilder().append(ADOC_HEADER);
|
||||
Collections.sort(envVars);
|
||||
envVars.forEach(envVar -> stringBuilder.append(envVar.toString()).append("\n"));
|
||||
stringBuilder.append("|===");
|
||||
Files.write(output, stringBuilder.toString().getBytes());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
String sourcePath() {
|
||||
return "src/test/java/contracts";
|
||||
}
|
||||
|
||||
File rootDir() {
|
||||
return project.getRootDir();
|
||||
}
|
||||
|
||||
void info(String log) {
|
||||
this.project.getLogger().info(log);
|
||||
}
|
||||
}
|
||||
|
||||
class EnvVar implements Comparable<EnvVar> {
|
||||
final String name;
|
||||
final String description;
|
||||
final String defaultValue;
|
||||
|
||||
EnvVar(String name, String description, String defaultValue) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
EnvVar envVar = (EnvVar) o;
|
||||
return Objects.equals(name, envVar.name) &&
|
||||
Objects.equals(description, envVar.description) &&
|
||||
Objects.equals(defaultValue, envVar.defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, description, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(EnvVar o) {
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "|" + name + "|" + description + "|" + defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
org.gradle.daemon=false
|
||||
verifierVersion=3.0.0-SNAPSHOT
|
||||
springBootVersion=2.4.0-SNAPSHOT
|
||||
camelVersion=3.4.3
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.camel.StandaloneMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@SpringBootTest(classes = ContractTestsBase.Config.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@AutoConfigureMessageVerifier
|
||||
public abstract class ContractTestsBase {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ContractTestsBase.class);
|
||||
|
||||
/**
|
||||
* URL at which the application is running.
|
||||
*/
|
||||
@Value("${APPLICATION_BASE_URL}")
|
||||
String url;
|
||||
|
||||
/**
|
||||
* Optional username to access the application.
|
||||
*/
|
||||
@Value("${APPLICATION_USERNAME:}")
|
||||
String username;
|
||||
|
||||
/**
|
||||
* Optional password to access the application.
|
||||
*/
|
||||
@Value("${APPLICATION_PASSWORD:}")
|
||||
String password;
|
||||
|
||||
/**
|
||||
* Timeout to connect to the application to trigger a message.
|
||||
*/
|
||||
@Value("${MESSAGING_TRIGGER_CONNECT_TIMEOUT:5000}")
|
||||
Integer connectTimeout;
|
||||
|
||||
/**
|
||||
* Timeout to read the response from the application to trigger a message.
|
||||
*/
|
||||
@Value("${MESSAGING_TRIGGER_READ_TIMEOUT:5000}")
|
||||
Integer readTimeout;
|
||||
|
||||
/**
|
||||
* Defines the messaging type when dealing with message based contracts.
|
||||
*/
|
||||
@Value("${MESSAGING_TYPE:}")
|
||||
String messagingType;
|
||||
|
||||
@Autowired
|
||||
MessageVerifier messageVerifier;
|
||||
|
||||
@BeforeEach
|
||||
public void setup(TestInfo testInfo) {
|
||||
RestAssured.baseURI = this.url;
|
||||
if (StringUtils.hasText(this.username)) {
|
||||
RestAssured.authentication = RestAssured.basic(this.username, this.password);
|
||||
}
|
||||
setupMessagingFromContract(testInfo);
|
||||
}
|
||||
|
||||
private void setupMessagingFromContract(TestInfo testInfo) {
|
||||
try {
|
||||
YamlContract contract = ContractVerifierUtil.contract(this, testInfo.getDisplayName());
|
||||
setupMessagingIfPresent(contract);
|
||||
} catch (Exception e) {
|
||||
log.warn("An exception occurred while trying to setup messaging from contract", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupMessagingIfPresent(YamlContract contract) {
|
||||
if (contract.input == null && contract.outputMessage == null) {
|
||||
return;
|
||||
}
|
||||
setupAmqpIfPresent(contract);
|
||||
setupStandaloneIfPresent(contract);
|
||||
}
|
||||
|
||||
private void setupAmqpIfPresent(YamlContract contract) {
|
||||
AmqpMetadata amqpMetadata = AmqpMetadata.fromMetadata(contract.metadata);
|
||||
if (isMessagingType("rabbit") && hasDeclaredOutputQueue(amqpMetadata) || isMessagingType("kafka")) {
|
||||
log.info("First will try to receive a message to setup the connection with the broker");
|
||||
if (contract.input != null && StringUtils.hasText(contract.input.messageFrom)) {
|
||||
setupConnection(contract.input.messageFrom, contract);
|
||||
}
|
||||
if (contract.outputMessage != null && StringUtils.hasText(contract.outputMessage.sentTo)){
|
||||
setupConnection(contract.outputMessage.sentTo, contract);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupStandaloneIfPresent(YamlContract contract) {
|
||||
StandaloneMetadata metadata = StandaloneMetadata.fromMetadata(contract.metadata);
|
||||
if (StringUtils.hasText(metadata.getSetup().getOptions())) {
|
||||
log.info("First will try to receive a message to setup the connection with the broker");
|
||||
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.SETUP);
|
||||
setupConnection(metadata.getSetup().getOptions(), contract);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMessageType(YamlContract contract,
|
||||
ContractVerifierMessageMetadata.MessageType output) {
|
||||
contract.metadata.put(ContractVerifierMessageMetadata.METADATA_KEY,
|
||||
new ContractVerifierMessageMetadata(output));
|
||||
}
|
||||
|
||||
private void setupConnection(String destination, YamlContract contract) {
|
||||
if (StringUtils.isEmpty(destination)) {
|
||||
return;
|
||||
}
|
||||
log.info("Setting up destination [{}]", destination);
|
||||
this.messageVerifier.receive(destination, 100, TimeUnit.MILLISECONDS, contract);
|
||||
}
|
||||
|
||||
private boolean hasDeclaredOutputQueue(AmqpMetadata amqpMetadata) {
|
||||
return StringUtils.hasText(amqpMetadata.getOutputMessage().getConnectToBroker().getDeclareQueueWithName());
|
||||
}
|
||||
|
||||
private boolean isMessagingType(String rabbit) {
|
||||
return rabbit.equalsIgnoreCase(this.messagingType);
|
||||
}
|
||||
|
||||
public void triggerMessage(String label) {
|
||||
String url = this.url + "/springcloudcontract/" + label;
|
||||
log.info("Will send a request to [{}] in order to trigger a message", url);
|
||||
restTemplate().postForObject(url, "", String.class);
|
||||
}
|
||||
|
||||
private RestTemplate restTemplate() {
|
||||
RestTemplateBuilder builder = new RestTemplateBuilder()
|
||||
.setConnectTimeout(Duration.ofMillis(this.connectTimeout))
|
||||
.setReadTimeout(Duration.ofMillis(this.readTimeout));
|
||||
if (StringUtils.hasText(this.username)) {
|
||||
builder = builder.basicAuthentication(this.username, this.password);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(MessagingAutoConfig.class)
|
||||
@EnableAutoConfiguration
|
||||
protected static class Config {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.camel.ConsumerTemplate;
|
||||
import org.apache.camel.Exchange;
|
||||
import org.apache.camel.Message;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
|
||||
import org.springframework.cloud.contract.verifier.messaging.kafka.KafkaMetadata;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty("MESSAGING_TYPE")
|
||||
@Profile("messagingtype")
|
||||
public class MessagingAutoConfig {
|
||||
|
||||
/**
|
||||
* Type of messaging. Can be either [rabbit] or [kafka].
|
||||
*/
|
||||
@Value("${MESSAGING_TYPE:}")
|
||||
String messagingType;
|
||||
|
||||
/**
|
||||
* For RabbitMQ - brokers addresses.
|
||||
*/
|
||||
@Value("${SPRING_RABBITMQ_ADDRESSES:}")
|
||||
String springRabbitmqAddresses;
|
||||
|
||||
/**
|
||||
* For Kafka - brokers addresses.
|
||||
*/
|
||||
@Value("${SPRING_KAFKA_BOOTSTRAP_SERVERS:}")
|
||||
String springKafkaBootstrapServers;
|
||||
|
||||
@Bean
|
||||
public ContractVerifierMessaging<Message> contractVerifierMessaging(
|
||||
MessageVerifier<Message> exchange) {
|
||||
return new ContractVerifierCamelHelper(exchange);
|
||||
}
|
||||
|
||||
@Bean
|
||||
MessageVerifier<Message> manualMessageVerifier(ConsumerTemplate consumerTemplate) {
|
||||
return new MessageVerifier<Message>() {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(MessageVerifier.class);
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit, YamlContract yamlContract) {
|
||||
String uri = messagingType() + "://" + destination + additionalOptions(yamlContract);
|
||||
log.info("Camel URI [{}]", uri);
|
||||
Exchange exchange = consumerTemplate.receive(uri, timeUnit.toMillis(timeout));
|
||||
if (exchange == null) {
|
||||
return null;
|
||||
}
|
||||
return exchange.getMessage();
|
||||
}
|
||||
|
||||
private String messagingType() {
|
||||
if (messagingType.equalsIgnoreCase("kafka")) {
|
||||
return "kafka";
|
||||
}
|
||||
return "rabbitmq";
|
||||
}
|
||||
|
||||
private String additionalOptions(YamlContract contract) {
|
||||
if (contract == null) {
|
||||
return "";
|
||||
}
|
||||
if (messagingType.equalsIgnoreCase("kafka")) {
|
||||
return setKafkaOpts(contract);
|
||||
}
|
||||
return setRabbitOpts(contract);
|
||||
}
|
||||
|
||||
private String setKafkaOpts(YamlContract contract) {
|
||||
String opts = defaultOpts(contract);
|
||||
KafkaMetadata metadata = KafkaMetadata.fromMetadata(contract.metadata);
|
||||
ContractVerifierMessageMetadata messageMetadata = ContractVerifierMessageMetadata.fromMetadata(contract.metadata);
|
||||
if (inputMessage(messageMetadata) && StringUtils.hasText(metadata.getInput().getConnectToBroker().getAdditionalOptions())) {
|
||||
return opts + "&" + metadata.getInput().getConnectToBroker().getAdditionalOptions();
|
||||
}
|
||||
else if (StringUtils.hasText(metadata.getOutputMessage().getConnectToBroker().getAdditionalOptions())) {
|
||||
return opts + "&" + metadata.getOutputMessage().getConnectToBroker().getAdditionalOptions();
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
private String defaultOpts(YamlContract contract) {
|
||||
String consumerGroup = sameConsumerGroupForSameContract(contract);
|
||||
return "?brokers=" + getRequiredProperty("SPRING_KAFKA_BOOTSTRAP_SERVERS", springKafkaBootstrapServers) + "&autoOffsetReset=latest&groupId=" + consumerGroup + "&shutdownTimeout=5";
|
||||
}
|
||||
|
||||
private String sameConsumerGroupForSameContract(YamlContract contract) {
|
||||
return contract.input.hashCode() + "_" + contract.outputMessage.hashCode();
|
||||
}
|
||||
|
||||
private String setRabbitOpts(YamlContract contract) {
|
||||
String opts = "?addresses=" + getRequiredProperty("SPRING_RABBITMQ_ADDRESSES", springRabbitmqAddresses);
|
||||
AmqpMetadata metadata = AmqpMetadata.fromMetadata(contract.metadata);
|
||||
ContractVerifierMessageMetadata messageMetadata = ContractVerifierMessageMetadata.fromMetadata(contract.metadata);
|
||||
if (inputMessage(messageMetadata) && StringUtils.hasText(metadata.getInput().getConnectToBroker().getAdditionalOptions())) {
|
||||
return opts + "&" + metadata.getInput().getConnectToBroker().getAdditionalOptions();
|
||||
}
|
||||
else if (StringUtils.hasText(metadata.getOutputMessage().getConnectToBroker().getAdditionalOptions())) {
|
||||
return opts + "&" + metadata.getOutputMessage().getConnectToBroker().getAdditionalOptions();
|
||||
}
|
||||
return defaultOpts(opts, metadata, messageMetadata);
|
||||
}
|
||||
|
||||
private String getRequiredProperty(String name, String value) {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
throw new IllegalStateException("The property [" + name + "] must not be empty!");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean inputMessage(ContractVerifierMessageMetadata messageMetadata) {
|
||||
return messageMetadata.getMessageType() == ContractVerifierMessageMetadata.MessageType.INPUT;
|
||||
}
|
||||
|
||||
private String defaultOpts(String opts, AmqpMetadata amqpMetadata, ContractVerifierMessageMetadata messageMetadata) {
|
||||
AmqpMetadata.ConnectToBroker connectToBroker = inputMessage(messageMetadata) ? amqpMetadata.getInput().getConnectToBroker() : amqpMetadata.getOutputMessage().getConnectToBroker();
|
||||
MessageProperties messageProperties = inputMessage(messageMetadata) ? amqpMetadata.getInput().getMessageProperties() : amqpMetadata.getOutputMessage().getMessageProperties();
|
||||
if (StringUtils.hasText(connectToBroker.getDeclareQueueWithName())) {
|
||||
opts = opts + "&queue=" + connectToBroker.getDeclareQueueWithName();
|
||||
}
|
||||
if (messageProperties != null && StringUtils.hasText(messageProperties.getReceivedRoutingKey())) {
|
||||
opts = opts + "&routingKey=" + messageProperties.getReceivedRoutingKey();
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, YamlContract yamlContract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, yamlContract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message message, String destination, YamlContract yamlContract) {
|
||||
throw new UnsupportedOperationException("Currently supports only receiving");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination, YamlContract yamlContract) {
|
||||
throw new UnsupportedOperationException("Currently supports only receiving");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
|
||||
|
||||
ContractVerifierCamelHelper(MessageVerifier<Message> exchange) {
|
||||
super(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContractVerifierMessage convert(Message receive) {
|
||||
if (receive == null) {
|
||||
return null;
|
||||
}
|
||||
return new ContractVerifierMessage(receive.getBody(), receive.getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@SpringBootTest(classes = RestBase.Config.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public abstract class RestBase {
|
||||
|
||||
@Value("${APPLICATION_BASE_URL}")
|
||||
String url;
|
||||
|
||||
@Value("${APPLICATION_USERNAME:}")
|
||||
String username;
|
||||
|
||||
@Value("${APPLICATION_PASSWORD:}")
|
||||
String password;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
RestAssured.baseURI = this.url;
|
||||
if (StringUtils.hasText(this.username)) {
|
||||
RestAssured.authentication = RestAssured.basic(this.username, this.password);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
protected static class Config {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto Configuration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
contracts.MessagingAutoConfig
|
||||
@@ -0,0 +1 @@
|
||||
stubrunner.camel.enabled: false
|
||||
@@ -14,4 +14,5 @@ if [[ "${MESSAGING_TYPE}" != "" ]]; then
|
||||
ADDITIONAL_OPTS="${ADDITIONAL_OPTS} --thin.profile=${MESSAGING_TYPE}"
|
||||
fi
|
||||
|
||||
echo "Please wait for the dependencies to be downloaded..."
|
||||
java -Djava.security.egd=file:/dev/./urandom -jar /stub-runner-boot.jar ${ADDITIONAL_OPTS}
|
||||
|
||||
20
docs/pom.xml
20
docs/pom.xml
@@ -44,7 +44,7 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>schema-generation</id>
|
||||
<id>resource-generation</id>
|
||||
<phase>generate-test-resources</phase>
|
||||
<goals>
|
||||
<goal>test</goal>
|
||||
@@ -77,6 +77,11 @@
|
||||
<version>${jackson-module-jsonSchema.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-amqp</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
<profile>
|
||||
@@ -98,6 +103,19 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-docker-env-vars</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<workingDirectory>${maven.multiModuleProjectDirectory}/docker/spring-cloud-contract-docker</workingDirectory>
|
||||
<executable>./build_adocs.sh</executable>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
|
||||
@@ -442,7 +442,7 @@ both for HTTP and messaging.
|
||||
==== Metadata
|
||||
|
||||
You can add `metadata` to your contract. Via the metadata you can pass in configuration to extensions. Below you can find
|
||||
an example of using the `wiremock` key and value being WireMock's `StubMapping` object. Spring Cloud Contract is able to
|
||||
an example of using the `wiremock` key. Its value is a map whose key is `stubMapping` and value being WireMock's `StubMapping` object. Spring Cloud Contract is able to
|
||||
patch parts of your generated stub mapping with your custom code. You may want to do that in order to add webhooks, custom
|
||||
delays or integrate with third party WireMock extensions.
|
||||
|
||||
@@ -472,6 +472,8 @@ include::{contract_kotlin_spec_path}/src/test/kotlin/org/springframework/cloud/c
|
||||
----
|
||||
====
|
||||
|
||||
You can check the <<contract-metadata-examples>> section for examples of what we support in the metadata section.
|
||||
|
||||
[[features-http]]
|
||||
== Contracts for HTTP
|
||||
|
||||
@@ -2006,3 +2008,10 @@ name of `scenario1` and the three following steps:
|
||||
|
||||
You can find more details about WireMock scenarios at
|
||||
https://wiremock.org/docs/stateful-behaviour/[https://wiremock.org/docs/stateful-behaviour/].
|
||||
|
||||
[[contract-metadata-examples]]
|
||||
=== Contract Metadata Examples
|
||||
|
||||
In the following sections you can find examples of the supported metadata entries.
|
||||
|
||||
include::{project-root}/docs/target/metadata.adoc[indent=0]
|
||||
@@ -76,54 +76,14 @@ The Docker image requires some environment variables to point to
|
||||
your running application, to the Artifact manager instance, and so on.
|
||||
The following list describes the environment variables:
|
||||
|
||||
- `PROJECT_GROUP`: Your project's group ID. Defaults to `com.example`.
|
||||
- `PROJECT_VERSION`: Your project's version. Defaults to `0.0.1-SNAPSHOT`.
|
||||
- `PROJECT_NAME`: Your project's artifact id. Defaults to `example`.
|
||||
- `PRODUCER_STUBS_CLASSIFIER`: Archive classifier used for generated producer stubs. Defaults to `stubs`.
|
||||
- `REPO_WITH_BINARIES_URL`: URL of your Artifact Manager. Defaults to `http://localhost:8081/artifactory/libs-release-local`,
|
||||
which is the default URL of https://jfrog.com/artifactory/[Artifactory] when running locally.
|
||||
- `REPO_WITH_BINARIES_USERNAME`: (optional) Username when the Artifact Manager is secured. Defaults to `admin`.
|
||||
- `REPO_WITH_BINARIES_PASSWORD`: (optional) Password when the Artifact Manager is secured. Defaults to `password`.
|
||||
- `PUBLISH_ARTIFACTS`: If set to `true`, publishes the artifact to binary storage. Defaults to `true`.
|
||||
- `PUBLISH_ARTIFACTS_OFFLINE`: If set to `true`, publishes the artifacts to local `.m2`. Defaults to `false`.
|
||||
|
||||
The following environment variables are used when contracts are in an external repository. To enable
|
||||
this feature, you must set the `EXTERNAL_CONTRACTS_ARTIFACT_ID` environment variable.
|
||||
|
||||
- `EXTERNAL_CONTRACTS_GROUP_ID`: Group ID of the project with contracts. Defaults to `com.example`
|
||||
- `EXTERNAL_CONTRACTS_ARTIFACT_ID`: Artifact ID of the project with contracts.
|
||||
- `EXTERNAL_CONTRACTS_CLASSIFIER`: Classifier of the project with contracts. Empty by default.
|
||||
- `EXTERNAL_CONTRACTS_VERSION`: Version of the project with contracts. Defaults to `+`, equivalent to picking the latest.
|
||||
- `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL`: URL of your Artifact Manager. It defaults to
|
||||
the value of `REPO_WITH_BINARIES_URL` environment variable.
|
||||
If that is not set, it defaults to `http://localhost:8081/artifactory/libs-release-local`,
|
||||
which is the default URL of https://jfrog.com/artifactory/[Artifactory] when running locally.
|
||||
- `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_USERNAME`: (optional) Username if the `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL`
|
||||
requires authentication. It defaults to `REPO_WITH_BINARIES_USERNAME`. If that is not set, it defaults to `admin`.
|
||||
- `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_PASSWORD`: (optional) Password if the `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL`
|
||||
requires authentication. It defaults to `REPO_WITH_BINARIES_PASSWORD`. If that is not set, it defaults to `password`.
|
||||
- `EXTERNAL_CONTRACTS_PATH`: Path to contracts for the given project, inside the project with contracts.
|
||||
Defaults to slash-separated `EXTERNAL_CONTRACTS_GROUP_ID` concatenated with `/` and `EXTERNAL_CONTRACTS_ARTIFACT_ID`. For example,
|
||||
for group id `cat-server-side.dog` and artifact ID `fish`, would result in `cat/dog/fish` for the contracts path.
|
||||
- `EXTERNAL_CONTRACTS_WORK_OFFLINE`; If set to `true`, retrieves the artifact with contracts
|
||||
from the container's `.m2`. Mount your local `.m2` as a volume available at the container's `/root/.m2` path.
|
||||
|
||||
CAUTION: You must not set both `EXTERNAL_CONTRACTS_WORK_OFFLINE` and `EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL`.
|
||||
|
||||
The following environment variables are used when running messaging based tests:
|
||||
|
||||
- `MESSAGING_TYPE` - what type of messaging system are you using (currently supported are `rabbit`, `kafka`)
|
||||
include::{project-root}/docker/spring-cloud-contract-docker/target/adoc/props.adoc[indent=0]
|
||||
|
||||
The following environment variables are used when tests are run:
|
||||
|
||||
- `APPLICATION_BASE_URL`: URL against which tests should be run.
|
||||
Remember that it has to be accessible from the Docker container (for example, `localhost`
|
||||
does not work)
|
||||
- `APPLICATION_USERNAME`: (optional) Username for basic authentication to your application.
|
||||
- `APPLICATION_PASSWORD`: (optional) Password for basic authentication to your application.
|
||||
include::{project-root}/docker/spring-cloud-contract-docker/target/adoc/appProps.adoc[indent=0]
|
||||
|
||||
[[docker-example-of-usage]]
|
||||
=== Example of Usage
|
||||
=== Example of Usage via HTTP
|
||||
|
||||
In this section, we explore a simple MVC application. To get started, clone the following
|
||||
git repository and cd to the resulting directory, by running the following commands:
|
||||
@@ -200,6 +160,216 @@ are run against the running application.
|
||||
http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/.
|
||||
The stubs are at http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar.
|
||||
|
||||
[[docker-example-of-usage-messaging]]
|
||||
=== Example of Usage via Messaging
|
||||
|
||||
If you want to use Spring Cloud Contract with messaging via the Docker images (e.g.
|
||||
in case of polyglot applications) then you'll have to have the following prerequisites met:
|
||||
|
||||
* Middleware (e.g. RabbitMQ or Kafka) must be running before generating tests
|
||||
* Your contract needs to call a method `triggerMessage(...)` with a `String` parameter that is equal to the contract's `label`.
|
||||
* Your application needs to have a HTTP endpoint via which we can trigger a message
|
||||
** That endpoint should not be available on production (could be enabled via an environment variable)
|
||||
|
||||
[[docker-example-of-usage-messaging-contract]]
|
||||
==== Example of a Messaging Contract
|
||||
|
||||
The contract needs to call a `triggerMessage(...)` method. That method is already provided in the base class for all tests in the docker image and will send out a request to the HTTP endpoint on the producer side. Below you can find examples of such contracts.
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
Contract.make {
|
||||
description 'Send a pong message in response to a ping message'
|
||||
label 'ping_pong'
|
||||
input {
|
||||
// You have to provide the `triggerMessage` method with the `label`
|
||||
// as a String parameter of the method
|
||||
triggeredBy('triggerMessage("ping_pong")')
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body([
|
||||
message: 'pong'
|
||||
])
|
||||
}
|
||||
metadata(
|
||||
[amqp:
|
||||
[
|
||||
outputMessage: [
|
||||
connectToBroker: [
|
||||
declareQueueWithName: "queue"
|
||||
],
|
||||
messageProperties: [
|
||||
receivedRoutingKey: '#'
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
}
|
||||
----
|
||||
|
||||
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
description: 'Send a pong message in response to a ping message'
|
||||
label: 'ping_pong'
|
||||
input:
|
||||
# You have to provide the `triggerMessage` method with the `label`
|
||||
# as a String parameter of the method
|
||||
triggeredBy: 'triggerMessage("ping_pong")'
|
||||
outputMessage:
|
||||
sentTo: 'output'
|
||||
body:
|
||||
message: 'pong'
|
||||
metadata:
|
||||
amqp:
|
||||
outputMessage:
|
||||
connectToBroker:
|
||||
declareQueueWithName: "queue"
|
||||
messageProperties:
|
||||
receivedRoutingKey: '#'
|
||||
----
|
||||
====
|
||||
|
||||
[[docker-example-of-usage-messaging-endpoint]]
|
||||
==== HTTP Endpoint to Trigger a Message
|
||||
|
||||
Why is there need to develop such an endpoint? Spring Cloud Contract
|
||||
would have to generate code in various languages (as it does in Java) to make it possible to trigger production
|
||||
code that sends a message to a broker. If such code is not generated then we need to be able to trigger the message anyways, and the way to do it is to provide an HTTP endpoint that the user will prepare in the language of their choosing.
|
||||
|
||||
The endpoint must have the following configuration:
|
||||
|
||||
- URL: `/springcloudcontract/{label}` where `label` can be any text
|
||||
- Method: `POST`
|
||||
- Basing on the `label` will generate a message that will be sent to a given destination according to the contract definition
|
||||
|
||||
Below you have an example of such an endpoint. If you're interested in
|
||||
providing an example in your language don't hesitate to file an issue in
|
||||
the https://github.com/spring-cloud/spring-cloud-contract/issues/new?assignees=&labels=&template=feature_request.md&title=New+Polyglot+Sample+of+a+HTTP+controller[Spring Cloud Contract repository at Github].
|
||||
|
||||
====
|
||||
[source,python,indent=0,subs="verbatim,attributes"]
|
||||
.Python
|
||||
----
|
||||
#!/usr/bin/env python
|
||||
|
||||
from flask import Flask
|
||||
from flask import jsonify
|
||||
import pika
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Production code that sends a message to RabbitMQ
|
||||
def send_message(cmd):
|
||||
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
|
||||
channel = connection.channel()
|
||||
channel.basic_publish(
|
||||
exchange='output',
|
||||
routing_key='#',
|
||||
body=cmd,
|
||||
properties=pika.BasicProperties(
|
||||
delivery_mode=2, # make message persistent
|
||||
))
|
||||
connection.close()
|
||||
return " [x] Sent via Rabbit: %s" % cmd
|
||||
|
||||
# This should be ran in tests (shouldn't be publicly available)
|
||||
if 'CONTRACT_TEST' in os.environ:
|
||||
@app.route('/springcloudcontract/<label>', methods=['POST'])
|
||||
def springcloudcontract(label):
|
||||
if label == "ping_pong":
|
||||
return send_message('{"message":"pong"}')
|
||||
else:
|
||||
raise ValueError('No such label expected.')
|
||||
----
|
||||
====
|
||||
|
||||
[[docker-example-of-usage-messaging-producer]]
|
||||
==== Running Message Tests on the Producer Side
|
||||
|
||||
Now, let's generate tests from contracts to test the producer side.
|
||||
We will run bash code to start the Docker image
|
||||
with attached contracts, however we will also add variables for the messaging
|
||||
code to work. In this case let's assume that the contracts are being stored in
|
||||
a Git repository.
|
||||
|
||||
====
|
||||
[source,bash]
|
||||
----
|
||||
#!/bin/bash
|
||||
set -x
|
||||
|
||||
CURRENT_DIR="$( pwd )"
|
||||
|
||||
export SC_CONTRACT_DOCKER_VERSION="${SC_CONTRACT_DOCKER_VERSION:-3.0.0-SNAPSHOT}"
|
||||
export APP_IP="$( ./whats_my_ip.sh )"
|
||||
export APP_PORT="${APP_PORT:-8000}"
|
||||
export APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
|
||||
export PROJECT_GROUP="${PROJECT_GROUP:-group}"
|
||||
export PROJECT_NAME="${PROJECT_NAME:-application}"
|
||||
export PROJECT_VERSION="${PROJECT_VERSION:-0.0.1-SNAPSHOT}"
|
||||
export PRODUCER_STUBS_CLASSIFIER="${PRODUCER_STUBS_CLASSIFIER:-stubs}"
|
||||
export FAIL_ON_NO_CONTRACTS="${FAIL_ON_NO_CONTRACTS:-false}"
|
||||
# In our Python app we want to enable the HTTP endpoint
|
||||
export CONTRACT_TEST="true"
|
||||
# In the Verifier docker container we want to add support for RabbitMQ
|
||||
export MESSAGING_TYPE="rabbit"
|
||||
|
||||
# Let's start the infrastructure (e.g. via Docker Compose)
|
||||
yes | docker-compose kill || echo "Nothing running"
|
||||
docker-compose up -d
|
||||
|
||||
echo "SC Contract Version [${SC_CONTRACT_DOCKER_VERSION}]"
|
||||
echo "Application URL [${APPLICATION_BASE_URL}]"
|
||||
echo "Project Version [${PROJECT_VERSION}]"
|
||||
|
||||
# Let's run python app
|
||||
gunicorn -w 4 --bind 0.0.0.0 main:app &
|
||||
APP_PID=$!
|
||||
|
||||
# Generate and run tests
|
||||
docker run --rm \
|
||||
--name verifier \
|
||||
# For the image to find the RabbitMQ running in another container
|
||||
-e "SPRING_RABBITMQ_ADDRESSES=${APP_IP}:5672" \
|
||||
# We need to tell the container what messaging middleware we will use
|
||||
-e "MESSAGING_TYPE=${MESSAGING_TYPE}" \
|
||||
-e "PUBLISH_STUBS_TO_SCM=false" \
|
||||
-e "PUBLISH_ARTIFACTS=false" \
|
||||
-e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" \
|
||||
-e "PROJECT_NAME=${PROJECT_NAME}" \
|
||||
-e "PROJECT_GROUP=${PROJECT_GROUP}" \
|
||||
-e "PROJECT_VERSION=${PROJECT_VERSION}" \
|
||||
-e "EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL=git://https://github.com/marcingrzejszczak/cdct_python_contracts.git" \
|
||||
-e "EXTERNAL_CONTRACTS_ARTIFACT_ID=${PROJECT_NAME}" \
|
||||
-e "EXTERNAL_CONTRACTS_GROUP_ID=${PROJECT_GROUP}" \
|
||||
-e "EXTERNAL_CONTRACTS_VERSION=${PROJECT_VERSION}" \
|
||||
-v "${CURRENT_DIR}/build/spring-cloud-contract/output:/spring-cloud-contract-output/" \
|
||||
springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"
|
||||
|
||||
kill $APP_PID
|
||||
|
||||
yes | docker-compose kill
|
||||
----
|
||||
====
|
||||
|
||||
What will happen is:
|
||||
|
||||
- Tests will be generated from contracts taken from Git
|
||||
- In the contract we've provided an entry in metadata called `declareQueueWithName` that will lead to creation of a queue in RabbitMQ with the given name *before* the request to trigger the message is sent
|
||||
- Via the `triggerMessage("ping_pong")` method call a POST request to the Python application to the `/springcloudcontract/ping_pong` endpoint will be made
|
||||
- The Python application will generate and send a `'{"message":"pong"}'` JSON via RabbitMQ to an exchange called `output`
|
||||
- The generated test will poll for a message sent to the `output` exchange
|
||||
- Once the message was received will assert its contents
|
||||
|
||||
After the tests have passed we know that the message was properly sent from the Python app to RabbitMQ.
|
||||
|
||||
[[docker-stubrunner]]
|
||||
== Running Stubs on the Consumer Side
|
||||
|
||||
@@ -254,7 +424,13 @@ $ STUBRUNNER_PORT="8083"
|
||||
$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
|
||||
$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
|
||||
# Run the docker with Stub Runner Boot
|
||||
$ docker run --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -e "STUBRUNNER_STUBS_MODE=REMOTE" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"
|
||||
$ docker run --rm \
|
||||
-e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" \
|
||||
-e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" \
|
||||
-e "STUBRUNNER_STUBS_MODE=REMOTE" \
|
||||
-p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" \
|
||||
-p "9876:9876" \
|
||||
springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"
|
||||
----
|
||||
====
|
||||
|
||||
@@ -283,3 +459,152 @@ $ curl -X GET http://localhost:9876/api/books
|
||||
IMPORTANT: If you want use the stubs that you have built locally, on your host,
|
||||
you should set the `-e STUBRUNNER_STUBS_MODE=LOCAL` environment variable and mount
|
||||
the volume of your local m2 (`-v "${HOME}/.m2/:/root/.m2:ro"`).
|
||||
|
||||
[[docker-stubrunner-example-messaging]]
|
||||
=== Example of Usage with Messaging
|
||||
|
||||
In order to make messaging work it's enough to pass the `MESSAGING_TYPE` environment variable with `kafka` or `rabbit` values. This will lead to setting up
|
||||
the Stub Runner Boot Docker image with dependencies required to connect to the broker.
|
||||
|
||||
In order to set the connection properties you can check out Spring Cloud Stream properties page to set proper environment variables.
|
||||
|
||||
// TODO: Change to current or sth
|
||||
* https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#integration-properties[Spring Boot Integration properties]
|
||||
** You can search for `spring.rabbitmq.xxx` or `spring.kafka.xxx` properties
|
||||
* https://docs.spring.io/spring-cloud-stream-binder-rabbit/docs/3.1.0.M1/reference/html/index.html#_configuration_options[Stream specific RabbitMQ properties]
|
||||
* https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/3.1.0.M1/reference/html/index.html#_configuration_options[Stream specific Kafka properties]
|
||||
|
||||
The most common property you would set is the location of the running middlewara.
|
||||
If a property to set it is called `spring.rabbitmq.addresses` or `spring.kafka.bootstrap-servers` then you should name the environment variable `SPRING_RABBITMQ_ADDRESSES` and `SPRING_KAFKA_BOOTSTRAP_SERVERS` respectively.
|
||||
|
||||
[[docker-middleware-standalone]]
|
||||
== Running Contract Tests against Existing Middleware
|
||||
|
||||
There is legitimate reason to run your contract tests against existing middleware. Some
|
||||
testing frameworks might give you false positive results - the test within your build
|
||||
passes whereas on production the communication fails.
|
||||
|
||||
In Spring Cloud Contract docker images we give an option to connect to existing middleware.
|
||||
As presented in previous subsections we do support Kafka and RabbitMQ out of the box. However,
|
||||
via https://camel.apache.org/components/latest/index.html[Apache Camel Components] we can support
|
||||
other middleware too. Let's take a look at the following examples of usage.
|
||||
|
||||
[[docker-verifier-running-middlware]]
|
||||
=== Spring Cloud Contract Docker and running Middleware
|
||||
|
||||
In order to connect to arbitrary middleware, we'll leverage the `standalone` metadata entry
|
||||
in the contract section.
|
||||
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
description: 'Send a pong message in response to a ping message'
|
||||
label: 'standalone_ping_pong' <1>
|
||||
input:
|
||||
triggeredBy: 'triggerMessage("ping_pong")' <2>
|
||||
outputMessage:
|
||||
sentTo: 'rabbitmq:output' <3>
|
||||
body: <4>
|
||||
message: 'pong'
|
||||
metadata:
|
||||
standalone: <5>
|
||||
setup: <6>
|
||||
options: rabbitmq:output?queue=output&routingKey=# <7>
|
||||
outputMessage: <8>
|
||||
additionalOptions: routingKey=#&queue=output <9>
|
||||
----
|
||||
<1> Label by which we'll be able to trigger the message via Stub Runner
|
||||
<2> As in the previous messaging examples we'll need to trigger the HTTP endpoint in the running application to make it send a message according to the provided protocol
|
||||
<3> `protocol:destination` as requested by Apache Camel
|
||||
<4> Output message body
|
||||
<5> Standalone metadata entry
|
||||
<6> Setup part will contain information about how to prepare for running contract tests before the actual call to HTTP endpoint of the running application is made
|
||||
<7> Apache Camel URI to be called in the setup phase. In this case we will try to poll for a message at the `output` exchange and due to to having the `queue=output` and `routingKey=#` a queue with name `output` will be set and bound to the `output` exchange with routing key `#`
|
||||
<8> Additional options (more technical ones) to be appended to the `protocol:destination` from point (3) - together will be combined in the following format `rabbitmq:output?routingKey=#&queue=output`.
|
||||
|
||||
For the contract tests to pass we will need as usual in case of messaging in polyglot environment
|
||||
a running application and running middleware. This time we will have different environment variables set for the Spring Cloud Contract Docker image.
|
||||
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
#!/bin/bash
|
||||
set -x
|
||||
|
||||
# Setup
|
||||
# Run the middleware
|
||||
docker-compose up -d rabbitmq <1>
|
||||
|
||||
# Run the python application
|
||||
gunicorn -w 4 --bind 0.0.0.0 main:app & <2>
|
||||
APP_PID=$!
|
||||
|
||||
docker run --rm \
|
||||
--name verifier \
|
||||
-e "STANDALONE_PROTOCOL=rabbitmq" \ <3>
|
||||
-e "CAMEL_COMPONENT_RABBITMQ_ADDRESSES=172.18.0.1:5672" \ <4>
|
||||
-e "PUBLISH_STUBS_TO_SCM=false" \
|
||||
-e "PUBLISH_ARTIFACTS=false" \
|
||||
-e "APPLICATION_BASE_URL=172.18.0.1" \
|
||||
-e "PROJECT_NAME=application" \
|
||||
-e "PROJECT_GROUP=group" \
|
||||
-e "EXTERNAL_CONTRACTS_ARTIFACT_ID=application" \
|
||||
-e "EXTERNAL_CONTRACTS_GROUP_ID=group" \
|
||||
-e "EXTERNAL_CONTRACTS_VERSION=0.0.1-SNAPSHOT" \
|
||||
-v "${CURRENT_DIR}/build/spring-cloud-contract/output:/spring-cloud-contract-output/" \
|
||||
springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"
|
||||
|
||||
|
||||
# Teardown
|
||||
kill $APP_PID
|
||||
yes | docker-compose kill
|
||||
----
|
||||
<1> We need to have the middleware running first
|
||||
<2> The application needs to be up and running
|
||||
<3> Via the `STANDALONE_PROTOCOL` environment variable we will fetch a https://camel.apache.org/components/latest/index.html[Apache Camel Component]. The artifact that we will fetch is `org.apache.camel.springboot:camel-${STANDALONE_PROTOCOL}-starter`. In other words `STANDALONE_PROTOCOL` is matching Camel's component.
|
||||
<4> We're setting addresses (we could be setting credentials) via Camel's Spring Boot Starter mechanisms. Example for https://camel.apache.org/components/latest/rabbitmq-component.html#_spring_boot_auto_configuration[Apache Camel's RabbitMQ Spring Boot Auto-Configuration]
|
||||
|
||||
[[docker-stubrunner-running-middlware]]
|
||||
=== Stub Runner Docker and running Middleware
|
||||
|
||||
In order to trigger a stub message against running middleware, we can run Stub Runner Docker image in the following manner.
|
||||
|
||||
Example of usage
|
||||
|
||||
```bash
|
||||
$ docker run \
|
||||
-e "CAMEL_COMPONENT_RABBITMQ_ADDRESSES=172.18.0.1:5672" \ <1>
|
||||
-e "STUBRUNNER_IDS=group:application:0.0.1-SNAPSHOT" \ <2>
|
||||
-e "STUBRUNNER_REPOSITORY_ROOT=git://https://github.com/marcingrzejszczak/cdct_python_contracts.git" \ <3>
|
||||
-e ADDITIONAL_OPTS="--thin.properties.dependencies.rabbitmq=org.apache.camel.springboot:camel-rabbitmq-starter:3.4.0" \ <4>
|
||||
-e "STUBRUNNER_STUBS_MODE=REMOTE" \ <5>
|
||||
-v "${HOME}/.m2/:/root/.m2:ro" \ <6>
|
||||
-p 8750:8750 \ <7>
|
||||
springcloud/spring-cloud-contract-stub-runner:3.0.0-SNAPSHOT <8>
|
||||
```
|
||||
<1> We're injecting the address of RabbitMQ via https://camel.apache.org/components/latest/rabbitmq-component.html#_spring_boot_auto_configuration[Apache Camel's Spring Boot Auto-Configuration]
|
||||
<2> We're telling Stub Runner which stubs to download
|
||||
<3> We're providing an external location for our stubs (Git repository)
|
||||
<4> Via the `ADDITIONAL_OPTS=--thin.properties.dependencies.XXX=GROUP:ARTIFACT:VERSION` property we're telling Stub Runner which additional dependency to fetch at runtime. In this case we want to fetch `camel-rabbitmq-starter` so `XXX` is a random string and we want to fetch `org.apache.camel.springboot:camel-rabbitmq-starter` artifact in version `3.4.0`.
|
||||
<5> Since we're using Git, the remote option of fetching stubs needs to be set
|
||||
<6> So that we speed up launching of Stub Runner, we're attaching our local Maven repository `.m2` as a volume. If you don't have it populated you can consider setting the write permissions via `:rw` instead read only `:ro`.
|
||||
<7> We expose the port `8750` at which Stub Runner is running.
|
||||
<8> Coordinates of the Stub Runner Docker image.
|
||||
|
||||
After a while you'll notice the following text in your console, which means that Stub Runner is ready to accept requests.
|
||||
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
o.a.c.impl.engine.AbstractCamelContext : Apache Camel 3.4.3 (camel-1) started in 0.007 seconds
|
||||
o.s.c.c.s.server.StubRunnerBoot : Started StubRunnerBoot in 14.483 seconds (JVM running for 18.666)
|
||||
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
|
||||
o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
|
||||
----
|
||||
|
||||
To get the list of triggers you can send an HTTP GET request to `localhost:8750/triggers` endpoint. To trigger a stub message, you can send a HTTP POST request to `localhost:8750/triggers/standalone_ping_pong`. In the console you'll see:
|
||||
|
||||
[source,bash.indent=0]
|
||||
----
|
||||
o.s.c.c.v.m.camel.CamelStubMessages : Will send a message to URI [rabbitmq:output?routingKey=#&queue=output]
|
||||
----
|
||||
|
||||
If you check the RabbitMQ management console, you'll see that there's 1 message available in the `output` queue.
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.docs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
|
||||
/**
|
||||
* This test generates additional resources in the `target` folder that are then
|
||||
* referenced by the documentation.
|
||||
*/
|
||||
class AdditionalResourcesGenerationTests {
|
||||
|
||||
// @formatter:off
|
||||
private static final String CONTRACT = "description: Some description\n"
|
||||
+ "name: some name\n"
|
||||
+ "priority: 8\n"
|
||||
+ "ignored: true\n"
|
||||
+ "inProgress: true\n"
|
||||
+ "request:\n"
|
||||
+ " method: PUT\n"
|
||||
+ " url: /foo\n"
|
||||
+ " queryParameters:\n"
|
||||
+ " a: b\n"
|
||||
+ " b: c\n"
|
||||
+ " headers:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " fooReq: baz\n"
|
||||
+ " cookies:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " fooReq: baz\n"
|
||||
+ " body:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " matchers:\n"
|
||||
+ " body:\n"
|
||||
+ " - path: $.foo\n"
|
||||
+ " type: by_regex\n"
|
||||
+ " value: bar\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: foo\n"
|
||||
+ " regex: bar\n"
|
||||
+ "response:\n"
|
||||
+ " status: 200\n"
|
||||
+ " fixedDelayMilliseconds: 1000\n"
|
||||
+ " headers:\n"
|
||||
+ " foo2: bar\n"
|
||||
+ " foo3: foo33\n"
|
||||
+ " fooRes: baz\n"
|
||||
+ " body:\n"
|
||||
+ " foo2: bar\n"
|
||||
+ " foo3: baz\n"
|
||||
+ " nullValue: null\n"
|
||||
+ " matchers:\n"
|
||||
+ " body:\n"
|
||||
+ " - path: $.foo2\n"
|
||||
+ " type: by_regex\n"
|
||||
+ " value: bar\n"
|
||||
+ " - path: $.foo3\n"
|
||||
+ " type: by_command\n"
|
||||
+ " value: executeMe($it)\n"
|
||||
+ " - path: $.nullValue\n"
|
||||
+ " type: by_null\n"
|
||||
+ " value: null\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: foo2\n"
|
||||
+ " regex: bar\n"
|
||||
+ " - key: foo3\n"
|
||||
+ " command: andMeToo($it)\n"
|
||||
+ " cookies:\n"
|
||||
+ " - key: foo2\n"
|
||||
+ " regex: bar\n"
|
||||
+ " - key: foo3\n"
|
||||
+ " predefined:\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_produce_a_json_schema_of_a_yaml_model() throws IOException {
|
||||
String schemaString = generateJsonSchemaForClass(YamlContract.class);
|
||||
File schemaFile = new File("target/contract_schema.json");
|
||||
Files.write(schemaFile.toPath(), schemaString.getBytes());
|
||||
}
|
||||
|
||||
private String generateJsonSchemaForClass(Class clazz)
|
||||
throws JsonProcessingException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
|
||||
JsonSchema schema = schemaGen.generateSchema(clazz);
|
||||
return mapper.writeValueAsString(schema);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_convert_yaml_to_contract() throws IOException {
|
||||
File ymlFile = new File("target/contract.yml");
|
||||
Files.write(ymlFile.toPath(), CONTRACT.getBytes());
|
||||
|
||||
Collection<Contract> contracts = new YamlContractConverter().convertFrom(ymlFile);
|
||||
|
||||
BDDAssertions.then(contracts).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_produce_an_adoc_with_all_of_metadata_classes() throws Exception {
|
||||
List<Class> metadata = metadataClasses();
|
||||
File doc = new File("target/metadata.adoc");
|
||||
|
||||
StringBuilder sb = adocWithMetadata(metadata);
|
||||
|
||||
Files.write(doc.toPath(), sb.toString().getBytes());
|
||||
}
|
||||
|
||||
private StringBuilder adocWithMetadata(List<Class> metadata) throws Exception {
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Class metadatum : metadata) {
|
||||
SpringCloudContractMetadata newInstance = (SpringCloudContractMetadata) metadatum
|
||||
.newInstance();
|
||||
String description = newInstance.description();
|
||||
String key = newInstance.key();
|
||||
List<Class> additionalClasses = classesToLookAt(metadatum, newInstance);
|
||||
// @formatter:off
|
||||
sb
|
||||
.append("[[metadata-").append(key).append("]]\n")
|
||||
.append("#### Metadata ").append(key).append("\n\n")
|
||||
.append("* key: `").append(key).append("`").append("\n")
|
||||
.append("* description:\n\n").append(description).append("\n\n")
|
||||
.append("Example:\n\n")
|
||||
.append("```yaml\n").append(mapper.writeValueAsString(newInstance)).append("\n```\n\n")
|
||||
// To make the schema collapsable
|
||||
.append("+++ <details><summary> +++\nClick here to expand the JSON schema:\n+++ </summary><div> +++\n")
|
||||
.append("```json\n").append(generateJsonSchemaForClass(metadatum)).append("\n```\n")
|
||||
.append("+++ </div></details> +++\n\n")
|
||||
.append("If you're interested in learning more about the types and its properties, please check out the following classes:\n\n")
|
||||
.append(additionalClasses.stream().map(aClass -> "* `" + aClass.getName() + "`").collect(Collectors.joining("\n")))
|
||||
.append("\n\n");
|
||||
// @formatter:on
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
private List<Class> classesToLookAt(Class metadatum,
|
||||
SpringCloudContractMetadata newInstance) {
|
||||
List<Class> additionalClasses = new ArrayList<>();
|
||||
additionalClasses.add(metadatum);
|
||||
additionalClasses.addAll(newInstance.additionalClassesToLookAt());
|
||||
return additionalClasses;
|
||||
}
|
||||
|
||||
private List<Class> metadataClasses() throws ClassNotFoundException {
|
||||
BeanDefinitionRegistry bdr = new SimpleBeanDefinitionRegistry();
|
||||
ClassPathBeanDefinitionScanner s = new ClassPathBeanDefinitionScanner(bdr, false);
|
||||
TypeFilter tf = new AssignableTypeFilter(SpringCloudContractMetadata.class);
|
||||
s.addIncludeFilter(tf);
|
||||
String basePackage = "org.springframework.cloud.contract";
|
||||
s.scan(basePackage);
|
||||
String[] beans = bdr.getBeanDefinitionNames();
|
||||
List<Class> metadata = new ArrayList<>();
|
||||
for (String bean : beans) {
|
||||
BeanDefinition beanDefinition = bdr.getBeanDefinition(bean);
|
||||
String beanClassName = beanDefinition.getBeanClassName();
|
||||
if (beanClassName != null && !beanClassName.contains(basePackage)) {
|
||||
continue;
|
||||
}
|
||||
metadata.add(Class.forName(beanClassName));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.docs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
|
||||
class JsonSchemaTests {
|
||||
|
||||
// @formatter:off
|
||||
private static final String CONTRACT = "description: Some description\n"
|
||||
+ "name: some name\n"
|
||||
+ "priority: 8\n"
|
||||
+ "ignored: true\n"
|
||||
+ "inProgress: true\n"
|
||||
+ "request:\n"
|
||||
+ " method: PUT\n"
|
||||
+ " url: /foo\n"
|
||||
+ " queryParameters:\n"
|
||||
+ " a: b\n"
|
||||
+ " b: c\n"
|
||||
+ " headers:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " fooReq: baz\n"
|
||||
+ " cookies:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " fooReq: baz\n"
|
||||
+ " body:\n"
|
||||
+ " foo: bar\n"
|
||||
+ " matchers:\n"
|
||||
+ " body:\n"
|
||||
+ " - path: $.foo\n"
|
||||
+ " type: by_regex\n"
|
||||
+ " value: bar\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: foo\n"
|
||||
+ " regex: bar\n"
|
||||
+ "response:\n"
|
||||
+ " status: 200\n"
|
||||
+ " fixedDelayMilliseconds: 1000\n"
|
||||
+ " headers:\n"
|
||||
+ " foo2: bar\n"
|
||||
+ " foo3: foo33\n"
|
||||
+ " fooRes: baz\n"
|
||||
+ " body:\n"
|
||||
+ " foo2: bar\n"
|
||||
+ " foo3: baz\n"
|
||||
+ " nullValue: null\n"
|
||||
+ " matchers:\n"
|
||||
+ " body:\n"
|
||||
+ " - path: $.foo2\n"
|
||||
+ " type: by_regex\n"
|
||||
+ " value: bar\n"
|
||||
+ " - path: $.foo3\n"
|
||||
+ " type: by_command\n"
|
||||
+ " value: executeMe($it)\n"
|
||||
+ " - path: $.nullValue\n"
|
||||
+ " type: by_null\n"
|
||||
+ " value: null\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: foo2\n"
|
||||
+ " regex: bar\n"
|
||||
+ " - key: foo3\n"
|
||||
+ " command: andMeToo($it)\n"
|
||||
+ " cookies:\n"
|
||||
+ " - key: foo2\n"
|
||||
+ " regex: bar\n"
|
||||
+ " - key: foo3\n"
|
||||
+ " predefined:\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_produce_a_json_schema_of_a_yaml_model() throws IOException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
|
||||
JsonSchema schema = schemaGen.generateSchema(YamlContract.class);
|
||||
String schemaString = mapper.writeValueAsString(schema);
|
||||
File schemaFile = new File("target/contract_schema.json");
|
||||
Files.write(schemaFile.toPath(), schemaString.getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_convert_yaml_to_contract() throws IOException {
|
||||
File ymlFile = new File("target/contract.yml");
|
||||
Files.write(ymlFile.toPath(), CONTRACT.getBytes());
|
||||
|
||||
Collection<Contract> contracts = new YamlContractConverter().convertFrom(ymlFile);
|
||||
|
||||
BDDAssertions.then(contracts).isNotEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -59,13 +59,14 @@ response:
|
||||
predefined:
|
||||
metadata:
|
||||
wiremock:
|
||||
"postServeActions": {
|
||||
"webhook": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"method": "POST",
|
||||
"body": "{ \"result\": \"SUCCESS\" }",
|
||||
"url": "http://localhost:56299/callback"
|
||||
stubMapping:
|
||||
"postServeActions": {
|
||||
"webhook": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"method": "POST",
|
||||
"body": "{ \"result\": \"SUCCESS\" }",
|
||||
"url": "http://localhost:56299/callback"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
pom.xml
5
pom.xml
@@ -23,7 +23,8 @@
|
||||
<inceptionYear>2016</inceptionYear>
|
||||
|
||||
<properties>
|
||||
<camel.version>3.3.0</camel.version>
|
||||
<!-- Align it also in Docker image -->
|
||||
<camel.version>3.4.3</camel.version>
|
||||
<checkstyle.version>2.17</checkstyle.version>
|
||||
<pact.version>4.0.9</pact.version>
|
||||
<jsch-agent.version>0.0.9</jsch-agent.version>
|
||||
@@ -71,8 +72,8 @@
|
||||
|
||||
<modules>
|
||||
<module>spring-cloud-contract-dependencies</module>
|
||||
<module>docs</module>
|
||||
<module>docker</module>
|
||||
<module>docs</module>
|
||||
<module>spring-cloud-contract-shade</module>
|
||||
<module>spring-cloud-contract-wiremock</module>
|
||||
<module>spring-cloud-contract-verifier</module>
|
||||
|
||||
@@ -50,13 +50,16 @@ import org.springframework.cloud.contract.spec.Contract
|
||||
contentType("application/json")
|
||||
}
|
||||
}
|
||||
metadata([wiremock: '''\
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}
|
||||
'''
|
||||
metadata([
|
||||
wiremock: [
|
||||
stubMapping: '''\
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}
|
||||
'''
|
||||
]
|
||||
])
|
||||
}
|
||||
// end::metadata[]
|
||||
|
||||
@@ -11,12 +11,13 @@ response:
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
metadata:
|
||||
wiremock: >
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
wiremock:
|
||||
stubMapping: >
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
# end::metadata[]
|
||||
---
|
||||
request:
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.util.Collection;
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public interface ContractConverter<T> extends ContractStorer<T> {
|
||||
public interface ContractConverter<T> extends ContractStorer<T>, ContractReader<T> {
|
||||
|
||||
/**
|
||||
* Should this file be accepted by the converter. Can use the file extension to check
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.spec;
|
||||
|
||||
/**
|
||||
* Defines how to read converted contracts from a byte array representation back to
|
||||
* contracts.
|
||||
*
|
||||
* @param <T> contracts type
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface ContractReader<T> {
|
||||
|
||||
/**
|
||||
* Reads contracts from bytes.
|
||||
* @param bytes - byte representation of contracts
|
||||
* @return contracts
|
||||
*/
|
||||
default T read(byte[] bytes) {
|
||||
throw new UnsupportedOperationException("Can't read contract from bytes");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -310,12 +310,12 @@ then:
|
||||
val contract =
|
||||
// tag::metadata[]
|
||||
contract {
|
||||
metadata("wiremock" to """
|
||||
metadata("wiremock" to ("stubmapping" to """
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}""")
|
||||
}"""))
|
||||
}
|
||||
// end::metadata[]
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ import org.springframework.cloud.contract.spec.internal.Headers;
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage;
|
||||
import org.springframework.cloud.contract.stubrunner.AvailablePortScanner.PortCallback;
|
||||
import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStub;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
|
||||
@@ -61,6 +64,8 @@ class StubRunnerExecutor implements StubFinder {
|
||||
|
||||
private StubServer stubServer;
|
||||
|
||||
private final YamlContractConverter yamlContractConverter = new YamlContractConverter();
|
||||
|
||||
StubRunnerExecutor(AvailablePortScanner portScanner,
|
||||
MessageVerifier<?> contractVerifierMessaging,
|
||||
List<HttpServerStub> serverStubs) {
|
||||
@@ -251,12 +256,22 @@ class StubRunnerExecutor implements StubFinder {
|
||||
OutputMessage outputMessage = groovyDsl.getOutputMessage();
|
||||
DslProperty<?> body = outputMessage.getBody();
|
||||
Headers headers = outputMessage.getHeaders();
|
||||
List<YamlContract> yamlContracts = yamlContractConverter
|
||||
.convertTo(Collections.singleton(groovyDsl));
|
||||
YamlContract contract = yamlContracts.get(0);
|
||||
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.OUTPUT);
|
||||
// TODO: Json is harcoded here
|
||||
this.contractVerifierMessaging.send(
|
||||
JsonOutput.toJson(BodyExtractor.extractClientValueFromBody(
|
||||
body == null ? null : body.getClientValue())),
|
||||
headers == null ? null : headers.asStubSideMap(),
|
||||
outputMessage.getSentTo().getClientValue());
|
||||
outputMessage.getSentTo().getClientValue(), contract);
|
||||
}
|
||||
|
||||
private void setMessageType(YamlContract contract,
|
||||
ContractVerifierMessageMetadata.MessageType output) {
|
||||
contract.metadata.put(ContractVerifierMessageMetadata.METADATA_KEY,
|
||||
new ContractVerifierMessageMetadata(output));
|
||||
}
|
||||
|
||||
private URL returnStubUrlIfMatches(boolean condition) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.stubrunner.junit;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
|
||||
/**
|
||||
@@ -30,22 +31,24 @@ class ExceptionThrowingMessageVerifier implements MessageVerifier {
|
||||
private static final String EXCEPTION_MESSAGE = "Please provide a custom MessageVerifier to use this feature";
|
||||
|
||||
@Override
|
||||
public void send(Object message, String destination) {
|
||||
public void send(Object message, String destination, YamlContract contract) {
|
||||
throw new UnsupportedOperationException(EXCEPTION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
throw new UnsupportedOperationException(EXCEPTION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination) {
|
||||
public Object receive(String destination, YamlContract contract) {
|
||||
throw new UnsupportedOperationException(EXCEPTION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
public void send(Object payload, Map headers, String destination,
|
||||
YamlContract contract) {
|
||||
throw new UnsupportedOperationException(EXCEPTION_MESSAGE);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.cloud.contract.stubrunner.StubConfiguration;
|
||||
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilderProvider;
|
||||
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions;
|
||||
import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -193,23 +194,25 @@ class LazyMessageVerifier implements MessageVerifier {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object message, String destination) {
|
||||
messageVerifier().send(message, destination);
|
||||
public void send(Object message, String destination, YamlContract contract) {
|
||||
messageVerifier().send(message, destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
return messageVerifier().receive(destination, timeout, timeUnit);
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
return messageVerifier().receive(destination, timeout, timeUnit, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination) {
|
||||
return messageVerifier().receive(destination);
|
||||
public Object receive(String destination, YamlContract contract) {
|
||||
return messageVerifier().receive(destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
messageVerifier().send(payload, headers, destination);
|
||||
public void send(Object payload, Map headers, String destination,
|
||||
YamlContract contract) {
|
||||
messageVerifier().send(payload, headers, destination, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import groovy.json.JsonOutput
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.cloud.contract.stubrunner.util.StubsParser
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
|
||||
import org.springframework.util.SocketUtils
|
||||
|
||||
@@ -168,22 +169,22 @@ class StubRunnerExecutorSpec extends Specification {
|
||||
boolean called
|
||||
|
||||
@Override
|
||||
void send(Object message, String destination) {
|
||||
void send(Object message, String destination, YamlContract contract) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination) {
|
||||
Object receive(String destination, YamlContract contract) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
void send(Object payload, Map headers, String destination) {
|
||||
void send(Object payload, Map headers, String destination, YamlContract contract) {
|
||||
this.called = true
|
||||
println "Body <${payload}>"
|
||||
assert !payload.toString().contains("cursor")
|
||||
@@ -201,23 +202,23 @@ class StubRunnerExecutorSpec extends Specification {
|
||||
private class AssertingStubMessages implements MessageVerifier<Object> {
|
||||
|
||||
@Override
|
||||
void send(Object message, String destination) {
|
||||
void send(Object message, String destination, YamlContract contract) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
<T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
<T> void send(T payload, Map<String, Object> headers, String destination, YamlContract contract) {
|
||||
assert !(JsonOutput.toJson(payload).contains("serverValue"))
|
||||
assert headers.entrySet().every { !(it.value.toString().contains("serverValue")) }
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination) {
|
||||
Object receive(String destination, YamlContract contract) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
|
||||
|
||||
/**
|
||||
@@ -68,22 +69,22 @@ class StubRunnerRuleCustomMsgVerifierSpec extends Specification {
|
||||
static class MyMessageVerifier implements MessageVerifier {
|
||||
|
||||
@Override
|
||||
void send(Object message, String destination) {
|
||||
void send(Object message, String destination, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to send a message")
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
Object receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to receive a message with timeout")
|
||||
}
|
||||
|
||||
@Override
|
||||
Object receive(String destination) {
|
||||
Object receive(String destination, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to receive a message")
|
||||
}
|
||||
|
||||
@Override
|
||||
void send(Object payload, Map headers, String destination) {
|
||||
void send(Object payload, Map headers, String destination, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to send a message with headers")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -82,22 +83,24 @@ class StubRunnerJUnit5ExtensionCustomMessageVerifierTests {
|
||||
static class MyMessageVerifier implements MessageVerifier {
|
||||
|
||||
@Override
|
||||
public void send(Object message, String destination) {
|
||||
public void send(Object message, String destination, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to send a message");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to receive a message with timeout");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination) {
|
||||
public Object receive(String destination, YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to receive a message");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
public void send(Object payload, Map headers, String destination,
|
||||
YamlContract contract) {
|
||||
throw new IllegalStateException("Failed to send a message with headers");
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -35,17 +34,14 @@ import org.springframework.cloud.contract.spec.Contract;
|
||||
|
||||
class DefaultWireMockStubPostProcessor implements WireMockStubPostProcessor {
|
||||
|
||||
private static final List<Class> APPLICABLE_CLASSES = Arrays.asList(String.class,
|
||||
StubMapping.class, Map.class);
|
||||
|
||||
public static final String WIREMOCK_METADATA_ENTRY = "wiremock";
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public StubMapping postProcess(StubMapping stubMapping, Contract contract) {
|
||||
Object wiremock = getWiremockEntry(contract);
|
||||
StubMapping stubMappingFromMetadata = stubMappingFromMetadata(wiremock);
|
||||
WireMockMetaData wireMockMetaData = WireMockMetaData
|
||||
.fromMetadata(contract.getMetadata());
|
||||
StubMapping stubMappingFromMetadata = stubMappingFromMetadata(
|
||||
wireMockMetaData.getStubMapping());
|
||||
stubMapping.setResponse(mergedResponse(stubMapping, stubMappingFromMetadata));
|
||||
if (stubMappingFromMetadata.getPostServeActions() != null) {
|
||||
setPostServeActions(stubMapping, stubMappingFromMetadata);
|
||||
@@ -149,10 +145,6 @@ class DefaultWireMockStubPostProcessor implements WireMockStubPostProcessor {
|
||||
: stubMapping.getResponse().getFixedDelayMilliseconds();
|
||||
}
|
||||
|
||||
private Object getWiremockEntry(Contract contract) {
|
||||
return contract.getMetadata().get(WIREMOCK_METADATA_ENTRY);
|
||||
}
|
||||
|
||||
private StubMapping stubMappingFromMetadata(Object wiremock) {
|
||||
if (wiremock instanceof String) {
|
||||
return StubMapping.buildFrom((String) wiremock);
|
||||
@@ -176,13 +168,15 @@ class DefaultWireMockStubPostProcessor implements WireMockStubPostProcessor {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Contract contract) {
|
||||
boolean contains = contract.getMetadata().containsKey(WIREMOCK_METADATA_ENTRY);
|
||||
boolean contains = contract.getMetadata()
|
||||
.containsKey(WireMockMetaData.METADATA_KEY);
|
||||
if (!contains) {
|
||||
return false;
|
||||
}
|
||||
Object wiremock = getWiremockEntry(contract);
|
||||
return APPLICABLE_CLASSES.stream()
|
||||
.anyMatch(aClass -> aClass.isAssignableFrom(wiremock.getClass()));
|
||||
Object stubMapping = WireMockMetaData.fromMetadata(contract.getMetadata())
|
||||
.getStubMapping();
|
||||
return WireMockMetaData.APPLICABLE_CLASSES.stream()
|
||||
.anyMatch(aClass -> aClass.isAssignableFrom(stubMapping.getClass()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
|
||||
public class WireMockMetaData implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Key under which this metadata entry can be found in contract's metadata.
|
||||
*/
|
||||
public static final String METADATA_KEY = "wiremock";
|
||||
|
||||
/**
|
||||
* Applicable classes for Stub Mapping.
|
||||
*/
|
||||
static final List<Class> APPLICABLE_CLASSES = Arrays.asList(String.class,
|
||||
StubMapping.class, Map.class);
|
||||
|
||||
/**
|
||||
* {@link StubMapping} represented by one of the classes in
|
||||
* {@link #APPLICABLE_CLASSES}.
|
||||
*/
|
||||
private Object stubMapping;
|
||||
|
||||
public Object getStubMapping() {
|
||||
return stubMapping;
|
||||
}
|
||||
|
||||
public void setStubMapping(Object stubMapping) {
|
||||
this.stubMapping = stubMapping;
|
||||
}
|
||||
|
||||
public static WireMockMetaData fromMetadata(Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, WireMockMetaData.METADATA_KEY,
|
||||
new WireMockMetaData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Metadata for extending WireMock stubs.\n\nStubMapping can be "
|
||||
+ "one of the following classes "
|
||||
+ APPLICABLE_CLASSES.stream()
|
||||
.map(aClass -> "`" + aClass.getSimpleName() + "`")
|
||||
.collect(Collectors.toList())
|
||||
+ ". Please check "
|
||||
+ "the http://wiremock.org/docs/stubbing/ for more information about the StubMapping class properties.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class> additionalClassesToLookAt() {
|
||||
return Collections.singletonList(StubMapping.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -66,7 +67,9 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
@Test
|
||||
void should_not_be_applicable_for_invalid_metadata_entry() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", 5);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping", 5);
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isFalse();
|
||||
}
|
||||
@@ -74,15 +77,17 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
@Test
|
||||
void should_be_applicable_for_valid_metadata_entry() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", "foo");
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping", "foo");
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
|
||||
contract.getMetadata().put("wiremock", new StubMapping());
|
||||
map.put("stubMapping", new StubMapping());
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
|
||||
contract.getMetadata().put("wiremock", new HashMap<>());
|
||||
map.put("stubMapping", new HashMap<>());
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
}
|
||||
@@ -90,7 +95,9 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_string() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", POST_SERVE_ACTION);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping", POST_SERVE_ACTION);
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
@@ -102,7 +109,9 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_stub_mapping() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", StubMapping.buildFrom(POST_SERVE_ACTION));
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping", StubMapping.buildFrom(POST_SERVE_ACTION));
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
@@ -115,8 +124,10 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_map()
|
||||
throws JsonProcessingException {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock",
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping",
|
||||
new ObjectMapper().readValue(POST_SERVE_ACTION, HashMap.class));
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
@@ -128,7 +139,9 @@ class DefaultWireMockStubPostProcessorTests {
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_string_and_contains_response() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", RESPONSE_DELAY);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("stubMapping", RESPONSE_DELAY);
|
||||
contract.getMetadata().put("wiremock", map);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
|
||||
package org.springframework.cloud.contract.verifier.converter
|
||||
|
||||
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.util.logging.Slf4j
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.spec.ContractConverter
|
||||
|
||||
/**
|
||||
* Simple converter from and to a {@link YamlContract} to a collection of {@link Contract}
|
||||
*
|
||||
@@ -71,6 +72,15 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
List<YamlContract> read(byte[] bytes) {
|
||||
try {
|
||||
return Collections.singletonList(this.mapper.readValue(bytes, YamlContract))
|
||||
} catch (Exception e) {
|
||||
return this.mapper.readerForListOf(YamlContract.class).readValue(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
protected String name(YamlContract contract) {
|
||||
return (contract.name ?:
|
||||
String.valueOf(Math.abs(contract.hashCode()))) + ".yml"
|
||||
|
||||
@@ -115,6 +115,7 @@ class SingleContractMetadata {
|
||||
final String definedOutputTestContentType
|
||||
final ContentType outputTestContentType
|
||||
final ContentType evaluatedOutputTestContentType
|
||||
String methodName
|
||||
private final boolean http
|
||||
|
||||
SingleContractMetadata(Contract currentContract, ContractMetadata contractMetadata) {
|
||||
@@ -222,6 +223,13 @@ class SingleContractMetadata {
|
||||
}
|
||||
|
||||
String methodName() {
|
||||
if (this.methodName == null) {
|
||||
this.methodName = calculateMethodName()
|
||||
}
|
||||
return this.methodName
|
||||
}
|
||||
|
||||
private String calculateMethodName() {
|
||||
if (contract.name) {
|
||||
String name = NamesUtil.
|
||||
camelCase(NamesUtil.convertIllegalPackageChars(contract.name))
|
||||
|
||||
@@ -21,15 +21,28 @@ import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
class BodyReader {
|
||||
|
||||
private static final Log log = LogFactory.getLog(BodyReader.class);
|
||||
|
||||
private final GeneratedClassMetaData generatedClassMetaData;
|
||||
|
||||
private final YamlContractConverter converter = new YamlContractConverter();
|
||||
|
||||
BodyReader(GeneratedClassMetaData generatedClassMetaData) {
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
}
|
||||
@@ -49,30 +62,58 @@ class BodyReader {
|
||||
return "new String(" + readBytesFromFileString(metadata, property, side) + ")";
|
||||
}
|
||||
|
||||
void storeContractAsYaml(SingleContractMetadata metadata) {
|
||||
Contract contract = metadata.getContract();
|
||||
List<YamlContract> contracts = this.converter
|
||||
.convertTo(Collections.singleton(contract));
|
||||
Map<String, byte[]> store = this.converter.store(contracts);
|
||||
store.forEach(
|
||||
(name, bytes) -> writeFileForBothIdeAndBuildTool(metadata, bytes, name));
|
||||
}
|
||||
|
||||
private String byteBodyToAFileForTestMethod(SingleContractMetadata metadata,
|
||||
FromFileProperty property, CommunicationType side) {
|
||||
GeneratedClassDataForMethod classDataForMethod = new GeneratedClassDataForMethod(
|
||||
this.generatedClassMetaData.generatedClassData, metadata.methodName());
|
||||
GeneratedClassDataForMethod classDataForMethod = classDataForMethod(metadata);
|
||||
String newFileName = classDataForMethod.getMethodName() + "_"
|
||||
+ side.name().toLowerCase() + "_" + property.fileName();
|
||||
writeFileForBothIdeAndBuildTool(metadata, property.asBytes(), newFileName);
|
||||
return newFileName;
|
||||
}
|
||||
|
||||
private GeneratedClassDataForMethod classDataForMethod(
|
||||
SingleContractMetadata metadata) {
|
||||
return new GeneratedClassDataForMethod(
|
||||
this.generatedClassMetaData.generatedClassData, metadata.methodName());
|
||||
}
|
||||
|
||||
private void writeFileForBothIdeAndBuildTool(SingleContractMetadata metadata,
|
||||
byte[] bytes, String newFileName) {
|
||||
GeneratedClassDataForMethod classDataForMethod = classDataForMethod(metadata);
|
||||
java.nio.file.Path parent = classDataForMethod.testClassPath().getParent();
|
||||
if (parent == null) {
|
||||
parent = classDataForMethod.testClassPath();
|
||||
}
|
||||
File newFile = new File(parent.toFile(), newFileName);
|
||||
if (newFile.exists()) {
|
||||
return;
|
||||
}
|
||||
// for IDE
|
||||
try {
|
||||
Files.write(newFile.toPath(), property.asBytes());
|
||||
Path path = newFile.toPath();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Writing file for [" + path
|
||||
+ "] for body reading in generated test (for IDE)");
|
||||
}
|
||||
Files.write(path, bytes);
|
||||
// for plugin
|
||||
generatedTestResourcesFileBytes(property, newFile);
|
||||
generatedTestResourcesFileBytes(bytes, newFile);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return newFileName;
|
||||
}
|
||||
|
||||
private void generatedTestResourcesFileBytes(FromFileProperty property, File newFile)
|
||||
private void generatedTestResourcesFileBytes(byte[] bytes, File newFile)
|
||||
throws IOException {
|
||||
Assert.notNull(
|
||||
this.generatedClassMetaData.configProperties.getGeneratedTestSourcesDir(),
|
||||
@@ -89,7 +130,12 @@ class BodyReader {
|
||||
.getGeneratedTestResourcesDir(),
|
||||
relativePath.toString());
|
||||
newFileInGeneratedTestSources.getParentFile().mkdirs();
|
||||
Files.write(newFileInGeneratedTestSources.toPath(), property.asBytes());
|
||||
Path generatedTestSourceFilePath = newFileInGeneratedTestSources.toPath();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Writing file for [" + generatedTestSourceFilePath
|
||||
+ "] for body reading in generated test (Build tool)");
|
||||
}
|
||||
Files.write(generatedTestSourceFilePath, bytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,15 +22,23 @@ class MessagingBodyWhen implements When {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
MessagingBodyWhen(BlockBuilder blockBuilder) {
|
||||
private final BodyReader bodyReader;
|
||||
|
||||
MessagingBodyWhen(BlockBuilder blockBuilder, GeneratedClassMetaData metaData) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.bodyReader = new BodyReader(metaData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<When> apply(SingleContractMetadata metadata) {
|
||||
this.blockBuilder.addIndented("contractVerifierMessaging.send(inputMessage, \""
|
||||
+ metadata.getContract().getInput().getMessageFrom().getServerValue()
|
||||
+ "\")").addEndingIfNotPresent();
|
||||
this.bodyReader.storeContractAsYaml(metadata);
|
||||
this.blockBuilder
|
||||
.addIndented("contractVerifierMessaging.send(inputMessage, \"" + metadata
|
||||
.getContract().getInput().getMessageFrom().getServerValue()
|
||||
+ "\",")
|
||||
.addEmptyLine().indent()
|
||||
.addIndented("contract(this, \"" + metadata.methodName() + ".yml\"))")
|
||||
.addEndingIfNotPresent().unindent();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,21 +28,28 @@ class MessagingReceiveMessageThen implements Then, BodyMethodVisitor {
|
||||
|
||||
private final ComparisonBuilder comparisonBuilder;
|
||||
|
||||
private final BodyReader bodyReader;
|
||||
|
||||
MessagingReceiveMessageThen(BlockBuilder blockBuilder,
|
||||
GeneratedClassMetaData generatedClassMetaData,
|
||||
ComparisonBuilder comparisonBuilder) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
this.comparisonBuilder = comparisonBuilder;
|
||||
this.bodyReader = new BodyReader(generatedClassMetaData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<Then> apply(SingleContractMetadata singleContractMetadata) {
|
||||
OutputMessage outputMessage = singleContractMetadata.getContract()
|
||||
.getOutputMessage();
|
||||
this.blockBuilder.addLineWithEnding(
|
||||
this.bodyReader.storeContractAsYaml(singleContractMetadata);
|
||||
this.blockBuilder.addIndented(
|
||||
"ContractVerifierMessage response = contractVerifierMessaging.receive("
|
||||
+ sentToValue(outputMessage.getSentTo().getServerValue()) + ")");
|
||||
+ sentToValue(outputMessage.getSentTo().getServerValue()) + ",")
|
||||
.addEmptyLine().indent().addIndented("contract(this, \""
|
||||
+ singleContractMetadata.methodName() + ".yml\"))")
|
||||
.unindent().addEndingIfNotPresent().addEmptyLine();
|
||||
this.blockBuilder.addLineWithEnding(
|
||||
this.comparisonBuilder.assertThatIsNotNull("response"));
|
||||
return this;
|
||||
|
||||
@@ -26,16 +26,13 @@ class MessagingWhen implements When, BodyMethodVisitor {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
private final GeneratedClassMetaData generatedClassMetaData;
|
||||
|
||||
private final List<When> whens = new LinkedList<>();
|
||||
|
||||
MessagingWhen(BlockBuilder blockBuilder,
|
||||
GeneratedClassMetaData generatedClassMetaData) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
this.whens.addAll(Arrays.asList(new MessagingTriggeredByWhen(this.blockBuilder),
|
||||
new MessagingBodyWhen(this.blockBuilder),
|
||||
new MessagingBodyWhen(this.blockBuilder, generatedClassMetaData),
|
||||
new MessagingAssertThatWhen(this.blockBuilder)));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.NamesUtil;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -33,7 +34,8 @@ class NameProvider {
|
||||
private static final Log log = LogFactory.getLog(NameProvider.class);
|
||||
|
||||
String methodName(SingleContractMetadata singleContractMetadata) {
|
||||
return "validate_" + generateMethodName(singleContractMetadata);
|
||||
return ContractVerifierUtil.TEST_METHOD_PREFIX
|
||||
+ generateMethodName(singleContractMetadata);
|
||||
}
|
||||
|
||||
private String generateMethodName(SingleContractMetadata singleContractMetadata) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.cloud.contract.verifier.converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -54,7 +55,34 @@ public class YamlContract {
|
||||
|
||||
public boolean inProgress;
|
||||
|
||||
public Map<String, Object> metadata;
|
||||
public Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
YamlContract that = (YamlContract) o;
|
||||
return ignored == that.ignored && inProgress == that.inProgress
|
||||
&& Objects.equals(this.request, that.request)
|
||||
&& Objects.equals(this.response, that.response)
|
||||
&& Objects.equals(this.input, that.input)
|
||||
&& Objects.equals(this.outputMessage, that.outputMessage)
|
||||
&& Objects.equals(this.description, that.description)
|
||||
&& Objects.equals(this.label, that.label)
|
||||
&& Objects.equals(this.name, that.name)
|
||||
&& Objects.equals(this.priority, that.priority)
|
||||
&& this.metadata.equals(that.metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(request, response, input, outputMessage, description, label,
|
||||
name, priority, ignored, inProgress, metadata);
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ package org.springframework.cloud.contract.verifier.messaging;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
|
||||
/**
|
||||
* Core interface that allows you to receive messages.
|
||||
*
|
||||
@@ -38,13 +42,37 @@ public interface MessageVerifierReceiver<M> {
|
||||
* @param timeUnit param to define the unit of timeout
|
||||
* @return received message
|
||||
*/
|
||||
M receive(String destination, long timeout, TimeUnit timeUnit);
|
||||
default M receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
return receive(destination, timeout, timeUnit, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the message from the given destination. A default timeout will be applied.
|
||||
* @param destination destination from which the message will be received
|
||||
* @return received message
|
||||
*/
|
||||
M receive(String destination);
|
||||
default M receive(String destination) {
|
||||
return receive(destination, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the message from the given destination. You can provide the timeout for
|
||||
* receiving that message.
|
||||
* @param destination destination from which the message will be received
|
||||
* @param timeout timeout to wait for the message
|
||||
* @param timeUnit param to define the unit of timeout
|
||||
* @param contract contract related to this method
|
||||
* @return received message
|
||||
*/
|
||||
M receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
@Nullable YamlContract contract);
|
||||
|
||||
/**
|
||||
* Receives the message from the given destination. A default timeout will be applied.
|
||||
* @param destination destination from which the message will be received
|
||||
* @param contract contract related to this method
|
||||
* @return received message
|
||||
*/
|
||||
M receive(String destination, YamlContract contract);
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ package org.springframework.cloud.contract.verifier.messaging;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
|
||||
/**
|
||||
* Core interface that allows you to send messages.
|
||||
*
|
||||
@@ -35,7 +39,9 @@ public interface MessageVerifierSender<M> {
|
||||
* @param message to send
|
||||
* @param destination destination to which the message will be sent
|
||||
*/
|
||||
void send(M message, String destination);
|
||||
default void send(M message, String destination) {
|
||||
send(message, destination, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given payload with headers, to the given destination.
|
||||
@@ -44,6 +50,27 @@ public interface MessageVerifierSender<M> {
|
||||
* @param headers headers to send
|
||||
* @param destination destination to which the message will be sent
|
||||
*/
|
||||
<T> void send(T payload, Map<String, Object> headers, String destination);
|
||||
default <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(payload, headers, destination, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message to the given destination.
|
||||
* @param message to send
|
||||
* @param destination destination to which the message will be sent
|
||||
* @param contract contract related to this method
|
||||
*/
|
||||
void send(M message, String destination, @Nullable YamlContract contract);
|
||||
|
||||
/**
|
||||
* Sends the given payload with headers, to the given destination.
|
||||
* @param <T> payload type
|
||||
* @param payload payload to send
|
||||
* @param headers headers to send
|
||||
* @param destination destination to which the message will be sent
|
||||
* @param contract contract related to this method
|
||||
*/
|
||||
<T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
@Nullable YamlContract contract);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.amqp;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
|
||||
/**
|
||||
* Represents metadata for AMQP based communication.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class AmqpMetadata implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Key under which this metadata entry can be found in contract's metadata.
|
||||
*/
|
||||
public static final String METADATA_KEY = "amqp";
|
||||
|
||||
/**
|
||||
* Metadata for the input message.
|
||||
*/
|
||||
private MessageAmqpMetadata input = new MessageAmqpMetadata();
|
||||
|
||||
/**
|
||||
* Metadata for the output message.
|
||||
*/
|
||||
private MessageAmqpMetadata outputMessage = new MessageAmqpMetadata();
|
||||
|
||||
public MessageAmqpMetadata getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
public void setInput(MessageAmqpMetadata input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public MessageAmqpMetadata getOutputMessage() {
|
||||
return this.outputMessage;
|
||||
}
|
||||
|
||||
public void setOutputMessage(MessageAmqpMetadata outputMessage) {
|
||||
this.outputMessage = outputMessage;
|
||||
}
|
||||
|
||||
public static AmqpMetadata fromMetadata(Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, AmqpMetadata.METADATA_KEY,
|
||||
new AmqpMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Metadata for AMQP based communication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class> additionalClassesToLookAt() {
|
||||
return Collections.singletonList(MessageProperties.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* AMQP message metadata.
|
||||
*/
|
||||
public static class MessageAmqpMetadata {
|
||||
|
||||
/**
|
||||
* Spring AMQP message properties.
|
||||
*/
|
||||
private MessageProperties messageProperties;
|
||||
|
||||
/**
|
||||
* Properties related to connecting to a real broker.
|
||||
*/
|
||||
private ConnectToBroker connectToBroker = new ConnectToBroker();
|
||||
|
||||
public MessageProperties getMessageProperties() {
|
||||
return this.messageProperties;
|
||||
}
|
||||
|
||||
public void setMessageProperties(MessageProperties messageProperties) {
|
||||
this.messageProperties = messageProperties;
|
||||
}
|
||||
|
||||
public ConnectToBroker getConnectToBroker() {
|
||||
return this.connectToBroker;
|
||||
}
|
||||
|
||||
public void setConnectToBroker(ConnectToBroker connectToBroker) {
|
||||
this.connectToBroker = connectToBroker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Options related to connecting to the real broker.
|
||||
*/
|
||||
public static class ConnectToBroker {
|
||||
|
||||
/**
|
||||
* If set, will append any options to the existing ones that define connection to
|
||||
* the broker.
|
||||
*/
|
||||
private String additionalOptions;
|
||||
|
||||
/**
|
||||
* If set, will declare a queue with given name and bind it to the provided
|
||||
* exchange from the contract.
|
||||
*/
|
||||
private String declareQueueWithName;
|
||||
|
||||
public String getAdditionalOptions() {
|
||||
return this.additionalOptions;
|
||||
}
|
||||
|
||||
public void setAdditionalOptions(String additionalOptions) {
|
||||
this.additionalOptions = additionalOptions;
|
||||
}
|
||||
|
||||
public String getDeclareQueueWithName() {
|
||||
return declareQueueWithName;
|
||||
}
|
||||
|
||||
public void setDeclareQueueWithName(String declareQueueWithName) {
|
||||
this.declareQueueWithName = declareQueueWithName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.mockito.ArgumentMatchers;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageListener;
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.core.MessagePropertiesBuilder;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
@@ -36,7 +37,10 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
|
||||
import org.springframework.amqp.support.AmqpHeaders;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.mockito.Matchers.eq;
|
||||
@@ -96,7 +100,8 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
Message message = org.springframework.amqp.core.MessageBuilder
|
||||
.withBody(((String) payload).getBytes())
|
||||
.andProperties(MessagePropertiesBuilder.newInstance()
|
||||
@@ -111,7 +116,27 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
message.getMessageProperties().setReceivedRoutingKey(
|
||||
header(headers, AmqpHeaders.RECEIVED_ROUTING_KEY));
|
||||
}
|
||||
send(message, destination);
|
||||
send(message, destination, contract);
|
||||
}
|
||||
|
||||
public void mergeMessagePropertiesFromMetadata(YamlContract contract,
|
||||
Message message) {
|
||||
if (contract != null
|
||||
&& contract.metadata.containsKey(AmqpMetadata.METADATA_KEY)) {
|
||||
AmqpMetadata amqpMetadata = AmqpMetadata.fromMetadata(contract.metadata);
|
||||
ContractVerifierMessageMetadata messageMetadata = ContractVerifierMessageMetadata
|
||||
.fromMetadata(contract.metadata);
|
||||
boolean isInput = isInputMessage(messageMetadata);
|
||||
MessageProperties fromMetadata = isInput
|
||||
? amqpMetadata.getInput().getMessageProperties()
|
||||
: amqpMetadata.getOutputMessage().getMessageProperties();
|
||||
MetadataUtil.merge(message.getMessageProperties(), fromMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInputMessage(ContractVerifierMessageMetadata messageMetadata) {
|
||||
return messageMetadata
|
||||
.getMessageType() == ContractVerifierMessageMetadata.MessageType.INPUT;
|
||||
}
|
||||
|
||||
private String header(Map<String, Object> headers, String headerName) {
|
||||
@@ -130,7 +155,8 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message message, String destination) {
|
||||
public void send(Message message, String destination, YamlContract contract) {
|
||||
mergeMessagePropertiesFromMetadata(contract, message);
|
||||
final String routingKey = message.getMessageProperties().getReceivedRoutingKey();
|
||||
List<SimpleMessageListenerContainer> listenerContainers = this.messageListenerAccessor
|
||||
.getListenerContainersForDestination(destination, routingKey);
|
||||
@@ -178,7 +204,8 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
|
||||
ArgumentCaptor<String> routingKeyCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(this.rabbitTemplate, atLeastOnce()).send(eq(destination),
|
||||
@@ -207,8 +234,8 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ import org.apache.camel.support.DefaultExchange;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
@@ -40,21 +42,35 @@ public class CamelStubMessages implements MessageVerifier<Message> {
|
||||
|
||||
private final CamelContext context;
|
||||
|
||||
private final ProducerTemplate producerTemplate;
|
||||
|
||||
private final ConsumerTemplate consumerTemplate;
|
||||
|
||||
private final ContractVerifierCamelMessageBuilder builder;
|
||||
|
||||
@Autowired
|
||||
public CamelStubMessages(CamelContext context) {
|
||||
|
||||
public CamelStubMessages(CamelContext context, ProducerTemplate producerTemplate,
|
||||
ConsumerTemplate consumerTemplate) {
|
||||
this.context = context;
|
||||
this.producerTemplate = producerTemplate;
|
||||
this.consumerTemplate = consumerTemplate;
|
||||
this.builder = new ContractVerifierCamelMessageBuilder(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message message, String destination) {
|
||||
public void send(Message message, String destination, YamlContract contract) {
|
||||
try {
|
||||
ProducerTemplate producerTemplate = this.context.createProducerTemplate();
|
||||
Exchange exchange = new DefaultExchange(this.context);
|
||||
exchange.setIn(message);
|
||||
producerTemplate.send(destination, exchange);
|
||||
StandaloneMetadata standaloneMetadata = StandaloneMetadata
|
||||
.fromMetadata(contract != null ? contract.metadata : null);
|
||||
ContractVerifierMessageMetadata verifierMessageMetadata = ContractVerifierMessageMetadata
|
||||
.fromMetadata(contract != null ? contract.metadata : null);
|
||||
String finalDestination = finalDestination(destination,
|
||||
additionalOptions(verifierMessageMetadata, standaloneMetadata),
|
||||
verifierMessageMetadata);
|
||||
log.info("Will send a message to URI [" + finalDestination + "]");
|
||||
this.producerTemplate.send(finalDestination, exchange);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("Exception occurred while trying to send a message [" + message
|
||||
@@ -63,16 +79,47 @@ public class CamelStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(this.builder.create(payload, headers), destination);
|
||||
private String additionalOptions(
|
||||
ContractVerifierMessageMetadata verifierMessageMetadata,
|
||||
StandaloneMetadata metadata) {
|
||||
return verifierMessageMetadata
|
||||
.getMessageType() == ContractVerifierMessageMetadata.MessageType.INPUT
|
||||
? metadata.getInput().getAdditionalOptions()
|
||||
: metadata.getOutputMessage().getAdditionalOptions();
|
||||
}
|
||||
|
||||
public String finalDestination(String destination, String additionalOpts,
|
||||
ContractVerifierMessageMetadata verifierMessageMetadata) {
|
||||
String finalDestination = destination;
|
||||
if (verifierMessageMetadata
|
||||
.getMessageType() == ContractVerifierMessageMetadata.MessageType.SETUP) {
|
||||
return finalDestination;
|
||||
}
|
||||
if (StringUtils.hasText(additionalOpts)) {
|
||||
finalDestination = finalDestination + "?" + additionalOpts;
|
||||
}
|
||||
return finalDestination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
send(this.builder.create(payload, headers), destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
try {
|
||||
ConsumerTemplate consumerTemplate = this.context.createConsumerTemplate();
|
||||
Exchange exchange = consumerTemplate.receive(destination,
|
||||
StandaloneMetadata standaloneMetadata = StandaloneMetadata
|
||||
.fromMetadata(contract != null ? contract.metadata : null);
|
||||
ContractVerifierMessageMetadata verifierMessageMetadata = ContractVerifierMessageMetadata
|
||||
.fromMetadata(contract != null ? contract.metadata : null);
|
||||
String finalDestination = finalDestination(destination,
|
||||
additionalOptions(verifierMessageMetadata, standaloneMetadata),
|
||||
verifierMessageMetadata);
|
||||
log.info("Will receive a message from URI [" + finalDestination + "]");
|
||||
Exchange exchange = this.consumerTemplate.receive(finalDestination,
|
||||
timeUnit.toMillis(timeout));
|
||||
return exchange != null ? exchange.getIn() : null;
|
||||
}
|
||||
@@ -84,8 +131,8 @@ public class CamelStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package org.springframework.cloud.contract.verifier.messaging.camel;
|
||||
|
||||
import org.apache.camel.CamelContext;
|
||||
import org.apache.camel.ConsumerTemplate;
|
||||
import org.apache.camel.Message;
|
||||
import org.apache.camel.ProducerTemplate;
|
||||
import org.apache.camel.spring.boot.CamelAutoConfiguration;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
@@ -47,8 +49,9 @@ public class ContractVerifierCamelConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
MessageVerifier<Message> contractVerifierMessageExchange(CamelContext camelContext) {
|
||||
return new CamelStubMessages(camelContext);
|
||||
MessageVerifier<Message> contractVerifierMessageExchange(CamelContext camelContext,
|
||||
ProducerTemplate producerTemplate, ConsumerTemplate consumerTemplate) {
|
||||
return new CamelStubMessages(camelContext, producerTemplate, consumerTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -68,6 +71,9 @@ class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
|
||||
|
||||
@Override
|
||||
protected ContractVerifierMessage convert(Message receive) {
|
||||
if (receive == null) {
|
||||
return null;
|
||||
}
|
||||
return new ContractVerifierMessage(receive.getBody(), receive.getHeaders());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.camel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
|
||||
/**
|
||||
* Represents metadata for standalone communication.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class StandaloneMetadata implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Key under which this metadata entry can be found in contract's metadata.
|
||||
*/
|
||||
public static final String METADATA_KEY = "standalone";
|
||||
|
||||
/**
|
||||
* Metadata for the setup message.
|
||||
*/
|
||||
private SetupMetadata setup = new SetupMetadata();
|
||||
|
||||
/**
|
||||
* Metadata for the input message.
|
||||
*/
|
||||
private MessageMetadata input = new MessageMetadata();
|
||||
|
||||
/**
|
||||
* Metadata for the output message.
|
||||
*/
|
||||
private MessageMetadata outputMessage = new MessageMetadata();
|
||||
|
||||
public SetupMetadata getSetup() {
|
||||
return this.setup;
|
||||
}
|
||||
|
||||
public void setSetup(SetupMetadata setup) {
|
||||
this.setup = setup;
|
||||
}
|
||||
|
||||
public MessageMetadata getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
public void setInput(MessageMetadata input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public MessageMetadata getOutputMessage() {
|
||||
return this.outputMessage;
|
||||
}
|
||||
|
||||
public void setOutputMessage(MessageMetadata outputMessage) {
|
||||
this.outputMessage = outputMessage;
|
||||
}
|
||||
|
||||
public static StandaloneMetadata fromMetadata(Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, StandaloneMetadata.METADATA_KEY,
|
||||
new StandaloneMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Metadata for standalone communication - with running middleware";
|
||||
}
|
||||
|
||||
/**
|
||||
* Message metadata.
|
||||
*/
|
||||
public static class MessageMetadata {
|
||||
|
||||
/**
|
||||
* If set, will append any options to the existing ones that define connection to
|
||||
* the broker.
|
||||
*/
|
||||
private String additionalOptions;
|
||||
|
||||
public String getAdditionalOptions() {
|
||||
return this.additionalOptions;
|
||||
}
|
||||
|
||||
public void setAdditionalOptions(String additionalOptions) {
|
||||
this.additionalOptions = additionalOptions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup metadata.
|
||||
*/
|
||||
public static class SetupMetadata {
|
||||
|
||||
/**
|
||||
* If set, will be set as the full URI.
|
||||
*/
|
||||
private String options;
|
||||
|
||||
public String getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public void setOptions(String options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -46,12 +47,13 @@ public class SpringIntegrationStubMessages implements MessageVerifier<Message<?>
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(this.builder.create(payload, headers), destination);
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
send(this.builder.create(payload, headers), destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
try {
|
||||
MessageChannel messageChannel = this.context.getBean(destination,
|
||||
MessageChannel.class);
|
||||
@@ -65,7 +67,8 @@ public class SpringIntegrationStubMessages implements MessageVerifier<Message<?>
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
try {
|
||||
PollableChannel messageChannel = this.context.getBean(destination,
|
||||
PollableChannel.class);
|
||||
@@ -79,8 +82,8 @@ public class SpringIntegrationStubMessages implements MessageVerifier<Message<?>
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message<?> receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.internal;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
|
||||
/**
|
||||
* Metadata representation of the Contract Verifier messaging.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class ContractVerifierMessageMetadata implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Metadata entry in the contract.
|
||||
*/
|
||||
public static final String METADATA_KEY = "verifierMessage";
|
||||
|
||||
private MessageType messageType;
|
||||
|
||||
public ContractVerifierMessageMetadata(MessageType messageType) {
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public ContractVerifierMessageMetadata() {
|
||||
}
|
||||
|
||||
public MessageType getMessageType() {
|
||||
return this.messageType;
|
||||
}
|
||||
|
||||
public void setMessageType(MessageType messageType) {
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public static ContractVerifierMessageMetadata fromMetadata(
|
||||
Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, METADATA_KEY,
|
||||
new ContractVerifierMessageMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Internal metadata entries used by the framework";
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of a message.
|
||||
*/
|
||||
public enum MessageType {
|
||||
|
||||
/**
|
||||
* Setup message.
|
||||
*/
|
||||
SETUP,
|
||||
|
||||
/**
|
||||
* Input message.
|
||||
*/
|
||||
INPUT,
|
||||
|
||||
/**
|
||||
* Output message.
|
||||
*/
|
||||
OUTPUT
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,9 @@ package org.springframework.cloud.contract.verifier.messaging.internal;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
|
||||
/**
|
||||
@@ -36,12 +39,35 @@ public class ContractVerifierMessaging<M> {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
public void send(ContractVerifierMessage message, String destination,
|
||||
@Nullable YamlContract contract) {
|
||||
if (contract != null) {
|
||||
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.INPUT);
|
||||
}
|
||||
this.exchange.send(message.getPayload(), message.getHeaders(), destination,
|
||||
contract);
|
||||
}
|
||||
|
||||
public void send(ContractVerifierMessage message, String destination) {
|
||||
this.exchange.send(message.getPayload(), message.getHeaders(), destination);
|
||||
send(message, destination, null);
|
||||
}
|
||||
|
||||
public ContractVerifierMessage receive(String destination,
|
||||
@Nullable YamlContract contract) {
|
||||
if (contract != null) {
|
||||
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.OUTPUT);
|
||||
}
|
||||
return convert(this.exchange.receive(destination, contract));
|
||||
}
|
||||
|
||||
private void setMessageType(YamlContract contract,
|
||||
ContractVerifierMessageMetadata.MessageType output) {
|
||||
contract.metadata.put(ContractVerifierMessageMetadata.METADATA_KEY,
|
||||
new ContractVerifierMessageMetadata(output));
|
||||
}
|
||||
|
||||
public ContractVerifierMessage receive(String destination) {
|
||||
return convert(this.exchange.receive(destination));
|
||||
return receive(destination, null);
|
||||
}
|
||||
|
||||
public <T> ContractVerifierMessage create(T payload, Map<String, Object> headers) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import javax.jms.JMSException;
|
||||
import javax.jms.Message;
|
||||
import javax.jms.Session;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.jms.core.MessagePostProcessor;
|
||||
@@ -38,23 +39,25 @@ class JmsStubMessages implements MessageVerifier<Message> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message message, String destination) {
|
||||
public void send(Message message, String destination, YamlContract contract) {
|
||||
jmsTemplate.convertAndSend(destination, message, new ReplyToProcessor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
jmsTemplate.setReceiveTimeout(timeUnit.toMillis(timeout));
|
||||
return jmsTemplate.receive(destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
public void send(Object payload, Map headers, String destination,
|
||||
YamlContract contract) {
|
||||
jmsTemplate.send(destination, session -> {
|
||||
Message message = createMessage(session, payload);
|
||||
setHeaders(message, headers);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.kafka;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
|
||||
/**
|
||||
* Represents metadata for Kafka based communication.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class KafkaMetadata implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Key under which this metadata entry can be found in contract's metadata.
|
||||
*/
|
||||
public static final String METADATA_KEY = "kafka";
|
||||
|
||||
/**
|
||||
* Metadata for the input message.
|
||||
*/
|
||||
private MessageKafkaMetadata input = new MessageKafkaMetadata();
|
||||
|
||||
/**
|
||||
* Metadata for the output message.
|
||||
*/
|
||||
private MessageKafkaMetadata outputMessage = new MessageKafkaMetadata();
|
||||
|
||||
public MessageKafkaMetadata getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
public void setInput(MessageKafkaMetadata input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public MessageKafkaMetadata getOutputMessage() {
|
||||
return this.outputMessage;
|
||||
}
|
||||
|
||||
public void setOutputMessage(MessageKafkaMetadata outputMessage) {
|
||||
this.outputMessage = outputMessage;
|
||||
}
|
||||
|
||||
public static KafkaMetadata fromMetadata(Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, KafkaMetadata.METADATA_KEY,
|
||||
new KafkaMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Metadata for Kafka based communication";
|
||||
}
|
||||
|
||||
/**
|
||||
* Kafka message metadata.
|
||||
*/
|
||||
public static class MessageKafkaMetadata {
|
||||
|
||||
/**
|
||||
* Properties related to connecting to a real broker.
|
||||
*/
|
||||
private ConnectToBroker connectToBroker = new ConnectToBroker();
|
||||
|
||||
public ConnectToBroker getConnectToBroker() {
|
||||
return this.connectToBroker;
|
||||
}
|
||||
|
||||
public void setConnectToBroker(ConnectToBroker connectToBroker) {
|
||||
this.connectToBroker = connectToBroker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Options related to connecting to the real broker.
|
||||
*/
|
||||
public static class ConnectToBroker {
|
||||
|
||||
/**
|
||||
* If set, will append any options to the existing ones that define connection to
|
||||
* the broker.
|
||||
*/
|
||||
private String additionalOptions;
|
||||
|
||||
public String getAdditionalOptions() {
|
||||
return this.additionalOptions;
|
||||
}
|
||||
|
||||
public void setAdditionalOptions(String additionalOptions) {
|
||||
this.additionalOptions = additionalOptions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.apache.kafka.common.header.Header;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
|
||||
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
@@ -56,7 +57,7 @@ class KafkaStubMessages implements MessageVerifier<Message<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
String defaultTopic = this.kafkaTemplate.getDefaultTopic();
|
||||
try {
|
||||
this.kafkaTemplate.setDefaultTopic(destination);
|
||||
@@ -76,20 +77,22 @@ class KafkaStubMessages implements MessageVerifier<Message<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
return this.receiver.receive(destination, timeout, timeUnit);
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
return this.receiver.receive(destination, timeout, timeUnit, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
public void send(Object payload, Map headers, String destination,
|
||||
YamlContract contract) {
|
||||
Message<?> message = MessageBuilder.createMessage(payload,
|
||||
new MessageHeaders(headers));
|
||||
send(message, destination);
|
||||
send(message, destination, contract);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -104,7 +107,8 @@ class Receiver {
|
||||
this.consumers = consumers;
|
||||
}
|
||||
|
||||
Message receive(String topic, long timeout, TimeUnit timeUnit) {
|
||||
Message receive(String topic, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
Consumer consumer = this.consumers.get(topic);
|
||||
if (consumer == null) {
|
||||
throw new IllegalStateException(
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.messaging.noop;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
|
||||
/**
|
||||
@@ -27,20 +28,22 @@ import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
public class NoOpStubMessages implements MessageVerifier<Object> {
|
||||
|
||||
@Override
|
||||
public void send(Object message, String destination) {
|
||||
public void send(Object message, String destination, YamlContract contract) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Object receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receive(String destination) {
|
||||
public Object receive(String destination, YamlContract contract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -47,12 +48,13 @@ class StreamFromBinderMappingMessageSender implements MessageVerifierSender<Mess
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(this.builder.create(payload, headers), destination);
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
send(this.builder.create(payload, headers), destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
try {
|
||||
MessageChannel messageChannel = this.context.getBean(this.resolver
|
||||
.resolvedDestination(destination, DefaultChannels.OUTPUT),
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -43,12 +44,13 @@ class StreamInputDestinationMessageSender implements MessageVerifierSender<Messa
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(this.builder.create(payload, headers), destination);
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
send(this.builder.create(payload, headers), destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
try {
|
||||
InputDestination inputDestination = this.context
|
||||
.getBean(InputDestination.class);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -39,7 +40,8 @@ class StreamOutputDestinationMessageReceiver
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
try {
|
||||
OutputDestination outputDestination = this.context
|
||||
.getBean(OutputDestination.class);
|
||||
@@ -53,8 +55,8 @@ class StreamOutputDestinationMessageReceiver
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message<?> receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,15 @@ import java.util.concurrent.TimeUnit;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
|
||||
class StreamPollableChannelMessageReceiver
|
||||
implements MessageVerifierReceiver<Message<?>> {
|
||||
@@ -36,29 +41,46 @@ class StreamPollableChannelMessageReceiver
|
||||
|
||||
private final DestinationResolver destinationResolver;
|
||||
|
||||
private final PollableChannel messageChannel;
|
||||
|
||||
StreamPollableChannelMessageReceiver(ApplicationContext context) {
|
||||
this.context = context;
|
||||
this.destinationResolver = new DestinationResolver(context);
|
||||
this.messageChannel = new QueueChannel(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
MessageHandler handler = this.messageChannel::send;
|
||||
MessageChannel channel = null;
|
||||
try {
|
||||
PollableChannel messageChannel = this.context.getBean(this.destinationResolver
|
||||
.resolvedDestination(destination, DefaultChannels.INPUT),
|
||||
PollableChannel.class);
|
||||
return messageChannel.receive(timeUnit.toMillis(timeout));
|
||||
channel = this.context.getBean(this.destinationResolver.resolvedDestination(
|
||||
destination, DefaultChannels.INPUT), MessageChannel.class);
|
||||
if (channel instanceof SubscribableChannel) {
|
||||
((SubscribableChannel) channel).subscribe(handler);
|
||||
return this.messageChannel.receive(timeUnit.toMillis(timeout));
|
||||
}
|
||||
else if (channel instanceof PollableChannel) {
|
||||
return ((PollableChannel) channel).receive(timeUnit.toMillis(timeout));
|
||||
}
|
||||
throw new IllegalStateException("Unsupported channel type");
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("Exception occurred while trying to read a message from "
|
||||
+ " a channel with name [" + destination + "]", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
finally {
|
||||
if (channel instanceof SubscribableChannel) {
|
||||
((SubscribableChannel) channel).unsubscribe(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
public Message<?> receive(String destination, YamlContract contract) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
|
||||
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -45,12 +46,13 @@ class StreamStubMessageSender implements MessageVerifierSender<Message<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
send(this.builder.create(payload, headers), destination);
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
send(this.builder.create(payload, headers), destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
try {
|
||||
MessageChannel messageChannel = resolver().resolveDestination(destination);
|
||||
messageChannel.send(message);
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.messaging.stream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
|
||||
@@ -40,23 +41,25 @@ public class StreamStubMessages implements MessageVerifier<Message<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination) {
|
||||
this.sender.send(payload, headers, destination);
|
||||
public <T> void send(T payload, Map<String, Object> headers, String destination,
|
||||
YamlContract contract) {
|
||||
this.sender.send(payload, headers, destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
this.sender.send(message, destination);
|
||||
public void send(Message<?> message, String destination, YamlContract contract) {
|
||||
this.sender.send(message, destination, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
return this.receiver.receive(destination, timeout, timeUnit);
|
||||
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit,
|
||||
YamlContract contract) {
|
||||
return this.receiver.receive(destination, timeout, timeUnit, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(String destination) {
|
||||
return this.receiver.receive(destination);
|
||||
public Message<?> receive(String destination, YamlContract contract) {
|
||||
return this.receiver.receive(destination, contract);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -38,6 +39,8 @@ import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.xml.DOMNamespaceContext;
|
||||
|
||||
/**
|
||||
@@ -49,6 +52,12 @@ import org.springframework.cloud.contract.verifier.util.xml.DOMNamespaceContext;
|
||||
*/
|
||||
public final class ContractVerifierUtil {
|
||||
|
||||
/**
|
||||
* Prefix for the generated test names.
|
||||
*/
|
||||
// TODO: Find a better place for this.
|
||||
public static final String TEST_METHOD_PREFIX = "validate_";
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(ContractVerifierUtil.class);
|
||||
|
||||
private ContractVerifierUtil() {
|
||||
@@ -115,6 +124,34 @@ public final class ContractVerifierUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a file to bytes.
|
||||
* @param testClass - test class relative to which the file is stored
|
||||
* @param relativePath - relative path to the file
|
||||
* @return bytes of the file
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public static YamlContract contract(Object testClass, String relativePath) {
|
||||
String path = fromRelativePath(relativePath);
|
||||
byte[] bytes = fileToBytes(testClass, path);
|
||||
List<YamlContract> read = new YamlContractConverter().read(bytes);
|
||||
return read.isEmpty() ? null : read.get(0);
|
||||
}
|
||||
|
||||
static String fromRelativePath(String relativePath) {
|
||||
String path = relativePath;
|
||||
if (path.startsWith(TEST_METHOD_PREFIX)) {
|
||||
path = path.substring(TEST_METHOD_PREFIX.length());
|
||||
}
|
||||
if (path.endsWith("()")) {
|
||||
path = path.replace("()", "");
|
||||
}
|
||||
if (!path.endsWith(".yml")) {
|
||||
path = path + ".yml";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a builder for map
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFilter;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.FilterProvider;
|
||||
import com.fasterxml.jackson.databind.ser.PropertyWriter;
|
||||
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
|
||||
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Helper class that allows to work with metadata.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class MetadataUtil {
|
||||
|
||||
private static final ObjectMapper MAPPER = new VerifierObjectMapper();
|
||||
|
||||
private MetadataUtil() {
|
||||
throw new IllegalStateException("Can't instantiate a utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an object with entries from metadata for a given key.
|
||||
* @param metadata - metadata from the contract
|
||||
* @param key - key under which metadata can be found
|
||||
* @param objectToMerge - object to be filled with entries from the metadata
|
||||
* @param <T> - type of the object to merge
|
||||
* @return merged object with metadata or object without metadata entries if metadata
|
||||
* key wasn't present
|
||||
*/
|
||||
public static <T> T fromMetadata(Map<String, Object> metadata, String key,
|
||||
T objectToMerge) {
|
||||
if (metadata == null || !metadata.containsKey(key)) {
|
||||
return objectToMerge;
|
||||
}
|
||||
return merge(objectToMerge, metadata.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the object to merge.
|
||||
* @param objectToMerge - object to be filled with entries from the metadata
|
||||
* @param patch - object that is a patch for an object to merge
|
||||
* @param <T> - type of the object to merge
|
||||
* @return merged object with metadata or object without metadata entries if metadata
|
||||
* key wasn't present
|
||||
*/
|
||||
public static <T> T merge(T objectToMerge, Object patch) {
|
||||
if (patch == null) {
|
||||
return objectToMerge;
|
||||
}
|
||||
try {
|
||||
byte[] bytes = MAPPER.writer().writeValueAsBytes(patch);
|
||||
return MAPPER.readerForUpdating(objectToMerge).readValue(bytes);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static MetadataMap map() {
|
||||
return new MetadataMap();
|
||||
}
|
||||
|
||||
public static class MetadataMap implements Map<String, Object> {
|
||||
|
||||
private final Map<String, Object> delegate = new HashMap<>();
|
||||
|
||||
public MetadataMap entry(String key, Object value) {
|
||||
put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return this.delegate.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return this.delegate.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return this.delegate.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
return this.delegate.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
return this.delegate.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(Object key) {
|
||||
return this.delegate.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> m) {
|
||||
this.delegate.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
this.delegate.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return this.delegate.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
return this.delegate.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
return this.delegate.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return this.delegate.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.delegate.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VerifierObjectMapper extends ObjectMapper {
|
||||
|
||||
VerifierObjectMapper() {
|
||||
setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
|
||||
.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT)
|
||||
.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
.setDefaultPropertyInclusion(JsonInclude.Include.NON_ABSENT);
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
FilterProvider filters = new SimpleFilterProvider()
|
||||
.addFilter("non default properties", new MyFilter());
|
||||
addMixIn(Object.class, PropertyFilterMixIn.class);
|
||||
setFilterProvider(filters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonFilter("non default properties")
|
||||
class PropertyFilterMixIn {
|
||||
|
||||
}
|
||||
|
||||
class MyFilter extends SimpleBeanPropertyFilter implements Serializable {
|
||||
|
||||
private static final Map<Class, Object> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void serializeAsField(Object pojo, JsonGenerator jgen,
|
||||
SerializerProvider provider, PropertyWriter writer) throws Exception {
|
||||
if (pojo instanceof Map || pojo instanceof Collection) {
|
||||
writer.serializeAsField(pojo, jgen, provider);
|
||||
return;
|
||||
}
|
||||
Object defaultInstance = CACHE.computeIfAbsent(pojo.getClass(),
|
||||
this::defaultInstance);
|
||||
if (defaultInstance instanceof CantInstantiateThisClass
|
||||
|| !valueSameAsDefault(pojo, defaultInstance, writer.getName())) {
|
||||
writer.serializeAsField(pojo, jgen, provider);
|
||||
}
|
||||
}
|
||||
|
||||
private Object defaultInstance(Class aClass) {
|
||||
try {
|
||||
return aClass.newInstance();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new CantInstantiateThisClass();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean valueSameAsDefault(Object pojo, Object defaultInstance,
|
||||
String fieldName) {
|
||||
Field field = ReflectionUtils.findField(pojo.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
return false;
|
||||
}
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
try {
|
||||
return Objects.equals(field.get(pojo), field.get(defaultInstance));
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CantInstantiateThisClass {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for metadata objects parsed from the metadata in a contract. Will be used to
|
||||
* scan for implementations of the interface in order to build a schema of how metadata
|
||||
* can look like.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Name of the key under which this metadata entry will be present in contract's
|
||||
* metadata.
|
||||
* @return key name
|
||||
*/
|
||||
String key();
|
||||
|
||||
/**
|
||||
* Short description of the metadata. Will be used in the generated documentation.
|
||||
* @return description of the metadata.
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* Collection of additional classes to look at if one is interested.
|
||||
* @return additional classes
|
||||
*/
|
||||
default List<Class> additionalClassesToLookAt() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -126,7 +126,8 @@ class FooSpec extends Specification {
|
||||
\t\t\tbookReturnedTriggered()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -200,7 +201,8 @@ public class FooTest {
|
||||
\t\t\tbookReturnedTriggered();
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -284,10 +286,12 @@ class FooSpec extends Specification {
|
||||
\t\t\t)
|
||||
|
||||
\t\twhen:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input")
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -370,10 +374,12 @@ public class FooTest {
|
||||
\t\t\t);
|
||||
|
||||
\t\t// when:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input");
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -447,7 +453,8 @@ class FooSpec extends Specification {
|
||||
\t\t\t)
|
||||
|
||||
\t\twhen:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete")
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tbookWasDeleted()
|
||||
|
||||
\t\tthen:
|
||||
@@ -516,7 +523,8 @@ public class FooTest {
|
||||
\t\t\t);
|
||||
|
||||
\t\t// when:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete");
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tbookWasDeleted();
|
||||
|
||||
\t}
|
||||
@@ -587,10 +595,12 @@ public class FooTest {
|
||||
\t\t\t);
|
||||
|
||||
\t\t// when:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input");
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -660,10 +670,12 @@ class FooSpec extends Specification {
|
||||
\t\t\t)
|
||||
|
||||
\t\twhen:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input")
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -744,10 +756,12 @@ public class FooTest {
|
||||
\t\t\t);
|
||||
|
||||
\t\t// when:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input");
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -817,7 +831,8 @@ public class FooTest {
|
||||
\t\t\trequestIsCalled();
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -962,7 +977,8 @@ public class FooTest {
|
||||
when:
|
||||
String test = singleTestGenerator(contractDsl)
|
||||
then:
|
||||
test.contains('ContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote")')
|
||||
test.contains('ContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote"')
|
||||
test.contains('contract(this, "foo.yml"))')
|
||||
test.contains('assertThat(response).isNotNull()')
|
||||
test.contains('assertThat(response.getHeader("processId")).isNotNull()')
|
||||
test.contains('assertThat(response.getHeader("processId").toString()).matches("[\\\\S\\\\s]+")')
|
||||
@@ -1024,7 +1040,8 @@ class FooSpec extends Specification {
|
||||
\t\t\trequestIsCalled()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1095,7 +1112,8 @@ class FooSpec extends Specification {
|
||||
\t\t\trequestIsCalled()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("topic.rateablequote",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1166,7 +1184,8 @@ class FooSpec extends Specification {
|
||||
\t\t\trequestIsCalled()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive(toString())
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive(toString(),
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1240,7 +1259,8 @@ public class FooTest {
|
||||
\t\t\trequestIsCalled();
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive(toString());
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive(toString(),
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -1317,7 +1337,8 @@ class FooSpec extends Specification {
|
||||
\t\t\tfoo()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1360,7 +1381,8 @@ public class FooTest {
|
||||
\t\t\tfoo();
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -1434,10 +1456,12 @@ class FooSpec extends Specification {
|
||||
\t\t\t)
|
||||
|
||||
\t\twhen:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "foo")
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "foo",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1480,10 +1504,12 @@ public class FooTest {
|
||||
\t\t\t);
|
||||
|
||||
\t\t// when:
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "foo");
|
||||
\t\t\tcontractVerifierMessaging.send(inputMessage, "foo",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
@@ -1566,7 +1592,8 @@ class FooSpec extends Specification {
|
||||
\t\t\tcreateNewPerson()
|
||||
|
||||
\t\tthen:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("personEventsTopic")
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("personEventsTopic",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"))
|
||||
\t\t\tresponse != null
|
||||
|
||||
\t\tand:
|
||||
@@ -1622,7 +1649,8 @@ public class FooTest {
|
||||
\t\t\tcreateNewPerson();
|
||||
|
||||
\t\t// then:
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("personEventsTopic");
|
||||
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("personEventsTopic",
|
||||
\t\t\t\t\tcontract(this, "foo.yml"));
|
||||
\t\t\tassertThat(response).isNotNull();
|
||||
|
||||
\t\t// and:
|
||||
|
||||
@@ -137,6 +137,14 @@ class SingleTestGeneratorSpec extends Specification {
|
||||
tmp = tmpFolder.newFolder()
|
||||
File classpath = new File(SingleTestGeneratorSpec.class.getResource('/classpath/').toURI())
|
||||
FileSystemUtils.copyRecursively(classpath, tmp)
|
||||
def resource = SingleTestGeneratorSpec.class.getResource('/request.json')?.toURI()
|
||||
if (resource != null) {
|
||||
new File(resource)?.delete()
|
||||
}
|
||||
resource = SingleTestGeneratorSpec.class.getResource('/response.json')?.toURI()
|
||||
if (resource != null) {
|
||||
new File(resource)?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private static writeContract(File file) {
|
||||
@@ -335,7 +343,10 @@ class SingleTestGeneratorSpec extends Specification {
|
||||
}
|
||||
''')
|
||||
and:
|
||||
ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties()
|
||||
ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(
|
||||
generatedTestResourcesDir: file.parentFile,
|
||||
generatedTestSourcesDir: file.parentFile
|
||||
)
|
||||
properties.testFramework = testFramework
|
||||
ContractMetadata contract = new ContractMetadata(file.toPath(), true, 1, 2,
|
||||
convertAsCollection(new File('/'), file))
|
||||
@@ -982,7 +993,8 @@ class SingleTestGeneratorSpec extends Specification {
|
||||
File temp = tmpFolder.newFolder()
|
||||
and:
|
||||
ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(
|
||||
testFramework: testFramework, contractsDslDir: contractLocation.parentFile,
|
||||
testFramework: testFramework,
|
||||
contractsDslDir: contractLocation.parentFile,
|
||||
basePackageForTests: 'a.b',
|
||||
generatedTestSourcesDir: temp,
|
||||
generatedTestResourcesDir: tmpFolder.newFolder()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -722,7 +722,7 @@ name: "post1"
|
||||
priority: null
|
||||
ignored: false
|
||||
inProgress: false
|
||||
metadata: null
|
||||
metadata: {}
|
||||
'''
|
||||
String expectedYaml2 = '''\
|
||||
---
|
||||
@@ -765,10 +765,18 @@ name: "post2"
|
||||
priority: null
|
||||
ignored: false
|
||||
inProgress: false
|
||||
metadata: null
|
||||
metadata: {}
|
||||
'''
|
||||
when:
|
||||
Map<String, byte[]> strings = converter.store([
|
||||
Map<String, byte[]> strings = converter.store(yamlContracts())
|
||||
then:
|
||||
strings.size() == 2
|
||||
new String(strings["post1.yml"]).trim() == expectedYaml1.trim()
|
||||
new String(strings["post2.yml"]).trim() == expectedYaml2.trim()
|
||||
}
|
||||
|
||||
private List<YamlContract> yamlContracts() {
|
||||
return [
|
||||
new YamlContract(
|
||||
name: "post1",
|
||||
request: new YamlContract.Request(method: "POST", url: "/users/1"),
|
||||
@@ -777,12 +785,7 @@ metadata: null
|
||||
name: "post2",
|
||||
request: new YamlContract.Request(method: "POST", url: "/users/2"),
|
||||
response: new YamlContract.Response(status: 200)
|
||||
),
|
||||
])
|
||||
then:
|
||||
strings.size() == 2
|
||||
new String(strings["post1.yml"]).trim() == expectedYaml1.trim()
|
||||
new String(strings["post2.yml"]).trim() == expectedYaml2.trim()
|
||||
)]
|
||||
}
|
||||
|
||||
def "should parse messaging contract for [#file]"() {
|
||||
@@ -1289,6 +1292,23 @@ metadata: null
|
||||
yamlContract.request.bodyFromFileAsBytes != null
|
||||
}
|
||||
|
||||
def "should read contract from bytes"() {
|
||||
given:
|
||||
Map<String, byte[]> strings = converter.store([new YamlContract(
|
||||
name: "post1",
|
||||
request: new YamlContract.Request(method: "POST", url: "/users/1"),
|
||||
response: new YamlContract.Response(status: 200)
|
||||
)])
|
||||
when:
|
||||
List<YamlContract> yamlContracts = converter.read(strings.values().first())
|
||||
then:
|
||||
yamlContracts.size() == 1
|
||||
YamlContract yamlContract = yamlContracts.first()
|
||||
yamlContract.request.method == "POST"
|
||||
yamlContract.request.url == "/users/1"
|
||||
yamlContract.response.status == 200
|
||||
}
|
||||
|
||||
def "should convert REST YAML with XML request and response to DSL"() {
|
||||
given:
|
||||
assert converter.isAccepted(ymlRestXml)
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.cloud.contract.verifier.messaging.amqp
|
||||
|
||||
import com.rabbitmq.client.Channel
|
||||
import org.mockito.exceptions.verification.WantedButNotInvoked
|
||||
import shaded.com.google.common.collect.ImmutableMap
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.amqp.core.Binding
|
||||
@@ -32,6 +31,7 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
|
||||
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitProperties
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract
|
||||
|
||||
import static org.mockito.Mockito.mock
|
||||
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON
|
||||
@@ -63,11 +63,9 @@ class SpringAmqpStubMessagesSpec extends Specification {
|
||||
|
||||
when:
|
||||
messageVerifier.send(payload,
|
||||
ImmutableMap.builder()
|
||||
.put(DEFAULT_CLASSID_FIELD_NAME, "org.example.Some")
|
||||
.put("amqp_receivedRoutingKey", routingKey)
|
||||
.put("contentType", CONTENT_TYPE_JSON)
|
||||
.build(),
|
||||
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
|
||||
"amqp_receivedRoutingKey" : routingKey,
|
||||
"contentType" : CONTENT_TYPE_JSON],
|
||||
exchange)
|
||||
then:
|
||||
1 * messageListenerAdapter.onMessage({ Message msg ->
|
||||
@@ -77,6 +75,38 @@ class SpringAmqpStubMessagesSpec extends Specification {
|
||||
})
|
||||
}
|
||||
|
||||
def "should send amqp message with metadata id"() {
|
||||
given:
|
||||
listenerContainer.setMessageListener(messageListenerAdapter)
|
||||
listenerContainer.setQueueNames(queueName)
|
||||
Binding binding = BindingBuilder.bind(new Queue(queueName)).to(new DirectExchange(exchange)).with(routingKey)
|
||||
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [listenerContainer], [binding])
|
||||
SpringAmqpStubMessages messageVerifier = new SpringAmqpStubMessages(rabbitTemplate, messageListenerAccessor, rabbitProperties)
|
||||
|
||||
when:
|
||||
messageVerifier.send(payload,
|
||||
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
|
||||
"amqp_receivedRoutingKey" : routingKey,
|
||||
"contentType" : CONTENT_TYPE_JSON],
|
||||
exchange, new YamlContract(metadata:
|
||||
[
|
||||
"amqp": [
|
||||
"input": ["messageProperties": [
|
||||
correlationId: "correlationIdValue",
|
||||
consumerQueue: "queue"]]
|
||||
],
|
||||
"verifierMessage" : [messageType: "INPUT"]
|
||||
]))
|
||||
then:
|
||||
1 * messageListenerAdapter.onMessage({ Message msg ->
|
||||
msg.getMessageProperties().getReceivedRoutingKey() == "resource.created" &&
|
||||
msg.getMessageProperties().getContentType() == CONTENT_TYPE_JSON &&
|
||||
msg.getMessageProperties().getHeaders().get(DEFAULT_CLASSID_FIELD_NAME) == "org.example.Some" &&
|
||||
msg.getMessageProperties().getCorrelationId() == "correlationIdValue" &&
|
||||
msg.getMessageProperties().getConsumerQueue() == "queue"
|
||||
})
|
||||
}
|
||||
|
||||
def "should send amqp message for non transactional channel"() {
|
||||
given:
|
||||
rabbitProperties.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.SIMPLE)
|
||||
@@ -102,11 +132,9 @@ class SpringAmqpStubMessagesSpec extends Specification {
|
||||
|
||||
when:
|
||||
messageVerifier.send(payload,
|
||||
ImmutableMap.builder()
|
||||
.put(DEFAULT_CLASSID_FIELD_NAME, "org.example.Some")
|
||||
.put("amqp_receivedRoutingKey", routingKey)
|
||||
.put("contentType", CONTENT_TYPE_JSON)
|
||||
.build(),
|
||||
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
|
||||
"amqp_receivedRoutingKey" : routingKey,
|
||||
"contentType" : CONTENT_TYPE_JSON],
|
||||
exchange)
|
||||
then:
|
||||
createChannelCalled
|
||||
@@ -138,11 +166,9 @@ class SpringAmqpStubMessagesSpec extends Specification {
|
||||
|
||||
when:
|
||||
messageVerifier.send(payload,
|
||||
ImmutableMap.builder()
|
||||
.put(DEFAULT_CLASSID_FIELD_NAME, "org.example.Some")
|
||||
.put("amqp_receivedRoutingKey", routingKey)
|
||||
.put("contentType", CONTENT_TYPE_JSON)
|
||||
.build(),
|
||||
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
|
||||
"amqp_receivedRoutingKey" : routingKey,
|
||||
"contentType" : CONTENT_TYPE_JSON],
|
||||
exchange)
|
||||
then:
|
||||
createChannelCalled
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.amqp;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
class AmqpMetadataTests {
|
||||
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
|
||||
@Test
|
||||
void should_parse_the_metadata_entry() throws JsonProcessingException {
|
||||
// @formatter:off
|
||||
String yamlEntry = "amqp:\n"
|
||||
+ " input:\n"
|
||||
+ " connectToBroker:\n"
|
||||
+ " additionalOptions: \"foo1\"\n"
|
||||
+ " declareQueueWithName: \"foo2\"\n"
|
||||
+ " messageProperties:\n"
|
||||
+ " replyTo: \"foo3\"\n"
|
||||
+ " outputMessage:\n"
|
||||
+ " connectToBroker:\n"
|
||||
+ " additionalOptions: \"bar1\"\n"
|
||||
+ " declareQueueWithName: \"bar2\"\n"
|
||||
+ " messageProperties:\n"
|
||||
+ " replyTo: \"bar3\"\n";
|
||||
// @formatter:on
|
||||
|
||||
AmqpMetadata metadata = AmqpMetadata.fromMetadata(
|
||||
this.mapper.readerForMapOf(Object.class).readValue(yamlEntry));
|
||||
|
||||
then(metadata.getInput().getConnectToBroker().getAdditionalOptions())
|
||||
.isEqualTo("foo1");
|
||||
then(metadata.getInput().getConnectToBroker().getDeclareQueueWithName())
|
||||
.isEqualTo("foo2");
|
||||
then(metadata.getInput().getMessageProperties().getReplyTo()).isEqualTo("foo3");
|
||||
then(metadata.getOutputMessage().getConnectToBroker().getAdditionalOptions())
|
||||
.isEqualTo("bar1");
|
||||
then(metadata.getOutputMessage().getConnectToBroker().getDeclareQueueWithName())
|
||||
.isEqualTo("bar2");
|
||||
then(metadata.getOutputMessage().getMessageProperties().getReplyTo())
|
||||
.isEqualTo("bar3");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.messaging.kafka;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class KafkaMetadataTests {
|
||||
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
|
||||
@Test
|
||||
void should_parse_the_metadata_entry() throws JsonProcessingException {
|
||||
// @formatter:off
|
||||
String yamlEntry = "kafka:\n"
|
||||
+ " input:\n"
|
||||
+ " connectToBroker:\n"
|
||||
+ " additionalOptions: foo\n"
|
||||
+ " outputMessage:\n"
|
||||
+ " connectToBroker:\n"
|
||||
+ " additionalOptions: bar";
|
||||
// @formatter:on
|
||||
|
||||
KafkaMetadata metadata = KafkaMetadata.fromMetadata(
|
||||
this.mapper.readerForMapOf(Object.class).readValue(yamlEntry));
|
||||
|
||||
String serialized = this.mapper.writer().forType(KafkaMetadata.class)
|
||||
.writeValueAsString(metadata);
|
||||
BDDAssertions.then(serialized).isEqualToNormalizingPunctuationAndWhitespace(
|
||||
yamlEntry.replace("kafka:\n", ""));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -161,4 +161,14 @@ public class ContractVerifierUtilTest {
|
||||
assertThat(node).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_set_path_without_prefix_and_with_suffix() {
|
||||
assertThat(ContractVerifierUtil.fromRelativePath("validate_foo()"))
|
||||
.isEqualTo("foo.yml");
|
||||
assertThat(ContractVerifierUtil.fromRelativePath("validate_foo"))
|
||||
.isEqualTo("foo.yml");
|
||||
assertThat(ContractVerifierUtil.fromRelativePath("foo")).isEqualTo("foo.yml");
|
||||
assertThat(ContractVerifierUtil.fromRelativePath("foo.yml")).isEqualTo("foo.yml");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
|
||||
// tag::class[]
|
||||
class contract_rest_with_tags implements Supplier<Collection<Contract>> {
|
||||
@@ -64,10 +63,9 @@ class contract_rest_with_tags implements Supplier<Collection<Contract>> {
|
||||
static Object metadata = Collections.singletonList(
|
||||
// tag::metadata[]
|
||||
Contract.make(c -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("wiremock",
|
||||
"{ \"response\" : { \"fixedDelayMilliseconds\" : 2000 } }");
|
||||
c.metadata(map);
|
||||
c.metadata(MetadataUtil.map().entry("wiremock",
|
||||
ContractVerifierUtil.map().entry("stubMapping",
|
||||
"{ \"response\" : { \"fixedDelayMilliseconds\" : 2000 } }")));
|
||||
}));
|
||||
|
||||
// end::metadata[]
|
||||
|
||||
Reference in New Issue
Block a user