Added integration for JMS

fixes gh-1141
This commit is contained in:
Marcin Grzejszczak
2019-09-09 14:22:02 +02:00
parent 530fdee87f
commit c7b4f0125b
33 changed files with 1941 additions and 15 deletions

View File

@@ -101,6 +101,7 @@ You can use one of the following four integration configurations:
* Spring Integration
* Spring Cloud Stream
* Spring AMQP
* Spring JMS
Since we use Spring Boot, if you have added one of these libraries to the classpath, all
the messaging configuration is automatically set up.
@@ -1013,3 +1014,140 @@ stubrunner:
mockConnection: false
----
====
[[features-messaging-stub-runner-jms]]
=== Consumer Side Messaging With Spring JMS
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
integrate with Spring JMS.
The integration assumes that you have a running instance of a JMS broker (e.g. `activemq` embedded broker).
[[features-messaging-stub-runner-jms-adding]]
==== Adding the Runner to the Project
You need to have both Spring JMS and Spring Cloud Contract Stub Runner on the classpath. Remember to annotate your test class
with `@AutoConfigureStubRunner`.
:input_name: input
:output_name: output
[[features-messaging-stub-runner-jms-example]]
==== Examples
Assume that the stub structure looks as follows:
====
[source,bash,indent=0]
----
├── stubs
├── bookDeleted.groovy
├── bookReturned1.groovy
└── bookReturned2.groovy
----
====
Further assume the following test configuration:
====
[source,yml,indent=0]
----
stubrunner:
repository-root: stubs:classpath:/stubs/
ids: my:stubs
stubs-mode: remote
spring:
activemq:
send-timeout: 1000
jms:
template:
receive-timeout: 1000
----
====
Now consider the following contracts (we number them 1 and 2):
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, we use the `StubTigger` interface, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message would then pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `{output_name}` destination.
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message would pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
====

View File

@@ -65,6 +65,11 @@
<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.apache.camel</groupId>
<artifactId>camel-spring-boot-starter</artifactId>

View File

