Brings back Eureka support, updates samples, removes mocked kafka & rabbit samples

This commit is contained in:
Marcin Grzejszczak
2022-11-15 12:20:56 +01:00
parent eeda962d5d
commit fa51d6a076
58 changed files with 611 additions and 2434 deletions

View File

@@ -19,8 +19,7 @@
<main.basedir>${basedir}/..</main.basedir>
<maven.plugin.plugin.version>3.4</maven.plugin.plugin.version>
<configprops.inclusionPattern>stubrunner.*|wiremock.*|</configprops.inclusionPattern>
<!-- TODO: Remove me -->
<!--<upload-docs-zip.phase>deploy</upload-docs-zip.phase>-->
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
<!-- Aligned with Groovy in SC-Build -->
<groovy.version>4.0.0</groovy.version>
</properties>
@@ -149,11 +148,10 @@
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
<!-- TODO: Remove me -->
<!--<plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>-->
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
</plugin>

62
pom.xml
View File

@@ -50,6 +50,8 @@
<gmavenplus-plugin.version>1.13.0</gmavenplus-plugin.version>
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
<awaitility.version>4.2.0</awaitility.version>
<artemis.version>${artemis-jms-server.version}</artemis.version>
<testcontainers.version>1.17.5</testcontainers.version>
<!-- We need to have compatibility with Gradle -->
<groovy.version>4.0.0</groovy.version>
@@ -70,6 +72,7 @@
<javadoc.failOnError>false</javadoc.failOnError>
<javadoc.failOnWarnings>false</javadoc.failOnWarnings>
<artemis-jms-server.version>2.26.0</artemis-jms-server.version>
</properties>
<modules>
@@ -119,6 +122,26 @@
<artifactId>camel-spring-boot</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-rabbitmq-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-direct-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-bean-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-jackson-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
@@ -144,6 +167,11 @@
<artifactId>camel-activemq</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-server</artifactId>
<version>${artemis-jms-server.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>
@@ -477,7 +505,21 @@
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-junit</artifactId>
<version>${artemis.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -620,6 +662,24 @@
<enabled>true</enabled>
</snapshots>
</repository>
<!-- FIXME: 4.0 -->
<!-- Netflix -->
<!--<repository>
<id>netflix-snapshots</id>
<name>Netflix Snapshots</name>
<url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>-->
<repository>
<id>netflix-candidates</id>
<name>Netflix Candidates</name>
<url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-candidates</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<profiles>

View File

@@ -75,11 +75,6 @@
<artifactId>jopt-simple</artifactId>
<optional>true</optional>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<optional>true</optional>
</dependency>-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>

View File

@@ -16,7 +16,7 @@
package org.springframework.cloud.contract.stubrunner;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
/**
@@ -32,13 +32,13 @@ public class BatchStubRunnerFactory {
private final StubDownloader stubDownloader;
private final MessageVerifier<?> contractVerifierMessaging;
private final MessageVerifierSender<?> contractVerifierMessaging;
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions) {
this(stubRunnerOptions, new NoOpStubMessages());
}
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, MessageVerifier verifier) {
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, MessageVerifierSender<?> verifier) {
this(stubRunnerOptions, aetherStubDownloader(stubRunnerOptions), verifier);
}
@@ -47,7 +47,7 @@ public class BatchStubRunnerFactory {
}
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubRunnerOptions = stubRunnerOptions;
this.stubDownloader = stubDownloader;
this.contractVerifierMessaging = contractVerifierMessaging;

View File

@@ -30,7 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.StringUtils;
@@ -60,7 +60,7 @@ public class StubRunner implements StubRunning {
}
public StubRunner(StubRunnerOptions stubRunnerOptions, String repositoryPath, StubConfiguration stubsConfiguration,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubsConfiguration = stubsConfiguration;
this.stubRunnerOptions = stubRunnerOptions;
List<HttpServerStub> serverStubs = SpringFactoriesLoader.loadFactories(HttpServerStub.class, null);

View File

@@ -41,7 +41,7 @@ import org.springframework.cloud.contract.stubrunner.AvailablePortScanner.PortCa
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.MessageVerifierSender;
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;
@@ -57,7 +57,7 @@ class StubRunnerExecutor implements StubFinder {
private final AvailablePortScanner portScanner;
private final MessageVerifier<?> contractVerifierMessaging;
private final MessageVerifierSender<?> contractVerifierMessaging;
private final List<HttpServerStub> serverStubs;
@@ -65,7 +65,7 @@ class StubRunnerExecutor implements StubFinder {
private final YamlContractConverter yamlContractConverter = new YamlContractConverter();
StubRunnerExecutor(AvailablePortScanner portScanner, MessageVerifier<?> contractVerifierMessaging,
StubRunnerExecutor(AvailablePortScanner portScanner, MessageVerifierSender<?> contractVerifierMessaging,
List<HttpServerStub> serverStubs) {
this.portScanner = portScanner;
this.contractVerifierMessaging = contractVerifierMessaging;

View File

@@ -36,7 +36,7 @@ import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockH
import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -53,10 +53,10 @@ class StubRunnerFactory {
private final StubDownloader stubDownloader;
private final MessageVerifier<?> contractVerifierMessaging;
private final MessageVerifierSender<?> contractVerifierMessaging;
StubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubRunnerOptions = stubRunnerOptions;
this.stubDownloader = stubDownloader;
this.contractVerifierMessaging = contractVerifierMessaging;

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -26,7 +25,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -34,8 +32,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.verifier.messaging.kafka.ContractVerifierKafkaConfiguration;
import org.springframework.cloud.contract.verifier.messaging.kafka.KafkaStubMessagesInitializer;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -59,21 +55,10 @@ import org.springframework.util.StringUtils;
@ConditionalOnClass({ KafkaTemplate.class, EmbeddedKafkaBroker.class })
@ConditionalOnProperty(name = "stubrunner.kafka.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnBean(EmbeddedKafkaBroker.class)
@AutoConfigureBefore(ContractVerifierKafkaConfiguration.class)
public class StubRunnerKafkaConfiguration {
private static final Log log = LogFactory.getLog(StubRunnerKafkaConfiguration.class);
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "stubrunner.kafka.initializer.enabled", havingValue = "true", matchIfMissing = true)
KafkaStubMessagesInitializer stubRunnerKafkaStubMessagesInitializer() {
if (log.isDebugEnabled()) {
log.debug("Registering a noop kafka messages initializer");
}
return (broker, kafkaProperties) -> new HashMap<>();
}
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,

View File

@@ -21,7 +21,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -36,6 +35,8 @@ 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.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -156,9 +157,12 @@ public class StubRunnerConfiguration {
}
@SuppressWarnings("unchecked")
class LazyMessageVerifier implements MessageVerifier {
private MessageVerifier<?> messageVerifier;
private MessageVerifierSender<?> messageVerifierSender;
private MessageVerifierReceiver<?> messageVerifierReceiver;
private final BeanFactory beanFactory;
@@ -166,36 +170,40 @@ class LazyMessageVerifier implements MessageVerifier {
this.beanFactory = beanFactory;
}
private MessageVerifier messageVerifier() {
if (this.messageVerifier == null) {
try {
this.messageVerifier = this.beanFactory.getBean(MessageVerifier.class);
}
catch (BeansException ex) {
this.messageVerifier = new NoOpStubMessages();
}
private MessageVerifierSender messageVerifierSender() {
if (this.messageVerifierSender == null) {
this.messageVerifierSender = this.beanFactory.getBeanProvider(MessageVerifierSender.class)
.getIfAvailable(NoOpStubMessages::new);
}
return this.messageVerifier;
return this.messageVerifierSender;
}
private MessageVerifierReceiver messageVerifierReceiver() {
if (this.messageVerifierReceiver == null) {
this.messageVerifierReceiver = this.beanFactory.getBeanProvider(MessageVerifierReceiver.class)
.getIfAvailable(NoOpStubMessages::new);
}
return this.messageVerifierReceiver;
}
@Override
public void send(Object message, String destination, YamlContract contract) {
messageVerifier().send(message, destination, contract);
messageVerifierSender().send(message, destination, contract);
}
@Override
public Object receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
return messageVerifier().receive(destination, timeout, timeUnit, contract);
return messageVerifierReceiver().receive(destination, timeout, timeUnit, contract);
}
@Override
public Object receive(String destination, YamlContract contract) {
return messageVerifier().receive(destination, contract);
return messageVerifierReceiver().receive(destination, contract);
}
@Override
public void send(Object payload, Map headers, String destination, YamlContract contract) {
messageVerifier().send(payload, headers, destination, contract);
messageVerifierSender().send(payload, headers, destination, contract);
}
}

View File

@@ -30,14 +30,17 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubRunning;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubsRegistrar;
import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler;
import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean;
import org.springframework.cloud.netflix.eureka.InstanceInfoFactory;
import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration;
@@ -92,11 +95,16 @@ public class EurekaStubsRegistrar implements StubsRegistrar {
+ ", " + instance.getNonSecurePort() + ", " + instance.getInstanceId() + "]");
InstanceInfo instanceInfo = new InstanceInfoFactory().create(instance);
ApplicationInfoManager applicationInfoManager = new ApplicationInfoManager(instance, instanceInfo);
AbstractDiscoveryClientOptionalArgs args = args();
AbstractDiscoveryClientOptionalArgs<?> args = args();
EurekaClient client = new CloudEurekaClient(applicationInfoManager, this.eurekaClientConfigBean, args,
this.context);
EurekaRegistration registration = EurekaRegistration.builder(instance)
.with(this.eurekaClientConfigBean, this.context).with(client).build();
EurekaHealthCheckHandler eurekaHealthCheckHandler = new EurekaHealthCheckHandler(
StatusAggregator.getDefault());
eurekaHealthCheckHandler.setApplicationContext(context);
eurekaHealthCheckHandler.afterPropertiesSet();
registration.setHealthCheckHandler(new SimpleObjectProvider<>(eurekaHealthCheckHandler));
this.registrations.add(registration);
try {
this.serviceRegistry.register(registration);
@@ -110,7 +118,7 @@ public class EurekaStubsRegistrar implements StubsRegistrar {
}
}
private AbstractDiscoveryClientOptionalArgs args() {
private AbstractDiscoveryClientOptionalArgs<?> args() {
try {
return this.context.getBean(AbstractDiscoveryClientOptionalArgs.class);
}

View File

@@ -4,6 +4,7 @@ org.springframework.cloud.contract.stubrunner.spring.cloud.loadbalancer.SpringCl
org.springframework.cloud.contract.stubrunner.messaging.integration.StubRunnerIntegrationConfiguration
org.springframework.cloud.contract.stubrunner.messaging.jms.StubRunnerJmsConfiguration
org.springframework.cloud.contract.stubrunner.messaging.stream.StubRunnerStreamConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.eureka.StubRunnerSpringCloudEurekaAutoConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.zookeeper.StubRunnerSpringCloudZookeeperAutoConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.consul.StubRunnerSpringCloudConsulAutoConfiguration
org.springframework.cloud.contract.stubrunner.messaging.StubRunnerStreamsIntegrationAutoConfiguration

View File

@@ -20,7 +20,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.contract.stubrunner.server.EnableStubRunnerServer;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author Marcin Grzejszczak
@@ -28,7 +27,6 @@ import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
// tag::stubrunnereureka[]
@SpringBootApplication
@EnableStubRunnerServer
@EnableEurekaClient
@AutoConfigureStubRunner
public class StubRunnerBootEurekaExample {

View File

@@ -16,7 +16,10 @@
package org.springframework.cloud.contract.stubrunner.spring.cloud
import spock.lang.Specification
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
@@ -42,55 +45,59 @@ import org.springframework.web.client.RestTemplate
@ActiveProfiles("cloudtest")
// tag::autoconfigure[]
@AutoConfigureStubRunner(
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
// end::autoconfigure[]
class StubRunnerSpringCloudAutoConfigurationSpec extends Specification {
class StubRunnerSpringCloudAutoConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
@LoadBalanced
RestTemplate restTemplate
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
StubFinder stubFinder
@Autowired
@LoadBalanced
RestTemplate restTemplate
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
void setupSpec() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
@BeforeAll
static void setupSpec() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
void cleanupSpec() {
setupSpec()
}
@AfterAll
static void cleanupSpec() {
setupSpec()
}
def setup() {
assert loadBalancerClientFactory instanceof StubRunnerLoadBalancerClientFactory
}
@BeforeEach
void setup() {
assert loadBalancerClientFactory.getClass().getSimpleName() == "StubRunnerLoadBalancerClientFactory"
}
// tag::test[]
def 'should make service discovery work'() {
expect: 'WireMocks are running'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
// end::test[]
// tag::test[]
@Test
void 'should make service discovery work'() {
expect: 'WireMocks are running'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
assert restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
assert restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
// end::test[]
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
}

View File

@@ -18,7 +18,8 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud
import org.junit.AfterClass
import org.junit.BeforeClass
import spock.lang.Specification
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
@@ -45,53 +46,55 @@ import org.springframework.web.client.RestTemplate
@SpringBootTest(classes = Config)
@ActiveProfiles("cloudtest")
@AutoConfigureStubRunner(
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
class StubRunnerSpringCloudReactiveAutoConfigurationSpec extends Specification {
@Autowired
StubFinder stubFinder
@Autowired
ReactiveDiscoveryClient reactiveDiscoveryClient;
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
RestTemplate restTemplate = new RestTemplate()
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
class StubRunnerSpringCloudReactiveAutoConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
ReactiveDiscoveryClient reactiveDiscoveryClient;
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
RestTemplate restTemplate = new RestTemplate()
@BeforeClass
@AfterClass
static void setupProps() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
@BeforeClass
@AfterClass
static void setupProps() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
def setup() {
assert loadBalancerClientFactory instanceof StubRunnerLoadBalancerClientFactory
}
@BeforeEach
void setup() {
assert loadBalancerClientFactory.getClass().getSimpleName() == "StubRunnerLoadBalancerClientFactory"
}
// tag::test[]
def 'should make service discovery work'() {
expect: 'WireMocks are running'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
ServiceInstance loanIssuance = reactiveDiscoveryClient.getInstances('loanIssuance').blockFirst()
restTemplate.getForObject(loanIssuance.uri.toString() + '/name', String) == 'loanIssuance'
ServiceInstance fraudDetection = reactiveDiscoveryClient.getInstances('someNameThatShouldMapFraudDetectionServer').blockFirst()
restTemplate.getForObject(fraudDetection.uri.toString() + '/name', String) == 'fraudDetectionServer'
}
// end::test[]
// tag::test[]
@Test
void 'should make service discovery work'() {
expect: 'WireMocks are running'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
ServiceInstance loanIssuance = reactiveDiscoveryClient.getInstances('loanIssuance').blockFirst()
assert restTemplate.getForObject(loanIssuance.uri.toString() + '/name', String) == 'loanIssuance'
ServiceInstance fraudDetection = reactiveDiscoveryClient.getInstances('someNameThatShouldMapFraudDetectionServer').blockFirst()
assert restTemplate.getForObject(fraudDetection.uri.toString() + '/name', String) == 'fraudDetectionServer'
}
// end::test[]
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import java.util.List;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.support.SimpleAmqpHeaderMapper;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.support.converter.MessagingMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static java.util.Collections.emptyList;
/**
* Configuration setting up {@link MessageVerifier} for use with plain
* spring-rabbit/spring-amqp.
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RabbitTemplate.class)
@AutoConfigureBefore(ContractVerifierIntegrationConfiguration.class)
@AutoConfigureAfter(ContractVerifierStreamAutoConfiguration.class)
public class ContractVerifierAmqpAutoConfiguration {
@Bean
@ConditionalOnBean({ RabbitTemplate.class, MessageVerifier.class })
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierMessaging(MessageVerifier<Message> exchange,
RabbitTemplate rabbitTemplate) {
return new ContractVerifierHelper(exchange, rabbitTemplate.getMessageConverter());
}
@Configuration
@ConditionalOnProperty(name = "stubrunner.amqp.enabled", havingValue = "true")
static class ContractVerifierAmqpSpyAutoConfiguration {
@SpyBean
private RabbitTemplate rabbitTemplate;
@Autowired(required = false)
private RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
@Autowired(required = false)
private List<SimpleMessageListenerContainer> simpleMessageListenerContainers = emptyList();
@Autowired(required = false)
private List<Binding> bindings = emptyList();
@Autowired
private RabbitProperties rabbitProperties;
@Bean
@ConditionalOnMissingBean
public MessageVerifier<Message> contractVerifierMessageExchange() {
return new SpringAmqpStubMessages(this.rabbitTemplate,
new MessageListenerAccessor(this.rabbitListenerEndpointRegistry,
this.simpleMessageListenerContainers, this.bindings),
this.rabbitProperties);
}
}
}
class ContractVerifierHelper extends ContractVerifierMessaging<Message> {
private final MessageConverter messageConverter;
ContractVerifierHelper(MessageVerifier<Message> exchange, MessageConverter messageConverter) {
super(exchange);
this.messageConverter = messageConverter;
}
@Override
protected ContractVerifierMessage convert(Message message) {
MessagingMessageConverter messageConverter = new MessagingMessageConverter(this.messageConverter,
new SimpleAmqpHeaderMapper());
org.springframework.messaging.Message<?> messagingMessage;
messagingMessage = (org.springframework.messaging.Message<?>) messageConverter.fromMessage(message);
return new ContractVerifierMessage(messagingMessage.getPayload(), messagingMessage.getHeaders());
}
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Binding.DestinationType;
import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
/**
* Abstraction hiding details of the different sources of message listeners.
*
* Needed because
* {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor}
* adds the listeners to the {@link RabbitListenerEndpointRegistry} so that the registry
* is empty when wired into an auto configuration class so we wrap it in the accessor to
* access the listeners late at runtime.
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
class MessageListenerAccessor {
private final RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
private final List<SimpleMessageListenerContainer> simpleMessageListenerContainers;
private final List<Binding> bindings;
MessageListenerAccessor(RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry,
List<SimpleMessageListenerContainer> simpleMessageListenerContainers, List<Binding> bindings) {
this.rabbitListenerEndpointRegistry = rabbitListenerEndpointRegistry;
this.simpleMessageListenerContainers = simpleMessageListenerContainers;
this.bindings = bindings;
}
List<SimpleMessageListenerContainer> getListenerContainersForDestination(String destination, String routingKey) {
List<SimpleMessageListenerContainer> listenerContainers = collectListenerContainers();
// we interpret the destination as exchange name and collect all the queues bound
// to this exchange
Set<String> queueNames = collectQueuesBoundToDestination(destination, routingKey);
return getListenersByBoundQueues(listenerContainers, queueNames);
}
private List<SimpleMessageListenerContainer> getListenersByBoundQueues(
List<SimpleMessageListenerContainer> listenerContainers, Set<String> queueNames) {
List<SimpleMessageListenerContainer> matchingContainers = new ArrayList<>();
for (SimpleMessageListenerContainer listenerContainer : listenerContainers) {
if (listenerContainer.getQueueNames() != null) {
for (String queueName : listenerContainer.getQueueNames()) {
if (queueNames.contains(queueName)) {
matchingContainers.add(listenerContainer);
break;
}
}
}
}
return matchingContainers;
}
private Set<String> collectQueuesBoundToDestination(String destination, String routingKey) {
Set<String> queueNames = new HashSet<>();
for (Binding binding : this.bindings) {
if (destination.equals(binding.getExchange())
&& (routingKey == null || routingKey.equals(binding.getRoutingKey()))
&& DestinationType.QUEUE.equals(binding.getDestinationType())) {
queueNames.add(binding.getDestination());
}
}
return queueNames;
}
private List<SimpleMessageListenerContainer> collectListenerContainers() {
List<SimpleMessageListenerContainer> listenerContainers = new ArrayList<>();
if (this.simpleMessageListenerContainers != null) {
listenerContainers.addAll(this.simpleMessageListenerContainers);
}
if (this.rabbitListenerEndpointRegistry != null) {
for (MessageListenerContainer listenerContainer : this.rabbitListenerEndpointRegistry
.getListenerContainers()) {
if (listenerContainer instanceof SimpleMessageListenerContainer) {
listenerContainers.add((SimpleMessageListenerContainer) listenerContainer);
}
}
}
return listenerContainers;
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Spring rabbit test utility that provides a mock ConnectionFactory to avoid having to
* connect against a running broker.
*
* Set verifier.amqp.mockConnection=true to enable the mocked ConnectionFactory
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ContractVerifierAmqpAutoConfiguration.class)
@ConditionalOnProperty(value = "stubrunner.amqp.mockConnection", havingValue = "true", matchIfMissing = true)
public class RabbitMockConnectionFactoryAutoConfiguration {
@Bean
public ConnectionFactory connectionFactory() {
final Connection mockConnection = mock(Connection.class);
final AMQP.Queue.DeclareOk mockDeclareOk = mock(AMQP.Queue.DeclareOk.class);
com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class,
new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
// hack for keeping backward compatibility with #303
if ("newConnection".equals(invocationOnMock.getMethod().getName())) {
return mockConnection;
}
return Mockito.RETURNS_DEFAULTS.answer(invocationOnMock);
}
});
try {
final Channel mockChannel = mock(Channel.class, invocationOnMock -> {
if ("queueDeclare".equals(invocationOnMock.getMethod().getName())) {
return mockDeclareOk;
}
return Mockito.RETURNS_DEFAULTS.answer(invocationOnMock);
});
when(mockConnection.isOpen()).thenReturn(true);
when(mockConnection.createChannel()).thenReturn(mockChannel);
when(mockConnection.createChannel(Mockito.anyInt())).thenReturn(mockChannel);
}
catch (Exception e) {
throw new RuntimeException(e);
}
return new AbstractConnectionFactory(mockConnectionFactory) {
@Override
public @NonNull org.springframework.amqp.rabbit.connection.Connection createConnection() {
return super.createBareConnection();
}
};
}
}

View File

@@ -1,212 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.rabbitmq.client.Channel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mockito.ArgumentCaptor;
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;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
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.messaging.MessageHeaders;
import org.springframework.util.Assert;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.verify;
import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME;
/**
* {@link MessageVerifier} implementation to integrate with plain
* spring-amqp/spring-rabbit. It is meant to be used without interacting with a running
* bus.
*
* It relies on the RabbitTemplate to be a spy to be able to capture send messages.
*
* Messages are not sent to the bus - but are handed over to a
* {@link SimpleMessageListenerContainer} which allows us to test the full deserialization
* and listener invocation.
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
public class SpringAmqpStubMessages implements MessageVerifier<Message> {
private static final Log log = LogFactory.getLog(SpringAmqpStubMessages.class);
private final RabbitTemplate rabbitTemplate;
private final MessageListenerAccessor messageListenerAccessor;
private RabbitProperties rabbitProperties;
@Autowired
public SpringAmqpStubMessages(RabbitTemplate rabbitTemplate, MessageListenerAccessor messageListenerAccessor,
RabbitProperties rabbitProperties) {
Assert.notNull(rabbitTemplate, "RabbitTemplate must be set");
Assert.isTrue(mockingDetails(rabbitTemplate).isSpy() || mockingDetails(rabbitTemplate).isMock(),
"StubRunner AMQP will work only if RabbiTemplate is a spy");
this.rabbitTemplate = rabbitTemplate;
this.messageListenerAccessor = messageListenerAccessor;
this.rabbitProperties = rabbitProperties;
}
@Override
public <T> void send(T payload, Map<String, Object> messageHeaders, String destination, YamlContract contract) {
final MessageHeaders headers = new MessageHeaders(messageHeaders);
Message message = org.springframework.amqp.core.MessageBuilder.withBody(((String) payload).getBytes())
.andProperties(MessagePropertiesBuilder.newInstance()
.setContentType(header(headers, MessageHeaders.CONTENT_TYPE)).copyHeaders(headers).build())
.build();
if (headers.containsKey(DEFAULT_CLASSID_FIELD_NAME)) {
message.getMessageProperties().setHeader(DEFAULT_CLASSID_FIELD_NAME,
headers.get(DEFAULT_CLASSID_FIELD_NAME));
}
if (headers.containsKey(AmqpHeaders.RECEIVED_ROUTING_KEY)) {
message.getMessageProperties().setReceivedRoutingKey(header(headers, AmqpHeaders.RECEIVED_ROUTING_KEY));
}
send(message, destination, contract);
}
private String header(MessageHeaders headers, String headerName) {
Object value = headers.get(headerName);
if (value == null) {
return "";
}
else if (value instanceof String) {
return (String) value;
}
else if (value instanceof Iterable) {
Iterable values = ((Iterable) value);
return values.iterator().hasNext() ? (String) values.iterator().next() : "";
}
return value.toString();
}
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;
}
@Override
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);
if (listenerContainers.isEmpty()) {
throw new IllegalStateException("no listeners found for destination " + destination);
}
for (SimpleMessageListenerContainer listenerContainer : listenerContainers) {
Object messageListener = listenerContainer.getMessageListener();
if (isChannelAwareListener(listenerContainer, messageListener)) {
try {
((ChannelAwareMessageListener) messageListener).onMessage(message,
createChannel(listenerContainer, transactionalChannel()));
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
else {
((MessageListener) messageListener).onMessage(message);
}
}
}
Channel createChannel(SimpleMessageListenerContainer listenerContainer, boolean transactional) {
return listenerContainer.getConnectionFactory().createConnection().createChannel(transactional);
}
boolean isChannelAwareListener(SimpleMessageListenerContainer listenerContainer, Object messageListener) {
return messageListener instanceof ChannelAwareMessageListener
&& listenerContainer.getConnectionFactory() != null;
}
private boolean transactionalChannel() {
if (this.rabbitProperties == null) {
// backward compatibility
return true;
}
return this.rabbitProperties.getPublisherConfirmType() == null
|| this.rabbitProperties.getPublisherConfirmType() == CachingConnectionFactory.ConfirmType.NONE;
}
@Override
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), routingKeyCaptor.capture(),
messageCaptor.capture(), ArgumentMatchers.any());
if (messageCaptor.getAllValues().isEmpty()) {
log.info("no messages found on destination [" + destination + "]");
return null;
}
else if (messageCaptor.getAllValues().size() > 1) {
log.info("multiple messages found on destination [" + destination + "] returning last one");
return messageCaptor.getValue();
}
Message message = messageCaptor.getValue();
if (message == null) {
log.info("no messages found on destination [" + destination + "]");
return null;
}
if (!routingKeyCaptor.getValue().isEmpty()) {
log.info("routing key passed [" + routingKeyCaptor.getValue() + "]");
message.getMessageProperties().setReceivedRoutingKey(routingKeyCaptor.getValue());
}
return message;
}
@Override
public Message receive(String destination, YamlContract contract) {
return receive(destination, 5, TimeUnit.SECONDS, contract);
}
}

View File

@@ -27,6 +27,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.jms.ContractVerifierJmsConfiguration;
@@ -54,16 +56,16 @@ public class ContractVerifierCamelConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierMessaging(MessageVerifier<Message> exchange) {
return new ContractVerifierCamelHelper(exchange);
public ContractVerifierMessaging<Message> contractVerifierMessaging(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
return new ContractVerifierCamelHelper(sender, receiver);
}
}
class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
ContractVerifierCamelHelper(MessageVerifier<Message> exchange) {
super(exchange);
ContractVerifierCamelHelper(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
super(sender, receiver);
}
@Override

View File

@@ -21,6 +21,8 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
@@ -47,16 +49,16 @@ public class ContractVerifierIntegrationConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message<?>> contractVerifierMessaging(MessageVerifier<Message<?>> exchange) {
return new ContractVerifierHelper(exchange);
public ContractVerifierMessaging<Message<?>> contractVerifierMessaging(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
return new ContractVerifierHelper(sender, receiver);
}
}
class ContractVerifierHelper extends ContractVerifierMessaging<Message<?>> {
ContractVerifierHelper(MessageVerifier<Message<?>> exchange) {
super(exchange);
ContractVerifierHelper(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
super(sender, receiver);
}
@Override

View File

@@ -25,6 +25,8 @@ import org.apache.commons.logging.LogFactory;
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;
/**
* Wrapper around messaging. Abstracts all message related operations like sending,
@@ -38,12 +40,18 @@ public class ContractVerifierMessaging<M> {
private static final Log log = LogFactory.getLog(ContractVerifierMessaging.class);
private final MessageVerifier<M> exchange;
private final MessageVerifierSender<M> sender;
public ContractVerifierMessaging(MessageVerifier<M> exchange) {
this.exchange = exchange;
if (exchange != null) {
log.info("The message verifier implementation is of type [" + exchange.getClass() + "]");
private final MessageVerifierReceiver<M> receiver;
public ContractVerifierMessaging(MessageVerifierSender<M> sender, MessageVerifierReceiver<M> receiver) {
this.sender = sender;
this.receiver = receiver;
if (sender != null) {
log.info("The message verifier sender implementation is of type [" + sender.getClass() + "]");
}
if (receiver != null) {
log.info("The message verifier receiver implementation is of type [" + receiver.getClass() + "]");
}
}
@@ -51,7 +59,7 @@ public class ContractVerifierMessaging<M> {
if (contract != null) {
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.INPUT);
}
this.exchange.send(message.getPayload(), message.getHeaders(), destination, contract);
this.sender.send(message.getPayload(), message.getHeaders(), destination, contract);
}
public void send(ContractVerifierMessage message, String destination) {
@@ -62,7 +70,7 @@ public class ContractVerifierMessaging<M> {
if (contract != null) {
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.OUTPUT);
}
return convert(this.exchange.receive(destination, contract));
return convert(this.receiver.receive(destination, contract));
}
private void setMessageType(YamlContract contract, ContractVerifierMessageMetadata.MessageType output) {

View File

@@ -34,6 +34,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
@@ -60,8 +62,8 @@ public class ContractVerifierJmsConfiguration {
@Bean
@ConditionalOnMissingBean
ContractVerifierMessaging<Message> contractVerifierJmsMessaging(MessageVerifier<Message> exchange) {
return new ContractVerifierJmsHelper(exchange);
ContractVerifierMessaging<Message> contractVerifierJmsMessaging(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
return new ContractVerifierJmsHelper(sender, receiver);
}
}
@@ -70,8 +72,8 @@ class ContractVerifierJmsHelper extends ContractVerifierMessaging<Message> {
private static final Log log = LogFactory.getLog(ContractVerifierJmsHelper.class);
ContractVerifierJmsHelper(MessageVerifier<Message> exchange) {
super(exchange);
ContractVerifierJmsHelper(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
super(sender, receiver);
}
@Override

View File

@@ -1,116 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.kafka;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
/**
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ KafkaTemplate.class, EmbeddedKafkaBroker.class })
@ConditionalOnProperty(name = "stubrunner.kafka.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore({ ContractVerifierIntegrationConfiguration.class, NoOpContractVerifierAutoConfiguration.class })
@ConditionalOnBean(EmbeddedKafkaBroker.class)
public class ContractVerifierKafkaConfiguration {
private static final Log log = LogFactory.getLog(ContractVerifierKafkaConfiguration.class);
@Bean
@ConditionalOnMissingBean
MessageVerifier<Message<?>> contractVerifierKafkaMessageExchange(Supplier<KafkaTemplate> kafkaTemplate,
EmbeddedKafkaBroker broker, KafkaProperties kafkaProperties, KafkaStubMessagesInitializer initializer) {
return new KafkaStubMessages(kafkaTemplate.get(), broker, kafkaProperties, initializer);
}
@Bean
@ConditionalOnMissingBean
Supplier<KafkaTemplate> contractVerifierKafkaTemplateSupplier(KafkaTemplate kafkaTemplate) {
return () -> kafkaTemplate;
}
@Bean
@ConditionalOnMissingBean
KafkaStubMessagesInitializer contractVerifierKafkaStubMessagesInitializer() {
if (log.isDebugEnabled()) {
log.debug("Registering contract verifier stub messages initializer");
}
return new ContractVerifierKafkaStubMessagesInitializer();
}
@Bean
@ConditionalOnMissingBean
ContractVerifierMessaging<Message<?>> contractVerifierKafkaMessaging(MessageVerifier<Message<?>> exchange) {
return new ContractVerifierKafkaHelper(exchange);
}
}
class ContractVerifierKafkaHelper extends ContractVerifierMessaging<Message<?>> {
ContractVerifierKafkaHelper(MessageVerifier<Message<?>> exchange) {
super(exchange);
}
@Override
protected ContractVerifierMessage convert(Message<?> message) {
return new ContractVerifierMessage(message.getPayload(), convertHeaders(message.getHeaders()));
}
private MessageHeaders convertHeaders(Map<String, Object> headers) {
final Map<String, Object> headersMap = new HashMap<>();
if (headers != null) {
headers.forEach((k, v) -> headersMap.put(k, maybeConvertValue(v)));
}
return new MessageHeaders(headersMap);
}
private Object maybeConvertValue(Object value) {
if (value == null) {
return value;
}
if (!(value instanceof byte[])) {
return value;
}
return new String((byte[]) value, StandardCharsets.UTF_8);
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.kafka;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.utils.KafkaTestUtils;
class ContractVerifierKafkaStubMessagesInitializer implements KafkaStubMessagesInitializer {
private static final Log log = LogFactory.getLog(ContractVerifierKafkaStubMessagesInitializer.class);
@Override
public Map<String, Consumer> initialize(EmbeddedKafkaBroker broker, KafkaProperties kafkaProperties) {
Map<String, Consumer> map = new HashMap<>();
for (String topic : broker.getTopics()) {
map.put(topic, prepareListener(broker, topic, kafkaProperties));
}
return map;
}
private Consumer prepareListener(EmbeddedKafkaBroker broker, String destination, KafkaProperties kafkaProperties) {
Map<String, Object> consumerProperties = KafkaTestUtils
.consumerProps(kafkaProperties.getConsumer().getGroupId(), "false", broker);
// Respect custom key/value deserializers and any additional props under
// 'spring.kafka.consumer.properties'
consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
kafkaProperties.getConsumer().getKeyDeserializer());
consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
kafkaProperties.getConsumer().getValueDeserializer());
consumerProperties.putAll(kafkaProperties.getConsumer().getProperties());
DefaultKafkaConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<>(
consumerProperties);
Consumer<String, String> consumer = consumerFactory.createConsumer();
broker.consumeFromAnEmbeddedTopic(consumer, destination);
if (log.isDebugEnabled()) {
log.debug("Prepared consumer for destination [" + destination + "]");
}
return consumer;
}
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.kafka;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import net.minidev.json.JSONObject;
import net.minidev.json.parser.JSONParser;
import net.minidev.json.parser.ParseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
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.support.KafkaHeaders;
import org.springframework.kafka.support.converter.MessagingMessageConverter;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
class KafkaStubMessages implements MessageVerifier<Message<?>> {
private static final Log log = LogFactory.getLog(KafkaStubMessages.class);
final KafkaTemplate kafkaTemplate;
private final Receiver receiver;
KafkaStubMessages(KafkaTemplate kafkaTemplate, EmbeddedKafkaBroker broker, KafkaProperties kafkaProperties,
KafkaStubMessagesInitializer initializer) {
this.kafkaTemplate = kafkaTemplate;
Map<String, Consumer> topicToConsumer = initializer.initialize(broker, kafkaProperties);
this.receiver = new Receiver(topicToConsumer);
}
@Override
public void send(Message<?> message, String destination, YamlContract contract) {
String defaultTopic = this.kafkaTemplate.getDefaultTopic();
try {
this.kafkaTemplate.setDefaultTopic(destination);
if (log.isDebugEnabled()) {
log.debug("Will send a message [" + message + "] to destination [" + destination + "]");
}
this.kafkaTemplate.send(message).get(5, TimeUnit.SECONDS);
this.kafkaTemplate.flush();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
finally {
this.kafkaTemplate.setDefaultTopic(defaultTopic);
}
}
@Override
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, YamlContract contract) {
return receive(destination, 5, TimeUnit.SECONDS, contract);
}
@Override
public void send(Object payload, Map headers, String destination, YamlContract contract) {
Message<?> message = MessageBuilder.createMessage(payload, new MessageHeaders(headers));
send(message, destination, contract);
}
}
class Receiver {
private static final Log log = LogFactory.getLog(Receiver.class);
private final MessagingMessageConverter messagingMessageConverter = new MessagingMessageConverter();
private final Map<String, Consumer> consumers;
Receiver(Map<String, Consumer> consumers) {
this.consumers = consumers;
}
Message receive(String topic, long timeout, TimeUnit timeUnit, YamlContract contract) {
Consumer consumer = this.consumers.get(topic);
if (consumer == null) {
throw new IllegalStateException("No consumer set up for topic [" + topic + "]");
}
ConsumerRecord<?, ?> record = KafkaTestUtils.getSingleRecord(consumer, topic, Duration.ofMillis(timeout));
if (log.isDebugEnabled()) {
log.debug("Got a single record for destination [" + topic + "]");
}
return toMessage(consumer, record);
}
Message toMessage(Consumer consumer, ConsumerRecord<?, ?> record) {
Map<String, Object> headersMap = toMap(record.headers());
// Leverage spring-kafka to add the headers
messagingMessageConverter.commonHeaders(null, consumer, headersMap, record.key(), record.topic(),
record.partition(), record.offset(),
record.timestampType() != null ? record.timestampType().name() : null, record.timestamp());
// commonHeaders() maps the record key under 'kafka_receivedMessageKey' - put
// under 'kafka_messageKey' as well to satisfy both client/server usages as there
// is not currently a way to set a header name based on client/server
headersMap.put(KafkaHeaders.KEY, record.key());
// TODO explore using MessagingMessageConverter to do all of the conversion
// (ideally delete this entire method)
Object textPayload = record.value();
// sometimes it's a message sometimes just payload
if (textPayload instanceof String && ((String) textPayload).contains("payload")
&& ((String) textPayload).contains("headers")) {
try {
Object object = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse((String) textPayload);
JSONObject jo = (JSONObject) object;
String payload = (String) jo.get("payload");
JSONObject headersInJson = (JSONObject) jo.get("headers");
headersMap.putAll(headersInJson);
return MessageBuilder.createMessage(unquoted(payload), new MessageHeaders(headersMap));
}
catch (ParseException ex) {
throw new IllegalStateException(ex);
}
}
return MessageBuilder.createMessage(unquoted(textPayload), new MessageHeaders(headersMap));
}
private Map<String, Object> toMap(Headers headers) {
Map<String, Object> map = new HashMap<>();
for (Header header : headers) {
map.put(header.key(), header.value());
}
return map;
}
private Object unquoted(Object value) {
String textPayload = value instanceof byte[] ? new String((byte[]) value) : value.toString();
if (textPayload.startsWith("\"") && textPayload.endsWith("\"")) {
return textPayload.substring(1, textPayload.length() - 1).replace("\\\"", "\"");
}
return textPayload;
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.kafka;
import java.util.Map;
import org.apache.kafka.clients.consumer.Consumer;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
/**
* Logic used to initialize {@link KafkaStubMessages}. This interface might have a
* different implementation for the producer side and for the consumer side. That's
* because you can't poll for a single message by different consumers.
*
* @author Marcin Grzejszczak
* @since 2.2.0
*/
public interface KafkaStubMessagesInitializer {
/**
* @param broker - embedded Kafka broker
* @param kafkaProperties - kafka properties
* @return topic to initialized consumer mapping
*/
Map<String, Consumer> initialize(EmbeddedKafkaBroker broker, KafkaProperties kafkaProperties);
}

View File

@@ -22,6 +22,8 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.context.annotation.Bean;
@@ -39,14 +41,14 @@ public class NoOpContractVerifierAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MessageVerifier.class)
public MessageVerifier<?> contractVerifierMessageExchange() {
public NoOpStubMessages contractVerifierMessageExchange() {
return new NoOpStubMessages();
}
@Bean
@ConditionalOnMissingBean(ContractVerifierMessaging.class)
public ContractVerifierMessaging<?> contractVerifierMessaging(MessageVerifier<?> exchange) {
return new ContractVerifierMessaging<>(exchange);
public ContractVerifierMessaging<Object> contractVerifierMessaging(NoOpStubMessages messages) {
return new ContractVerifierMessaging<>(messages, messages);
}
@Bean

View File

@@ -22,6 +22,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
@@ -44,8 +46,8 @@ public class ContractVerifierStreamAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<?> contractVerifierMessagingConverter(MessageVerifier<Message<?>> exchange) {
return new ContractVerifierHelper(exchange);
public ContractVerifierMessaging<?> contractVerifierMessagingConverter(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
return new ContractVerifierHelper(sender, receiver);
}
@Configuration(proxyBeanMethods = false)
@@ -79,8 +81,8 @@ public class ContractVerifierStreamAutoConfiguration {
class ContractVerifierHelper extends ContractVerifierMessaging<Message<?>> {
ContractVerifierHelper(MessageVerifier<Message<?>> exchange) {
super(exchange);
ContractVerifierHelper(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
super(sender, receiver);
}
@Override

View File

@@ -1,8 +1,5 @@
org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration
org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration
org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration
org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration
org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration
org.springframework.cloud.contract.verifier.messaging.jms.ContractVerifierJmsConfiguration
org.springframework.cloud.contract.verifier.messaging.kafka.ContractVerifierKafkaConfiguration
org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp
import spock.lang.Specification
import org.springframework.amqp.core.Message
import org.springframework.amqp.core.MessageBuilder
import org.springframework.amqp.core.MessagePropertiesBuilder
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON
/**
* @author Mathias Düsterhöft
*/
class ContractVerifierHelperSpec extends Specification {
def "should convert message"() {
given:
String payload = '''{"name":"some"}'''
Message message = MessageBuilder
.withBody(payload.bytes)
.andProperties(MessagePropertiesBuilder.newInstance()
.setHeader("my-header", "some")
.setContentType(CONTENT_TYPE_JSON)
.build()).build()
ContractVerifierHelper contractVerifierHelper = new ContractVerifierHelper(null, new Jackson2JsonMessageConverter())
when:
ContractVerifierMessage contractVerifierMessage = contractVerifierHelper.convert(message)
then:
((Map) contractVerifierMessage.payload).containsKey("name")
contractVerifierMessage.headers.containsKey("contentType")
contractVerifierMessage.headers.containsKey("my-header")
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp
import spock.lang.Specification
import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
import org.springframework.amqp.core.DirectExchange
import org.springframework.amqp.core.Queue
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
class MessageListenerAccessorSpec extends Specification {
String queueName = "test.queue"
String exchange = "test-exchange"
SimpleMessageListenerContainer listenerContainer
Binding binding
def "should get single simple listener container"() {
given:
givenSimpleMessageListenerContainer()
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [this.listenerContainer], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination(this.exchange, null)
then:
listenerContainersForDestination.size() == 1
listenerContainersForDestination.get(0) == this.listenerContainer
}
def "should get single simple listener container for matching routing key"() {
given:
givenSimpleMessageListenerContainer()
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [this.listenerContainer], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination(this.exchange, '#')
then:
listenerContainersForDestination.size() == 1
listenerContainersForDestination.get(0) == this.listenerContainer
}
def "should get empty simple listener container for non matching routing key"() {
given:
givenSimpleMessageListenerContainer()
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [this.listenerContainer], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination(this.exchange, 'not matching')
then:
listenerContainersForDestination.isEmpty()
}
def "should get empty listener container list for unknown destination"() {
given:
givenSimpleMessageListenerContainer()
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [this.listenerContainer], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination("some-exchange", null)
then:
listenerContainersForDestination.isEmpty()
}
def "should get empty listener container list for queue with no matching listener"() {
given:
givenSimpleMessageListenerContainer()
this.binding = BindingBuilder.bind(new Queue("some.queue")).to(new DirectExchange(this.exchange)).with("#")
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(null, [this.listenerContainer], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination(this.exchange, null)
then:
listenerContainersForDestination.isEmpty()
}
def "should get single simple listener container from RabbitListenerEndpointRegistry"() {
given:
givenSimpleMessageListenerContainer()
RabbitListenerEndpointRegistry rabbitListenerEndpointRegistryMock = Mock(RabbitListenerEndpointRegistry)
rabbitListenerEndpointRegistryMock.getListenerContainers() >> [this.listenerContainer]
MessageListenerAccessor messageListenerAccessor = new MessageListenerAccessor(rabbitListenerEndpointRegistryMock, [], [this.binding])
when:
List<SimpleMessageListenerContainer> listenerContainersForDestination = messageListenerAccessor.getListenerContainersForDestination(this.exchange, null)
then:
listenerContainersForDestination.size() == 1
listenerContainersForDestination.get(0) == this.listenerContainer
}
def givenSimpleMessageListenerContainer() {
this.listenerContainer = new SimpleMessageListenerContainer()
this.listenerContainer.setQueueNames(this.queueName)
this.binding = BindingBuilder.bind(new Queue(this.queueName)).to(new DirectExchange(this.exchange)).with("#")
}
}

View File

@@ -1,221 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp
import com.rabbitmq.client.Channel
import org.mockito.exceptions.verification.WantedButNotInvoked
import spock.lang.Specification
import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
import org.springframework.amqp.core.DirectExchange
import org.springframework.amqp.core.Message
import org.springframework.amqp.core.MessageProperties
import org.springframework.amqp.core.Queue
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
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
import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME
/**
* @author Mathias Düsterhöft
*/
class SpringAmqpStubMessagesSpec extends Specification {
RabbitTemplate rabbitTemplate = mock(RabbitTemplate.class)
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer()
MessageListenerAdapter messageListenerAdapter = Mock(MessageListenerAdapter.class)
RabbitProperties rabbitProperties = new RabbitProperties()
Message message = Mock(Message.class)
String queueName = "test.queue"
String exchange = "test-exchange"
String payload = '''{"name":"some"}'''
String routingKey = "resource.created"
def "should send amqp message with type 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)
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"
})
}
// for JDK 16 the merge option won't work and the metadata needs to be FULLY applied
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",
contentType: CONTENT_TYPE_JSON,
receivedRoutingKey: routingKey]]
],
"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)
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])
boolean createChannelCalled = false
boolean transactionalChannel = false
SpringAmqpStubMessages messageVerifier = new SpringAmqpStubMessages(rabbitTemplate, messageListenerAccessor, rabbitProperties) {
@Override
boolean isChannelAwareListener(SimpleMessageListenerContainer listenerContainer, Object messageListener) {
return true
}
@Override
Channel createChannel(SimpleMessageListenerContainer listenerContainer, boolean transactional) {
createChannelCalled = true
transactionalChannel = transactional
return null
}
}
when:
messageVerifier.send(payload,
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
"amqp_receivedRoutingKey" : routingKey,
"contentType" : CONTENT_TYPE_JSON],
exchange)
then:
createChannelCalled
!transactionalChannel
}
def "should send amqp message for transactional channel"() {
given:
rabbitProperties.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.NONE)
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])
boolean createChannelCalled = false
boolean transactionalChannel = false
SpringAmqpStubMessages messageVerifier = new SpringAmqpStubMessages(rabbitTemplate, messageListenerAccessor, rabbitProperties) {
@Override
boolean isChannelAwareListener(SimpleMessageListenerContainer listenerContainer, Object messageListener) {
return true
}
@Override
Channel createChannel(SimpleMessageListenerContainer listenerContainer, boolean transactional) {
createChannelCalled = true
transactionalChannel = transactional
return null
}
}
when:
messageVerifier.send(payload,
[(DEFAULT_CLASSID_FIELD_NAME): "org.example.Some",
"amqp_receivedRoutingKey" : routingKey,
"contentType" : CONTENT_TYPE_JSON],
exchange)
then:
createChannelCalled
transactionalChannel
}
def "should fail to receive a message if rabbit template wasn't called"() {
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.receive("foo")
then:
thrown(WantedButNotInvoked)
}
def "should return null if received called and message was sent without any body"() {
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)
and:
rabbitTemplate.send("foo", "bar", null, null)
expect:
messageVerifier.receive("foo") == null
}
def "should return match the message if received called and message was sent with a message with null payload"() {
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)
message.getMessageProperties() >> new MessageProperties()
and:
rabbitTemplate.send("foo", "bar", message, null)
expect:
messageVerifier.receive("foo") is message
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.kafka
import spock.lang.Specification
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.kafka.ContractVerifierKafkaHelper
import org.springframework.messaging.Message
import org.springframework.messaging.support.MessageBuilder
import static org.mockito.Mockito.mock
/**
* Unit tests for {@link ContractVerifierKafkaHelper}.
*
* @author Chris Bono
*/
class ContractVerifierKafkaHelperSpec extends Specification {
def "should convert message with no headers"() {
given:
Message message = MessageBuilder
.withPayload("some-data")
.build()
ContractVerifierKafkaHelper contractVerifierKafkaHelper = new ContractVerifierKafkaHelper(mock(MessageVerifier.class))
when:
ContractVerifierMessage contractVerifierMessage = contractVerifierKafkaHelper.convert(message)
then:
contractVerifierMessage.payload == "some-data"
}
def "should convert message with basic header"() {
given:
Message message = MessageBuilder
.withPayload("some-data")
.setHeader("some-header", "5150")
.build()
ContractVerifierKafkaHelper contractVerifierKafkaHelper = new ContractVerifierKafkaHelper(mock(MessageVerifier.class))
when:
ContractVerifierMessage contractVerifierMessage = contractVerifierKafkaHelper.convert(message)
then:
contractVerifierMessage.payload == "some-data"
contractVerifierMessage.headers.containsKey("some-header")
contractVerifierMessage.headers.get("some-header") == "5150"
}
def "should convert message with byte[] header"() {
given:
Message message = MessageBuilder
.withPayload("some-data")
.setHeader("some-header", "5150".getBytes())
.build()
ContractVerifierKafkaHelper contractVerifierKafkaHelper = new ContractVerifierKafkaHelper(mock(MessageVerifier.class))
when:
ContractVerifierMessage contractVerifierMessage = contractVerifierKafkaHelper.convert(message)
then:
contractVerifierMessage.payload == "some-data"
contractVerifierMessage.headers.containsKey("some-header")
new String(contractVerifierMessage.headers.get("some-header")) == "5150"
}
}

