Messaging polyglot support (#1472)

added support for AMQP, KAFKA and standalone options
This commit is contained in:
Marcin Grzejszczak
2020-08-21 18:05:32 +02:00
committed by GitHub
parent c5d3456d3a
commit a915cf102b
74 changed files with 3399 additions and 791 deletions

View 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

View File

@@ -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

View 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.

View File

@@ -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()
}
}

View File

@@ -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()
}

View File

@@ -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;
}
}

View File

@@ -1,2 +1,4 @@
org.gradle.daemon=false
verifierVersion=3.0.0-SNAPSHOT
springBootVersion=2.4.0-SNAPSHOT
camelVersion=3.4.3

View File

@@ -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 {
}
}

View File

@@ -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());
}
}

View File

@@ -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 {
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
contracts.MessagingAutoConfig

View File

@@ -0,0 +1 @@
stubrunner.camel.enabled: false