@@ -255,6 +255,7 @@ class StubRunnerExecutor implements StubFinder {
OutputMessage outputMessage = groovyDsl.getOutputMessage();
DslProperty<?> body = outputMessage.getBody();
Headers headers = outputMessage.getHeaders();
// TODO: Json is harcoded here
this.contractVerifierMessaging.send(
JsonOutput.toJson(BodyExtractor.extractClientValueFromBody(
body == null ? null : body.getClientValue())),

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2013-2019 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.jms;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.StreamMessage;
import javax.jms.TextMessage;
final class StubRunnerJmsAccessor {
private StubRunnerJmsAccessor() {
throw new IllegalStateException("Can't instantiate an utility class");
}
static Object getBody(Message message) {
try {
return getPayload(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
static Map<String, Object> getHeaders(Message message) {
try {
return headers(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
private static Map<String, Object> headers(Message message) throws JMSException {
Map<String, Object> headers = new HashMap<>();
if (message == null) {
return headers;
}
Enumeration enumeration = message.getPropertyNames();
while (enumeration.hasMoreElements()) {
Object element = enumeration.nextElement();
String asString = element.toString();
Object property = message.getObjectProperty(asString);
headers.put(asString, property);
}
return headers;
}
private static Object getPayload(Message message) throws JMSException {
if (message == null) {
return null;
}
else if (message instanceof TextMessage) {
return ((TextMessage) message).getText();
}
else if (message instanceof StreamMessage) {
return ((StreamMessage) message).readObject();
}
else if (message instanceof ObjectMessage) {
return ((ObjectMessage) message).getObject();
}
return message.getBody(Object.class);
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2013-2019 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.jms;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.jms.ConnectionFactory;
import javax.jms.MessageListener;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.spec.Contract;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.listener.MessageListenerContainer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration
@ConditionalOnClass(JmsTemplate.class)
@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true",
matchIfMissing = true)
public class StubRunnerJmsConfiguration {
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner
.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts
.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(
dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
List<Contract> matchingContracts = entries.getValue();
final String flowName = name + "_" + entries.getKey() + "_"
+ Math.abs(matchingContracts.hashCode());
// listener
StubRunnerJmsRouter router = new StubRunnerJmsRouter(matchingContracts,
beanFactory);
StubRunnerJmsRouter listener = (StubRunnerJmsRouter) beanFactory
.initializeBean(router, flowName);
beanFactory.registerSingleton(flowName, listener);
registerContainers(beanFactory, matchingContracts, flowName, listener);
}
}
return new FlowRegistrar();
}
private void registerContainers(ConfigurableListableBeanFactory beanFactory,
List<Contract> matchingContracts, String flowName,
StubRunnerJmsRouter listener) {
// listener's container
ConnectionFactory connectionFactory = beanFactory
.getBean(ConnectionFactory.class);
for (Contract matchingContract : matchingContracts) {
if (matchingContract.getInput() == null) {
continue;
}
String destination = MapConverter.getStubSideValuesForNonBody(
matchingContract.getInput().getMessageFrom()).toString();
MessageListenerContainer container = listenerContainer(destination,
connectionFactory, listener);
String containerName = flowName + ".container";
Object initializedContainer = beanFactory.initializeBean(container,
containerName);
beanFactory.registerSingleton(containerName, initializedContainer);
}
}
private MessageListenerContainer listenerContainer(String queueName,
ConnectionFactory connectionFactory, MessageListener listener) {
DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setDestinationName(queueName);
container.setMessageListener(listener);
return container;
}
static class FlowRegistrar {
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright 2013-2019 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.jms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import javax.jms.Message;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerJmsMessageSelector {
private static final Map<Message, Contract> CACHE = Collections
.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerJmsMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
Contract matchingContract(Message message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl
+ "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = StubRunnerJmsAccessor.getBody(message);
Object dslBody = MapConverter
.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug(
"Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass()
+ "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(),
(byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug(
"Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage,
Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage,
groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern)
+ " but the value is [" + dslBody.toString() + "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage,
BodyMatchers matchers, Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter
.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(
matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath
.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter
.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to "
+ unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath,
DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = StubRunnerJmsAccessor.getHeaders(message);
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null
&& valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to "
+ unmatchedText(value) + " but the value is ["
+ (valueInHeader != null ? valueInHeader.toString() : "null")
+ "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2013-2019 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.jms;
import java.util.List;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerJmsRouter implements MessageListener {
private final StubRunnerJmsMessageSelector selector;
private final BeanFactory beanFactory;
private final List<Contract> contracts;
private JmsTemplate jmsTemplate;
StubRunnerJmsRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
this.contracts = groovyDsls;
}
@Override
public void onMessage(javax.jms.Message message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null
&& dsl.getOutputMessage().getSentTo() != null) {
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
jmsTemplate().send(destination,
session -> new StubRunnerJmsTransformer(this.contracts)
.transform(session, dsl));
}
}
private JmsTemplate jmsTemplate() {
if (this.jmsTemplate == null) {
this.jmsTemplate = this.beanFactory.getBean(JmsTemplate.class);
}
return this.jmsTemplate;
}
}
class ReplyToProcessor implements MessagePostProcessor {
@Override
public javax.jms.Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("requiresReply", "no");
return message;
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2013-2019 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.jms;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import javax.jms.BytesMessage;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerJmsTransformer {
private final StubRunnerJmsMessageSelector selector;
StubRunnerJmsTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
}
public Message transform(Session session, Contract groovyDsl) {
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders()
.asStubSideMap();
Message newMessage = createMessage(session, outputBody);
setHeaders(newMessage, headers);
this.selector.updateCache(newMessage, groovyDsl);
return newMessage;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor
.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
Contract matchingContract(Message source) {
return this.selector.matchingContract(source);
}
private Message createMessage(Session session, Object payload) {
try {
if (payload instanceof String) {
return session.createTextMessage((String) payload);
}
else if (payload instanceof byte[]) {
BytesMessage bytesMessage = session.createBytesMessage();
bytesMessage.writeBytes((byte[]) payload);
return bytesMessage;
}
else if (payload instanceof Serializable) {
return session.createObjectMessage((Serializable) payload);
}
return session.createMessage();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private void setHeaders(Message message, Map<String, Object> headers) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
try {
if (value instanceof String) {
message.setStringProperty(key, (String) value);
}
else if (value instanceof Boolean) {
message.setBooleanProperty(key, (Boolean) value);
}
else {
message.setObjectProperty(key, value);
}
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -4,6 +4,7 @@ org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.StubRunnerSpringCloudAutoConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon.StubRunnerRibbonAutoConfiguration,\
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.zookeeper.StubRunnerSpringCloudZookeeperAutoConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.eureka.StubRunnerSpringCloudEurekaAutoConfiguration,\

View File

@@ -31,6 +31,16 @@
<artifactId>spring-messaging</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.jms</groupId>
<artifactId>jakarta.jms-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring-boot-starter</artifactId>

View File

@@ -30,12 +30,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.stereotype.Component;
/**
* @author Marcin Grzejszczak
*/
@Component
public class CamelStubMessages implements MessageVerifier<Message> {
private static final Logger log = LoggerFactory.getLogger(CamelStubMessages.class);

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
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;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -38,9 +39,9 @@ import org.springframework.context.annotation.Import;
@Configuration
@ConditionalOnClass(Message.class)
@Import(CamelAutoConfiguration.class)
@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true",
matchIfMissing = true)
@AutoConfigureBefore(NoOpContractVerifierAutoConfiguration.class)
@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore({ NoOpContractVerifierAutoConfiguration.class,
ContractVerifierJmsConfiguration.class })
public class ContractVerifierCamelConfiguration {
@Bean

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2013-2019 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.jms;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.StreamMessage;
import javax.jms.TextMessage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
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.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.jms.core.JmsTemplate;
/**
* @author Marcin Grzejszczak
*/
@Configuration
@ConditionalOnClass(JmsTemplate.class)
@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true",
matchIfMissing = true)
@AutoConfigureBefore(NoOpContractVerifierAutoConfiguration.class)
public class ContractVerifierJmsConfiguration {
@Bean
@ConditionalOnMissingBean
MessageVerifier<Message> contractVerifierJmsMessageExchange(
ObjectProvider<JmsTemplate> jmsTemplateProvider) {
JmsTemplate jmsTemplate = jmsTemplateProvider.getIfAvailable(JmsTemplate::new);
return new JmsStubMessages(jmsTemplate);
}
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierJmsMessaging(
MessageVerifier<Message> exchange) {
return new ContractVerifierJmsHelper(exchange);
}
}
class ContractVerifierJmsHelper extends ContractVerifierMessaging<Message> {
private static final Log log = LogFactory.getLog(ContractVerifierJmsHelper.class);
ContractVerifierJmsHelper(MessageVerifier<Message> exchange) {
super(exchange);
}
@Override
protected ContractVerifierMessage convert(Message message) {
try {
Map<String, Object> headers = headers(message);
return new ContractVerifierMessage(getPayload(message), headers);
}
catch (JMSException ex) {
log.warn("An exception occurred while trying to convert the JMS message", ex);
throw new IllegalStateException(ex);
}
}
private Map<String, Object> headers(Message message) throws JMSException {
Map<String, Object> headers = new HashMap<>();
if (message == null) {
return headers;
}
Enumeration enumeration = message.getPropertyNames();
while (enumeration.hasMoreElements()) {
Object element = enumeration.nextElement();
String asString = element.toString();
Object property = message.getObjectProperty(asString);
headers.put(asString, property);
}
return headers;
}
private Object getPayload(Message message) throws JMSException {
if (message == null) {
return null;
}
else if (message instanceof TextMessage) {
return ((TextMessage) message).getText();
}
else if (message instanceof StreamMessage) {
return ((StreamMessage) message).readObject();
}
else if (message instanceof ObjectMessage) {
return ((ObjectMessage) message).getObject();
}
return message.getBody(Object.class);
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2013-2019 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.jms;
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.jms.BytesMessage;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;
class JmsStubMessages implements MessageVerifier<Message> {
private final JmsTemplate jmsTemplate;
JmsStubMessages(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
@Override
public void send(Message message, String destination) {
jmsTemplate.convertAndSend(destination, message, new ReplyToProcessor());
}
@Override
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
jmsTemplate.setReceiveTimeout(timeUnit.toMillis(timeout));
return jmsTemplate.receive(destination);
}
@Override
public Message receive(String destination) {
return receive(destination, 5, TimeUnit.SECONDS);
}
@Override
public void send(Object payload, Map headers, String destination) {
jmsTemplate.send(destination, session -> {
Message message = createMessage(session, payload);
setHeaders(message, headers);
return message;
});
}
private Message createMessage(Session session, Object payload) throws JMSException {
if (payload instanceof String) {
return session.createTextMessage((String) payload);
}
else if (payload instanceof byte[]) {
BytesMessage bytesMessage = session.createBytesMessage();
bytesMessage.writeBytes((byte[]) payload);
return bytesMessage;
}
else if (payload instanceof Serializable) {
return session.createObjectMessage((Serializable) payload);
}
return session.createMessage();
}
private void setHeaders(Message message, Map<String, Object> headers) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
try {
if (value instanceof String) {
message.setStringProperty(key, (String) value);
}
else if (value instanceof Boolean) {
message.setBooleanProperty(key, (Boolean) value);
}
else {
message.setObjectProperty(key, value);
}
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
}
}
class ReplyToProcessor implements MessagePostProcessor {
@Override
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("requiresReply", "no");
return message;
}
}

View File

@@ -5,4 +5,5 @@ org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifi
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.noop.NoOpContractVerifierAutoConfiguration

View File

@@ -49,6 +49,7 @@
<module>samples-messaging-stream</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>spring-cloud-contract-stub-runner-boot-zookeeper</module>
@@ -56,6 +57,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>
</modules>
</profile>
<profile>
@@ -70,6 +72,7 @@
<module>samples-messaging-stream</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>spring-cloud-contract-stub-runner-boot-zookeeper</module>
@@ -79,6 +82,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>
</modules>
</profile>
</profiles>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<beansProjectDescription>
<version>1</version>
<pluginVersion><![CDATA[3.8.0.201606301005-RELEASE]]></pluginVersion>
<configSuffixes>
<configSuffix><![CDATA[xml]]></configSuffix>
</configSuffixes>
<enableImports><![CDATA[false]]></enableImports>
<configs>
<config>java:com.example.fraud.CamelMessagingApplication</config>
</configs>
<autoconfigs>
</autoconfigs>
<configSets>
</configSets>
</beansProjectDescription>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tests</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-sample-jms</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Sample Jms</name>
<description>Spring Cloud Contract Sample Jms</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</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>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2019 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 com.example;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@SuppressWarnings("serial")
public class BookDeleted implements Serializable {
public final String bookName;
@JsonCreator
public BookDeleted(@JsonProperty("bookName") String bookName) {
this.bookName = bookName;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2013-2019 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 com.example;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.jms.JMSException;
import javax.jms.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
@Component
public class BookDeleter {
private static final Logger log = LoggerFactory.getLogger(BookDeleter.class);
private AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false);
/**
* Scenario for "should generate tests triggered by a message": client side: if sends
* a message to input.messageFrom then message will be sent to output.messageFrom
* server side: will send a message to input, verify the message contents and await
* upon receiving message on the output messageFrom
*/
@JmsListener(destination = "delete")
public void bookDeleted(Message message) throws JMSException {
log.info("Deleting book " + message);
this.bookSuccessfulyDeleted.set(true);
log.info("Book successfuly deleted [" + this.bookSuccessfulyDeleted + "]");
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2019 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 com.example;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@SuppressWarnings("serial")
public class BookReturned implements Serializable {
public final String bookName;
@JsonCreator
BookReturned(@JsonProperty("bookName") String bookName) {
this.bookName = bookName;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2013-2019 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 com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private static final Logger log = LoggerFactory.getLogger(BookService.class);
private final JmsTemplate jmsTemplate;
public BookService(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
/**
* Scenario for "should generate tests triggered by a method": client side: must have
* a possibility to "trigger" sending of a message to the given messageFrom server
* side: will run the method and await upon receiving message on the output
* messageFrom. Method triggers sending a message to a source
*/
@JmsListener(destination = "input2")
public void returnBook() {
BookReturned bookReturned = new BookReturned("foo");
jmsTemplate.convertAndSend("output2", "{\"bookName\":\"foo\"}", message -> {
message.setStringProperty("BOOK-NAME", bookReturned.bookName);
return message;
});
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2019 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 com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.EnableJms;
@SpringBootApplication
@EnableJms
public class JmsMessagingApplication {
public static void main(String[] args) {
SpringApplication.run(JmsMessagingApplication.class, args);
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2013-2019 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 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 org.junit.BeforeClass
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootContextLoader
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.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.jms.core.JmsTemplate
import org.springframework.jms.core.MessagePostProcessor
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
/**
* SPIKE ON TESTS FROM NOTES IN MessagingSpec
*/
// Context configuration would end up in base class
@ContextConfiguration(classes = [JmsMessagingApplication], loader = SpringBootContextLoader)
@AutoConfigureMessageVerifier
class JmsMessagingApplicationSpec extends Specification {
// ALL CASES
@Autowired
JmsTemplate jmsTemplate
@Autowired
BookDeleter bookDeleter
@Inject MessageVerifier<Message> messageVerifier
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
@BeforeClass
static void init() {
System.setProperty("org.apache.activemq.SERIALIZABLE_PACKAGES", "*")
}
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')
}
@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')
}
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()
}
void bookReturnedTriggered() {
jmsTemplate.convertAndSend("output", '''{"bookName" : "foo" }''', new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("BOOK-NAME", "foo")
return message
}
})
}
PollingConditions pollingConditions = new PollingConditions()
void bookWasDeleted() {
pollingConditions.eventually {
assert bookDeleter.bookSuccessfulyDeleted.get()
}
}
}

View File

@@ -0,0 +1,2 @@
activemq:
broker-url: vm://embedded-broker?broker.persistent=false

View File

@@ -76,6 +76,16 @@ class CamelStubRunnerSpec extends Specification {
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[]
@@ -175,16 +185,6 @@ class CamelStubRunnerSpec extends Specification {
noExceptionThrown()
}
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
}
private boolean assertThatBodyContainsBookNameFoo(Object payload) {
String objectAsString = payload instanceof String ? payload :
JsonOutput.toJson(payload)

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tests</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-stub-runner-jms</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Stub Runner JMS</name>
<description>Spring Cloud Contract Stub Runner JMS</description>
<dependencies>
<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.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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.solidsoft.spock</groupId>
<artifactId>spock-global-unroll</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2019 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.jms
import com.fasterxml.jackson.annotation.JsonCreator
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
@CompileStatic
@EqualsAndHashCode
class BookReturned implements Serializable {
final String bookName
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
BookReturned(String bookName) {
this.bookName = bookName
}
}

View File

@@ -0,0 +1,274 @@
/*
* Copyright 2013-2019 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.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 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.jms.annotation.EnableJms
import org.springframework.jms.core.JmsTemplate
import org.springframework.jms.core.MessagePostProcessor
import org.springframework.test.context.ContextConfiguration
/**
* @author Marcin Grzejszczak
*/
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(properties = ["debug=true"])
@AutoConfigureStubRunner
@IgnoreIf({ os.windows })
class JmsStubRunnerSpec extends Specification {
@Autowired
StubFinder stubFinder
@Autowired
JmsTemplate jmsTemplate
def 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'() {
when:
// tag::client_send[]
jmsTemplate.
convertAndSend('input', new BookReturned('foo'), new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("sample", "header")
return message
}
})
// end::client_send[]
then:
// tag::client_receive[]
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
// end::client_receive[]
and:
// tag::client_receive_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('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[]
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
// end::client_trigger_receive[]
and:
// tag::client_trigger_message[]
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
// end::client_trigger_message[]
}
def 'should trigger a label for the existing groupId:artifactId'() {
when:
// tag::trigger_group_artifact[]
stubFinder.
trigger('my:stubs', 'return_book_1')
// end::trigger_group_artifact[]
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
}
def 'should trigger a label for the existing artifactId'() {
when:
// tag::trigger_artifact[]
stubFinder.trigger('stubs', 'return_book_1')
// end::trigger_artifact[]
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('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:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
}
def 'should trigger a label with no output message'() {
when:
// tag::trigger_no_output[]
jmsTemplate.
convertAndSend('delete', new BookReturned('foo'), new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("sample", "header")
return message
}
})
// end::trigger_no_output[]
then:
noExceptionThrown()
}
def 'should not trigger a message that does not match input'() {
when:
jmsTemplate.
convertAndSend('input', new BookReturned('notmatching'), new MessagePostProcessor() {
@Override
Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("wrong", "header")
return message
}
})
then:
TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
and:
receivedMessage == null
}
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
@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 =
// tag::sample_dsl[]
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('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('input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('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('delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// end::sample_dsl_3[]
}

View File

@@ -0,0 +1,14 @@
stubrunner:
repository-root: stubs:classpath:/stubs/
ids: my:stubs
stubs-mode: remote
spring:
activemq:
send-timeout: 1000
jms:
template:
receive-timeout: 1000
server:
port: 0
debug: true
logging.level.org.springframework.cloud.contract: debug

View File

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

View File

@@ -0,0 +1,13 @@
org.springframework.cloud.contract.spec.Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}

View File

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