View File

@@ -1,78 +0,0 @@
/*
* 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 org.junit.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.mock.mockito.MockitoPostProcessor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* @author Tim Ysewyn
* @author Henning Garus
*/
public class ContractVerifierAmqpAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class,
ContractVerifierAmqpAutoConfiguration.class, RabbitMockConnectionFactoryAutoConfiguration.class))
// register MockitoPostProcessor manually to perform the SpyBean injection for
// ContractVerifierAmqpAutoConfiguration. This is normally done automatically
// during test setup
.withInitializer(context -> MockitoPostProcessor.register((BeanDefinitionRegistry) context));
@Test
public void shouldNotCreateBeansByDefault() {
this.contextRunner.run((context) -> {
assertThat(context.getBeansOfType(SpringAmqpStubMessages.class)).hasSize(0);
assertThat(context.getBeansOfType(ContractVerifierHelper.class)).hasSize(0);
});
}
@Test
public void shouldNotCreateBeansWhenDisabled() {
this.contextRunner.withPropertyValues("stubrunner.amqp.enabled=false").run((context) -> {
assertThat(context.getBeansOfType(SpringAmqpStubMessages.class)).hasSize(0);
assertThat(context.getBeansOfType(ContractVerifierHelper.class)).hasSize(0);
});
}
@Test
public void shouldCreateBeansWhenExplicitlyEnabled() {
this.contextRunner.withPropertyValues("stubrunner.amqp.enabled=true").run((context) -> {
assertThat(context.getBeansOfType(SpringAmqpStubMessages.class)).hasSize(1);
assertThat(context.getBeansOfType(ContractVerifierHelper.class)).hasSize(1);
});
}
@Test
public void shouldNotThrowOnSendWhenRabbitTemplateIsTransacted() {
this.contextRunner.withPropertyValues("stubrunner.amqp.enabled=true").run(context -> assertThatCode(() -> {
RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.convertAndSend("A Message");
}).doesNotThrowAnyException());
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2021-2021 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 org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SpringAmqpStubMessagesTests {
@Test
void should_send_message_without_headers_and_contract() {
final RabbitTemplate rabbitTemplate = mock(RabbitTemplate.class);
final MessageListenerAccessor messageListenerAccessor = mock(MessageListenerAccessor.class);
final RabbitProperties rabbitProperties = mock(RabbitProperties.class);
final SimpleMessageListenerContainer messageListenerContainer = mock(SimpleMessageListenerContainer.class);
final MessageListener messageListener = mock(MessageListener.class);
when(messageListenerContainer.getMessageListener()).thenReturn(messageListener);
when(messageListenerAccessor.getListenerContainersForDestination(any(), any()))
.thenReturn(Collections.singletonList(messageListenerContainer));
final SpringAmqpStubMessages springAmqpStubMessages = new SpringAmqpStubMessages(rabbitTemplate,
messageListenerAccessor, rabbitProperties);
Assertions.assertThatCode(() -> springAmqpStubMessages.send(anyString(), null, anyString(), null))
.doesNotThrowAnyException();
}
}

View File

@@ -1,96 +0,0 @@
/*
* 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 java.util.function.Supplier;
import org.junit.Test;
import org.mockito.BDDMockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Tim Ysewyn
*/
public class ContractVerifierKafkaConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(KafkaAutoConfiguration.class, ContractVerifierKafkaConfiguration.class))
.withUserConfiguration(CustomConfiguration.class);
@Test
public void shouldCreateBeansByDefault() {
this.contextRunner.run((context) -> {
assertThat(context.getBeansOfType(KafkaStubMessages.class)).hasSize(1);
assertThat(context.getBeansOfType(ContractVerifierKafkaHelper.class)).hasSize(1);
});
}
@Test
public void shouldPickCustomKafkaTemplate() {
this.contextRunner.withUserConfiguration(CustomKafkaTemplateConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(KafkaStubMessages.class)).hasSize(1);
assertThat(context.getBean(KafkaStubMessages.class).kafkaTemplate)
.isSameAs(context.getBean(CustomKafkaTemplateConfiguration.class).myKafkaTemplate);
});
}
@Test
public void shouldNotCreateBeansWhenDisabled() {
this.contextRunner.withPropertyValues("stubrunner.kafka.enabled=false").run((context) -> {
assertThat(context.getBeansOfType(KafkaStubMessages.class)).hasSize(0);
assertThat(context.getBeansOfType(ContractVerifierKafkaHelper.class)).hasSize(0);
});
}
@Test
public void shouldCreateBeansWhenExplicitlyEnabled() {
this.contextRunner.withPropertyValues("stubrunner.kafka.enabled=true").run((context) -> {
assertThat(context.getBeansOfType(KafkaStubMessages.class)).hasSize(1);
assertThat(context.getBeansOfType(ContractVerifierKafkaHelper.class)).hasSize(1);
});
}
static class CustomConfiguration {
@Bean
public EmbeddedKafkaBroker embeddedKafkaBroker() {
return new EmbeddedKafkaBroker(1);
}
}
static class CustomKafkaTemplateConfiguration {
KafkaTemplate myKafkaTemplate = BDDMockito.mock(KafkaTemplate.class);
@Bean
Supplier<KafkaTemplate> myKafkaTemplate() {
return () -> myKafkaTemplate;
}
}
}

