Added integration for JMS
fixes gh-1141
This commit is contained in:
@@ -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]
|
||||
----
|
||||
====
|
||||
@@ -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>
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,\
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
tests/samples-messaging-jms/.springBeans
Normal file
16
tests/samples-messaging-jms/.springBeans
Normal 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>
|
||||
45
tests/samples-messaging-jms/pom.xml
Normal file
45
tests/samples-messaging-jms/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
activemq:
|
||||
broker-url: vm://embedded-broker?broker.persistent=false
|
||||
@@ -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)
|
||||
|
||||
77
tests/spring-cloud-contract-stub-runner-jms/pom.xml
Normal file
77
tests/spring-cloud-contract-stub-runner-jms/pom.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()')
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user