View File

@@ -45,18 +45,18 @@
</property>
</activation>
<modules>
<!-- <module>samples-messaging-camel</module>-->
<module>samples-messaging-camel</module>
<module>samples-messaging-integration</module>
<module>samples-messaging-amqp</module>
<!-- <module>samples-messaging-jms</module>-->
<!-- <module>spring-cloud-contract-stub-runner-camel</module>-->
<!-- <module>spring-cloud-contract-stub-runner-boot-eureka</module>-->
<module>samples-messaging-jms</module>
<module>spring-cloud-contract-stub-runner-camel</module>
<module>spring-cloud-contract-stub-runner-boot-eureka</module>
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
<module>spring-cloud-contract-stub-runner-context-path</module>
<module>spring-cloud-contract-stub-runner-integration</module>
<module>spring-cloud-contract-stub-runner-stream</module>
<module>spring-cloud-contract-stub-runner-amqp</module>
<!-- <module>spring-cloud-contract-stub-runner-jms</module>-->
<module>spring-cloud-contract-stub-runner-jms</module>
<module>spring-cloud-contract-stub-runner-kafka</module>
</modules>
</profile>
@@ -68,12 +68,12 @@
</property>
</activation>
<modules>
<!-- <module>samples-messaging-camel</module>-->
<module>samples-messaging-camel</module>
<module>samples-messaging-integration</module>
<module>samples-messaging-amqp</module>
<!-- <module>samples-messaging-jms</module>-->
<!-- <module>spring-cloud-contract-stub-runner-camel</module>-->
<!-- <module>spring-cloud-contract-stub-runner-boot-eureka</module>-->
<module>samples-messaging-jms</module>
<module>spring-cloud-contract-stub-runner-camel</module>
<module>spring-cloud-contract-stub-runner-boot-eureka</module>
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
<module>spring-cloud-contract-stub-runner-context-path</module>
<module>spring-cloud-contract-stub-runner-moco-contract-jar</module>
@@ -81,7 +81,7 @@
<module>spring-cloud-contract-stub-runner-integration</module>
<module>spring-cloud-contract-stub-runner-stream</module>
<module>spring-cloud-contract-stub-runner-amqp</module>
<!-- <module>spring-cloud-contract-stub-runner-jms</module>-->
<module>spring-cloud-contract-stub-runner-jms</module>
<module>spring-cloud-contract-stub-runner-kafka</module>
</modules>
</profile>

View File

@@ -22,6 +22,7 @@ import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.toomuchcoding.jsonassert.JsonAssertion
import groovy.json.JsonOutput
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
@@ -39,6 +40,7 @@ import static org.springframework.cloud.contract.verifier.messaging.util.Contrac
// Context configuration would end up in base class
@AutoConfigureMessageVerifier
@SpringBootTest(classes = AmqpMessagingApplication, properties = "stubrunner.amqp.enabled=true")
@Disabled("TODO: Migrate to middleware based approach")
class AmqpMessagingApplicationSpec {
// ALL CASES

View File

@@ -16,23 +16,23 @@
<dependencies>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<artifactId>camel-rabbitmq-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jms</artifactId>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-direct-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-activemq</artifactId>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-bean-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-jackson-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jackson</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -50,8 +50,18 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-junit4</artifactId>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -31,4 +31,9 @@ public class BookReturned implements Serializable {
this.bookName = bookName;
}
@Override
public String toString() {
return "BookReturned{" + "bookName='" + bookName + '\'' + '}';
}
}

View File

@@ -16,9 +16,10 @@
package com.example;
import org.apache.camel.CamelContext;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.activemq.ActiveMQComponent;
import org.apache.camel.component.rabbitmq.RabbitMQComponent;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.springframework.beans.factory.annotation.Value;
@@ -32,26 +33,25 @@ import org.springframework.context.annotation.Configuration;
public class BookRouteConfiguration {
@Bean
ActiveMQComponent activeMQComponent(@Value("${activemq.url:vm://localhost?broker.persistent=false}") String url) {
ActiveMQComponent component = new ActiveMQComponent();
component.setBrokerURL(url);
return component;
}
@Bean
RoutesBuilder myRouter(final BookService bookService, final BookDeleter bookDeleter) {
RoutesBuilder myRouter(final BookService bookService, final BookDeleter bookDeleter, CamelContext context,
@Value("${spring.rabbitmq.port}") int port) {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
RabbitMQComponent component = context.getComponent("rabbitmq", RabbitMQComponent.class);
component.setAddresses("localhost:" + port);
// scenario 1 - from bean to output
from("direct:start").unmarshal().json(JsonLibrary.Jackson, BookReturned.class).bean(bookService)
.to("jms:output");
.marshal().json(JsonLibrary.Jackson, BookReturned.class).to("rabbitmq:output?queue=output");
// scenario 2 - from input to output
from("jms:input").unmarshal().json(JsonLibrary.Jackson, BookReturned.class).bean(bookService)
.to("jms:output");
from("rabbitmq:input?queue=input").unmarshal().json(JsonLibrary.Jackson, BookReturned.class)
.bean(bookService).marshal().json(JsonLibrary.Jackson, BookReturned.class)
.to("rabbitmq:output");
// scenario 3 - from input to no output
from("jms:delete").unmarshal().json(JsonLibrary.Jackson, BookDeleted.class).bean(bookDeleter);
from("rabbitmq:delete?queue=delete").unmarshal().json(JsonLibrary.Jackson, BookDeleted.class)
.bean(bookDeleter);
}
};

View File

@@ -23,25 +23,28 @@ import com.jayway.jsonpath.JsonPath
import com.toomuchcoding.jsonassert.JsonAssertion
import org.apache.camel.Message
import org.apache.camel.model.ModelCamelContext
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import org.awaitility.Awaitility
import org.junit.jupiter.api.Test
import org.testcontainers.containers.RabbitMQContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
/**
* SPIKE ON TESTS FROM NOTES IN MessagingSpec
*/
// Context configuration would end up in base class
@AutoConfigureMessageVerifier
@SpringBootTest(classes = CamelMessagingApplication)
class CamelMessagingApplicationSpec extends Specification {
@SpringBootTest(classes = CamelMessagingApplication, properties = 'camel.component.rabbitmq.declare=true')
@Testcontainers
class CamelMessagingApplicationSpec {
// ALL CASES
@Autowired
@@ -53,11 +56,16 @@ class CamelMessagingApplicationSpec extends Specification {
ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper()
void setupSpec() {
System.setProperty("org.apache.activemq.SERIALIZABLE_PACKAGES", "*")
@Container
static RabbitMQContainer broker = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine");
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.rabbitmq.port", () -> broker.getAmqpPort());
}
def "should work for triggered based messaging"() {
@Test
void "should work for triggered based messaging"() {
given:
Contract.make {
label 'some_label'
@@ -65,7 +73,7 @@ class CamelMessagingApplicationSpec extends Specification {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
sentTo('rabbitmq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
@@ -76,20 +84,21 @@ class CamelMessagingApplicationSpec extends Specification {
when:
bookReturnedTriggered()
then:
def response = contractVerifierMessaging.receive('activemq:output')
response.headers.get('BOOK-NAME') == 'foo'
def response = contractVerifierMessaging.receive('rabbitmq:output')
assert response.headers.get('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.body))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
def "should generate tests triggered by a message"() {
@Test
void "should generate tests triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageFrom('rabbitmq:input')
messageBody([
bookName: 'foo'
])
@@ -99,7 +108,7 @@ class CamelMessagingApplicationSpec extends Specification {
}
}
outputMessage {
sentTo('jms:output')
sentTo('rabbitmq:output')
body([
bookName: 'foo'
])
@@ -112,22 +121,23 @@ class CamelMessagingApplicationSpec extends Specification {
when:
contractVerifierMessaging.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'jms:input')
[sample: 'header'], 'rabbitmq:input')
then:
def response = contractVerifierMessaging.receive('jms:output')
response.headers.get('BOOK-NAME') == 'foo'
def response = contractVerifierMessaging.receive('rabbitmq:output')
assert response.headers.get('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.body))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
def "should generate tests without destination, triggered by a message"() {
@Test
void "should generate tests without destination, triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageFrom('rabbitmq:delete')
messageBody([
bookName: 'foo'
])
@@ -141,9 +151,8 @@ class CamelMessagingApplicationSpec extends Specification {
when:
contractVerifierMessaging.
send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'jms:delete')
[sample: 'header'], 'rabbitmq:delete')
then:
noExceptionThrown()
bookWasDeleted()
}
@@ -152,12 +161,9 @@ class CamelMessagingApplicationSpec extends Specification {
sendBody('direct:start', '''{"bookName" : "foo" }''')
}
PollingConditions pollingConditions = new PollingConditions()
void bookWasDeleted() {
pollingConditions.eventually {
Awaitility.await().untilAsserted(() -> {
assert bookDeleter.bookSuccessfulyDeleted.get()
}
})
}
}

View File

@@ -0,0 +1,15 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
</configuration>

View File

@@ -16,7 +16,15 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-server</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -25,7 +33,7 @@
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -33,6 +41,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -18,13 +18,12 @@ package com.example;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.jms.JMSException;
import javax.jms.Message;
import jakarta.jms.JMSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@@ -46,7 +45,7 @@ public class BookDeleter {
public void bookDeleted(Message message) throws JMSException {
log.info("Deleting book " + message);
this.bookSuccessfulyDeleted.set(true);
log.info("Book successfuly deleted [" + this.bookSuccessfulyDeleted + "]");
log.info("Book successfully deleted [" + this.bookSuccessfulyDeleted + "]");
}
}

View File

@@ -44,7 +44,7 @@ public class BookService {
public void returnBook() {
BookReturned bookReturned = new BookReturned("foo");
jmsTemplate.convertAndSend("output2", "{\"bookName\":\"foo\"}", message -> {
message.setStringProperty("BOOK-NAME", bookReturned.bookName);
message.setStringProperty("BOOKNAME", bookReturned.bookName);
return message;
});
}

View File

@@ -17,14 +17,15 @@
package com.example
import javax.inject.Inject
import javax.jms.JMSException
import javax.jms.Message
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.toomuchcoding.jsonassert.JsonAssertion
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import jakarta.jms.JMSException
import jakarta.jms.Message
import org.awaitility.Awaitility
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@@ -44,131 +45,131 @@ import org.springframework.test.annotation.DirtiesContext
// Context configuration would end up in base class
@AutoConfigureMessageVerifier
@SpringBootTest(classes = JmsMessagingApplication)
class JmsMessagingApplicationSpec extends Specification {
class JmsMessagingApplicationSpec {
// ALL CASES
@Autowired
JmsTemplate jmsTemplate
@Autowired
BookDeleter bookDeleter
@Inject
MessageVerifier<Message> messageVerifier
@Inject
ContractVerifierMessaging contractVerifierMessaging
@Inject
ContractVerifierObjectMapper contractVerifierObjectMapper
// ALL CASES
@Autowired
JmsTemplate jmsTemplate
@Autowired
BookDeleter bookDeleter
@Inject
MessageVerifier<Message> messageVerifier
@Inject
ContractVerifierMessaging contractVerifierMessaging
@Inject
ContractVerifierObjectMapper contractVerifierObjectMapper
void setupSpec() {
System.setProperty("org.apache.activemq.SERIALIZABLE_PACKAGES", "*")
System.setProperty("debug", "true")
}
@BeforeAll
static void setupSpec() {
System.setProperty("debug", "true")
}
def "should work for triggered based messaging"() {
given:
Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
// generated test should look like this:
when:
bookReturnedTriggered()
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('output')
response.getHeader('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@Test
void "should work for triggered based messaging"() {
given:
Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOKNAME', 'foo')
}
}
}
// generated test should look like this:
when:
bookReturnedTriggered()
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('output')
assert response.getHeader('BOOKNAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@DirtiesContext
def "should generate tests triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('input2')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output2')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
// generated test should look like this:
when:
messageVerifier.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'input2')
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('output2')
response.getHeader('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@DirtiesContext
@Test
void "should generate tests triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('input2')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output2')
body([
bookName: 'foo'
])
headers {
header('BOOKNAME', 'foo')
}
}
}
// generated test should look like this:
when:
messageVerifier.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'input2')
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('output2')
assert response.getHeader('BOOKNAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
def "should generate tests without destination, triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// generated test should look like this:
when:
messageVerifier.
send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'delete')
then:
noExceptionThrown()
bookWasDeleted()
}
@Test
void "should generate tests without destination, triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// generated test should look like this:
when:
messageVerifier.
send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'delete')
then:
bookWasDeleted()
}
void bookReturnedTriggered() {
jmsTemplate.convertAndSend("output", '''{"bookName" : "foo" }''', new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("BOOK-NAME", "foo")
return message
}
})
}
void bookReturnedTriggered() {
jmsTemplate.convertAndSend("output", '''{"bookName" : "foo" }''', new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("BOOKNAME", "foo")
return message
}
})
}
PollingConditions pollingConditions = new PollingConditions()
void bookWasDeleted() {
pollingConditions.eventually {
assert bookDeleter.bookSuccessfulyDeleted.get()
}
}
void bookWasDeleted() {
Awaitility.await().untilAsserted( () -> {
assert bookDeleter.bookSuccessfulyDeleted.get()
})
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
@@ -27,6 +28,7 @@ import org.springframework.test.context.ActiveProfiles
@AutoConfigureStubRunner
@SpringBootTest(classes = AmqpMessagingApplication)
@ActiveProfiles("listener")
@Disabled("TODO: Migrate to middleware based approach")
class AmqpStubRunnerRabbitListenerSpec {
@Autowired

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
@@ -30,6 +31,7 @@ import static org.mockito.BDDMockito.then
@AutoConfigureStubRunner
@SpringBootTest(classes = AmqpMessagingApplication)
@Disabled("TODO: Migrate to middleware based approach")
class AmqpStubRunnerSpec {
@Autowired

View File

@@ -13,20 +13,11 @@
<packaging>jar</packaging>
<name>Spring Cloud Contract Stub Runner Boot Eureka</name>
<description>Spring Cloud Contract Stub Runner Boot Eureka</description>
<properties>
<jersey-apache-client4.version>1.19.1</jersey-apache-client4.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-apache-client4</artifactId>
<version>${jersey-apache-client4.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner-jetty</artifactId>
@@ -42,25 +33,11 @@
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -76,6 +53,11 @@
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -16,41 +16,35 @@
package org.springframework.cloud.contract.stubrunner.spring.cloud.eureka
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import java.util.concurrent.TimeUnit
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import groovy.util.logging.Slf4j
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.FilterConfig
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.awaitility.Awaitility
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.contract.stubrunner.StubFinder
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.cloud.netflix.eureka.EnableEurekaClient
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.http.client.ClientHttpResponse
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.client.DefaultResponseErrorHandler
import org.springframework.web.client.RequestCallback
import org.springframework.web.client.ResponseExtractor
import org.springframework.web.client.RestClientException
import org.springframework.web.client.RestTemplate
/**
* @author Marcin Grzejszczak
*/
@@ -62,24 +56,24 @@ import org.springframework.web.client.RestTemplate
"eureka.client.enabled=true",
"eureka.instance.leaseRenewalIntervalInSeconds=1",
"ribbon.ServerListRefreshInterval=100"])
@AutoConfigureStubRunner(ids =
["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"] ,
@AutoConfigureStubRunner(ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer", "org.springframework.cloud.contract.verifier.stubs:bootService"] ,
repositoryRoot = "classpath:m2repo/repository/" ,
stubsMode = StubRunnerProperties.StubsMode.REMOTE )
class StubRunnerSpringCloudEurekaAutoConfigurationSpec extends Specification {
@Slf4j
class StubRunnerSpringCloudEurekaAutoConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
@LoadBalanced
RestTemplate restTemplate
@Shared
@AutoCleanup
ConfigurableApplicationContext eurekaServer
void setupSpec() {
static ConfigurableApplicationContext eurekaServer
@BeforeAll
static void setupSpec() {
System.clearProperty("stubrunner.stubs.repository.root")
System.clearProperty("stubrunner.stubs.classifier")
eurekaServer = SpringApplication.run(EurekaServer,
@@ -90,44 +84,41 @@ class StubRunnerSpringCloudEurekaAutoConfigurationSpec extends Specification {
"--spring.profiles.active=eureka")
}
void cleanupSpec() {
@AfterAll
static void cleanupSpec() {
System.clearProperty("stubrunner.stubs.repository.root")
System.clearProperty("stubrunner.stubs.classifier")
}
PollingConditions conditions = new PollingConditions(timeout: 240, delay: 1)
def 'should make service discovery work'() {
@Test
void 'should make service discovery work'() {
expect: 'WireMocks are running'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
conditions.eventually {
assert restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
}
restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
log.info("Waiting for stubs to register in Eureka...")
Awaitility.await()
.pollInterval(1, TimeUnit.SECONDS)
.pollDelay(10, TimeUnit.SECONDS)
.atMost(1, TimeUnit.MINUTES)
.untilAsserted(() -> {
try {
assert restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
} catch (Exception ex) {
throw new AssertionError(ex)
}
})
assert restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
@Configuration
@EnableAutoConfiguration
@EnableEurekaClient
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
def template = new RestTemplate() {
@Override
protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor) throws RestClientException {
try {
return super.doExecute(url, method, requestCallback, responseExtractor)
}
catch (Exception e) {
throw new AssertionError(e)
}
}
}
def template = new RestTemplate()
template.errorHandler = new DefaultResponseErrorHandler() {
@Override
void handleError(ClientHttpResponse response) throws IOException {

View File

@@ -31,25 +31,11 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-jackson</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -60,11 +46,6 @@
<artifactId>camel-activemq</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@@ -1,283 +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.stubrunner.messaging.camel
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.apache.activemq.spring.ActiveMQConnectionFactory
import org.apache.camel.CamelContext
import org.apache.camel.ConsumerTemplate
import org.apache.camel.Exchange
import org.apache.camel.ProducerTemplate
import org.apache.camel.component.activemq.ActiveMQComponent
import org.apache.camel.component.jms.JmsConfiguration
import org.apache.camel.impl.engine.DefaultShutdownStrategy
import spock.lang.Ignore
import spock.lang.Specification
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.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.stubrunner.StubFinder
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
/**
* @author Marcin Grzejszczak
*/
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(properties = "debug=true")
@AutoConfigureStubRunner
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@Ignore
class CamelStubRunnerSpec extends Specification {
//TODO: fix extremely slow test that causes pipeline to fail and then unignore
@Autowired
StubFinder stubFinder
@Autowired
CamelContext camelContext
ConsumerTemplate consumerTemplate
ProducerTemplate producerTemplate
def setup() {
consumerTemplate = camelContext.createConsumerTemplate()
producerTemplate = camelContext.createProducerTemplate()
}
def cleanup() {
// ensure that message were taken from the queue
consumerTemplate.receive('jms:output', 100)
consumerTemplate.receive('jms:input', 100)
this.producerTemplate.stop()
this.consumerTemplate.stop()
def strategy = new DefaultShutdownStrategy(this.camelContext)
strategy.timeout = 1
this.camelContext.shutdownStrategy = strategy
}
def 'should not trigger a message that does not match input'() {
when:
producerTemplate.
sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value'])
then:
Exchange receivedMessage = consumerTemplate.receive('jms:output', 100)
and:
receivedMessage == null
}
def 'should download the stub and register a route for it'() {
when:
// tag::client_send[]
producerTemplate.
sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])
// end::client_send[]
then:
// tag::client_receive[]
Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)
// end::client_receive[]
and:
// tag::client_receive_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
// end::client_receive_message[]
}
def 'should trigger a message by label'() {
when:
// tag::client_trigger[]
stubFinder.trigger('return_book_1')
// end::client_trigger[]
then:
// tag::client_trigger_receive[]
Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)
// end::client_trigger_receive[]
and:
// tag::client_trigger_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
// end::client_trigger_message[]
}
def 'should trigger a label for the existing groupId:artifactId'() {
when:
// tag::trigger_group_artifact[]
stubFinder.
trigger('org.springframework.cloud.contract.verifier.stubs:camelService', 'return_book_1')
// end::trigger_group_artifact[]
then:
Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
}
def 'should trigger a label for the existing artifactId'() {
when:
// tag::trigger_artifact[]
stubFinder.trigger('camelService', 'return_book_1')
// end::trigger_artifact[]
then:
Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
}
def 'should throw an exception when missing label is passed'() {
when:
stubFinder.trigger('missing label')
then:
thrown(IllegalArgumentException)
}
def 'should throw an exception when missing label and artifactid is passed'() {
when:
stubFinder.trigger('some:service', 'return_book_1')
then:
thrown(IllegalArgumentException)
}
def 'should trigger messages by running all triggers'() {
when:
// tag::trigger_all[]
stubFinder.trigger()
// end::trigger_all[]
then:
Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
}
def 'should trigger a label with no output message'() {
when:
// tag::trigger_no_output[]
producerTemplate.
sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])
// end::trigger_no_output[]
then:
noExceptionThrown()
}
private boolean assertThatBodyContainsBookNameFoo(Object payload) {
String objectAsString = payload instanceof String ? payload :
JsonOutput.toJson(payload)
def json = new JsonSlurper().parseText(objectAsString)
return json.bookName == 'foo'
}
@Configuration
@ComponentScan
@EnableAutoConfiguration
static class Config {
@Bean
ActiveMQConnectionFactory activeMQConnectionFactory(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL: url)
try {
factory.trustAllPackages = true
}
catch (Throwable e) {
}
return factory
}
@Bean
JmsConfiguration jmsConfiguration(ActiveMQConnectionFactory activeMQConnectionFactory) {
return new JmsConfiguration(connectionFactory: activeMQConnectionFactory)
}
@Bean
ActiveMQComponent activeMQComponent(JmsConfiguration jmsConfiguration) {
return new ActiveMQComponent(configuration: jmsConfiguration)
}
}
Contract dsl =
// tag::sample_dsl[]
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('jms:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
// end::sample_dsl[]
Contract dsl2 =
// tag::sample_dsl_2[]
Contract.make {
label 'return_book_2'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
// end::sample_dsl_2[]
Contract dsl3 =
// tag::sample_dsl_3[]
Contract.make {
label 'delete_book'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// end::sample_dsl_3[]
}

View File

@@ -14,38 +14,24 @@
<name>Spring Cloud Contract Stub Runner JMS</name>
<description>Spring Cloud Contract Stub Runner JMS</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner-jetty</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -16,24 +16,23 @@
package org.springframework.cloud.contract.stubrunner.messaging.jms
import javax.jms.JMSException
import javax.jms.Message
import javax.jms.TextMessage
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.apache.activemq.ActiveMQConnectionFactory
import spock.lang.IgnoreIf
import spock.lang.Specification
import jakarta.jms.JMSException
import jakarta.jms.Message
import jakarta.jms.TextMessage
import org.assertj.core.api.BDDAssertions
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS
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.cloud.contract.spec.Contract
import org.springframework.cloud.contract.stubrunner.StubFinder
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.jms.annotation.EnableJms
@@ -44,21 +43,23 @@ import org.springframework.jms.core.MessagePostProcessor
*/
@SpringBootTest(classes = Config, properties = ["debug=true"])
@AutoConfigureStubRunner
@IgnoreIf({ os.windows })
class JmsStubRunnerSpec extends Specification {
@DisabledOnOs(OS.WINDOWS)
class JmsStubRunnerSpec {
@Autowired
StubFinder stubFinder
@Autowired
JmsTemplate jmsTemplate
def cleanup() {
@AfterEach
void cleanup() {
// ensure that message were taken from the queue
jmsTemplate.receive('output')
jmsTemplate.receive('input')
}
def 'should download the stub and register a route for it'() {
@Test
void 'should download the stub and register a route for it'() {
when:
// tag::client_send[]
jmsTemplate.
@@ -76,13 +77,14 @@ class JmsStubRunnerSpec extends Specification {
// end::client_receive[]
and:
// tag::client_receive_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getText())
assert receivedMessage.getStringProperty('BOOKNAME') == 'foo'
// end::client_receive_message[]
}
def 'should trigger a message by label'() {
@Test
void 'should trigger a message by label'() {
when:
// tag::client_trigger[]
stubFinder.trigger('return_book_1')
@@ -93,13 +95,14 @@ class JmsStubRunnerSpec extends Specification {
// end::client_trigger_receive[]
and:
// tag::client_trigger_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getText())
assert receivedMessage.getStringProperty('BOOKNAME') == 'foo'
// end::client_trigger_message[]
}
def 'should trigger a label for the existing groupId:artifactId'() {
@Test
void 'should trigger a label for the existing groupId:artifactId'() {
when:
// tag::trigger_group_artifact[]
stubFinder.
@@ -108,12 +111,13 @@ class JmsStubRunnerSpec extends Specification {
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getText())
assert receivedMessage.getStringProperty('BOOKNAME') == 'foo'
}
def 'should trigger a label for the existing artifactId'() {
@Test
void 'should trigger a label for the existing artifactId'() {
when:
// tag::trigger_artifact[]
stubFinder.trigger('stubs', 'return_book_1')
@@ -121,26 +125,25 @@ class JmsStubRunnerSpec extends Specification {
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getText())
assert receivedMessage.getStringProperty('BOOKNAME') == 'foo'
}
def 'should throw an exception when missing label is passed'() {
@Test
void 'should throw an exception when missing label is passed'() {
when:
stubFinder.trigger('missing label')
then:
thrown(IllegalArgumentException)
BDDAssertions.thenThrownBy(() -> stubFinder.trigger('missing label')).isInstanceOf(IllegalArgumentException)
}
def 'should throw an exception when missing label and artifactid is passed'() {
@Test
void 'should throw an exception when missing label and artifactid is passed'() {
when:
stubFinder.trigger('some:service', 'return_book_1')
then:
thrown(IllegalArgumentException)
BDDAssertions.thenThrownBy(() -> stubFinder.trigger('some:service', 'return_book_1')).isInstanceOf(IllegalArgumentException)
}
def 'should trigger messages by running all triggers'() {
@Test
void 'should trigger messages by running all triggers'() {
when:
// tag::trigger_all[]
stubFinder.trigger()
@@ -148,12 +151,13 @@ class JmsStubRunnerSpec extends Specification {
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getText())
assert receivedMessage.getStringProperty('BOOKNAME') == 'foo'
}
def 'should trigger a label with no output message'() {
@Test
void 'should trigger a label with no output message'() {
when:
// tag::trigger_no_output[]
jmsTemplate.
@@ -165,11 +169,10 @@ class JmsStubRunnerSpec extends Specification {
}
})
// end::trigger_no_output[]
then:
noExceptionThrown()
}
def 'should not trigger a message that does not match input'() {
@Test
void 'should not trigger a message that does not match input'() {
when:
jmsTemplate.
convertAndSend('input', new BookReturned('notmatching'), new MessagePostProcessor() {
@@ -182,7 +185,7 @@ class JmsStubRunnerSpec extends Specification {
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage == null
assert receivedMessage == null
}
private boolean assertThatBodyContainsBookNameFoo(Object payload) {
@@ -197,16 +200,7 @@ class JmsStubRunnerSpec extends Specification {
@EnableAutoConfiguration
@EnableJms
static class Config {
@Bean
ActiveMQConnectionFactory activeMQConnectionFactory(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL: url)
try {
factory.trustAllPackages = true
}
catch (Throwable e) {
}
return factory
}
}
Contract dsl =
@@ -220,7 +214,7 @@ class JmsStubRunnerSpec extends Specification {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
header('BOOKNAME', 'foo')
}
}
}
@@ -245,7 +239,7 @@ class JmsStubRunnerSpec extends Specification {
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
header('BOOKNAME', 'foo')
}
}
}

View File

@@ -7,7 +7,7 @@ org.springframework.cloud.contract.spec.Contract.make {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
header('BOOKNAME', 'foo')
}
}
}
}

View File

@@ -15,7 +15,7 @@ org.springframework.cloud.contract.spec.Contract.make {
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
header('BOOKNAME', 'foo')
}
}
}
}

View File

@@ -27,6 +27,7 @@ import org.assertj.core.api.BDDAssertions
import org.awaitility.Awaitility
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS
@@ -55,6 +56,7 @@ import org.springframework.stereotype.Component
@DisabledOnOs(value = OS.WINDOWS)
@EmbeddedKafka(topics = ["input", "input2", "output", "delete"])
@Commons
@Disabled("TODO: Migrate to middleware based approach")
class KafkaStubRunnerSpec {
@Autowired