From c7b4f0125bcb11e34e4119be136495aba7de4ff0 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 9 Sep 2019 14:22:02 +0200 Subject: [PATCH] Added integration for JMS fixes gh-1141 --- .../asciidoc/_project-features-messaging.adoc | 138 +++++++++ spring-cloud-contract-stub-runner/pom.xml | 5 + .../stubrunner/StubRunnerExecutor.java | 1 + .../messaging/jms/StubRunnerJmsAccessor.java | 84 ++++++ .../jms/StubRunnerJmsConfiguration.java | 130 +++++++++ .../jms/StubRunnerJmsMessageSelector.java | 250 ++++++++++++++++ .../messaging/jms/StubRunnerJmsRouter.java | 78 +++++ .../jms/StubRunnerJmsTransformer.java | 110 +++++++ .../main/resources/META-INF/spring.factories | 1 + spring-cloud-contract-verifier/pom.xml | 10 + .../messaging/camel/CamelStubMessages.java | 2 - .../ContractVerifierCamelConfiguration.java | 7 +- .../jms/ContractVerifierJmsConfiguration.java | 123 ++++++++ .../messaging/jms/JmsStubMessages.java | 111 +++++++ .../main/resources/META-INF/spring.factories | 1 + tests/pom.xml | 4 + tests/samples-messaging-jms/.springBeans | 16 + tests/samples-messaging-jms/pom.xml | 45 +++ .../main/java/com/example/BookDeleted.java | 34 +++ .../main/java/com/example/BookDeleter.java | 50 ++++ .../main/java/com/example/BookReturned.java | 34 +++ .../main/java/com/example/BookService.java | 52 ++++ .../com/example/JmsMessagingApplication.java | 31 ++ .../JmsMessagingApplicationSpec.groovy | 173 +++++++++++ .../src/test/resources/application.yml | 2 + .../camel/CamelStubRunnerSpec.groovy | 20 +- .../pom.xml | 77 +++++ .../messaging/jms/BookReturned.groovy | 32 ++ .../messaging/jms/JmsStubRunnerSpec.groovy | 274 ++++++++++++++++++ .../src/test/resources/application.yml | 14 + .../test/resources/stubs/bookDeleted.groovy | 13 + .../test/resources/stubs/bookReturned1.groovy | 13 + .../test/resources/stubs/bookReturned2.groovy | 21 ++ 33 files changed, 1941 insertions(+), 15 deletions(-) create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java create mode 100644 tests/samples-messaging-jms/.springBeans create mode 100644 tests/samples-messaging-jms/pom.xml create mode 100644 tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java create mode 100644 tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java create mode 100644 tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java create mode 100644 tests/samples-messaging-jms/src/main/java/com/example/BookService.java create mode 100644 tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java create mode 100644 tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy create mode 100644 tests/samples-messaging-jms/src/test/resources/application.yml create mode 100644 tests/spring-cloud-contract-stub-runner-jms/pom.xml create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy diff --git a/docs/src/main/asciidoc/_project-features-messaging.adoc b/docs/src/main/asciidoc/_project-features-messaging.adoc index 29df58dcee..4c691a58ee 100644 --- a/docs/src/main/asciidoc/_project-features-messaging.adoc +++ b/docs/src/main/asciidoc/_project-features-messaging.adoc @@ -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] +---- +==== \ No newline at end of file diff --git a/spring-cloud-contract-stub-runner/pom.xml b/spring-cloud-contract-stub-runner/pom.xml index 644290ee86..f4b26eaa55 100644 --- a/spring-cloud-contract-stub-runner/pom.xml +++ b/spring-cloud-contract-stub-runner/pom.xml @@ -65,6 +65,11 @@ jopt-simple true + + org.springframework.boot + spring-boot-starter-activemq + true + org.apache.camel camel-spring-boot-starter diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java index a999b669ec..1976050127 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java @@ -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())), diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java new file mode 100644 index 0000000000..265415bb55 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java @@ -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 getHeaders(Message message) { + try { + return headers(message); + } + catch (JMSException ex) { + throw new IllegalStateException(ex); + } + } + + private static Map headers(Message message) throws JMSException { + Map 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); + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java new file mode 100644 index 0000000000..7a6b8aae62 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java @@ -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> contracts = batchStubRunner + .getContracts(); + for (Entry> entry : contracts + .entrySet()) { + StubConfiguration key = entry.getKey(); + Collection value = entry.getValue(); + String name = key.getGroupId() + "_" + key.getArtifactId(); + MultiValueMap 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> entries : map.entrySet()) { + List 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 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 { + + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java new file mode 100644 index 0000000000..90b7eb38a2 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java @@ -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 CACHE = Collections + .synchronizedMap(new WeakHashMap<>()); + + private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class); + + private final List groovyDsls; + + private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper(); + + StubRunnerJmsMessageSelector(List 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 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 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 unmatchedJsonPath, + DocumentContext parsedJson, String jsonPath) { + try { + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath); + return true; + } + catch (Exception e) { + unmatchedJsonPath.add(e.getLocalizedMessage()); + return false; + } + } + + private List headersMatch(Message message, Contract groovyDsl) { + List unmatchedHeaders = new ArrayList<>(); + Map 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 + "]"; + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java new file mode 100644 index 0000000000..62839a3b7e --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java @@ -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 contracts; + + private JmsTemplate jmsTemplate; + + StubRunnerJmsRouter(List 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; + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java new file mode 100644 index 0000000000..0b5d63d143 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java @@ -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 groovyDsls) { + this.selector = new StubRunnerJmsMessageSelector(groovyDsls); + } + + public Message transform(Session session, Contract groovyDsl) { + Object outputBody = outputBody(groovyDsl); + Map 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 headers) { + for (Map.Entry 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); + } + } + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories index f0d56cf795..6b0934fcd7 100644 --- a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories @@ -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,\ diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index db66f6afca..1ca7d39637 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -31,6 +31,16 @@ spring-messaging true + + org.springframework + spring-jms + true + + + jakarta.jms + jakarta.jms-api + true + org.apache.camel camel-spring-boot-starter diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java index 77346e64aa..ac5dbe58c0 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java @@ -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 { private static final Logger log = LoggerFactory.getLogger(CamelStubMessages.class); diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java index 3563ca2dda..670db3ea6f 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java @@ -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 diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java new file mode 100644 index 0000000000..d56ea8cf9d --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java @@ -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 contractVerifierJmsMessageExchange( + ObjectProvider jmsTemplateProvider) { + JmsTemplate jmsTemplate = jmsTemplateProvider.getIfAvailable(JmsTemplate::new); + return new JmsStubMessages(jmsTemplate); + } + + @Bean + @ConditionalOnMissingBean + public ContractVerifierMessaging contractVerifierJmsMessaging( + MessageVerifier exchange) { + return new ContractVerifierJmsHelper(exchange); + } + +} + +class ContractVerifierJmsHelper extends ContractVerifierMessaging { + + private static final Log log = LogFactory.getLog(ContractVerifierJmsHelper.class); + + ContractVerifierJmsHelper(MessageVerifier exchange) { + super(exchange); + } + + @Override + protected ContractVerifierMessage convert(Message message) { + try { + Map 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 headers(Message message) throws JMSException { + Map 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); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java new file mode 100644 index 0000000000..6bfbcf7811 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java @@ -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 { + + 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 headers) { + for (Map.Entry 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; + } + +} diff --git a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories index 99c7544594..275d99c1ae 100644 --- a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/tests/pom.xml b/tests/pom.xml index d703e19874..b4442fc187 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -49,6 +49,7 @@ samples-messaging-stream samples-messaging-integration samples-messaging-amqp + samples-messaging-jms spring-cloud-contract-stub-runner-camel spring-cloud-contract-stub-runner-boot-eureka spring-cloud-contract-stub-runner-boot-zookeeper @@ -56,6 +57,7 @@ spring-cloud-contract-stub-runner-integration spring-cloud-contract-stub-runner-stream spring-cloud-contract-stub-runner-amqp + spring-cloud-contract-stub-runner-jms @@ -70,6 +72,7 @@ samples-messaging-stream samples-messaging-integration samples-messaging-amqp + samples-messaging-jms spring-cloud-contract-stub-runner-camel spring-cloud-contract-stub-runner-boot-eureka spring-cloud-contract-stub-runner-boot-zookeeper @@ -79,6 +82,7 @@ spring-cloud-contract-stub-runner-integration spring-cloud-contract-stub-runner-stream spring-cloud-contract-stub-runner-amqp + spring-cloud-contract-stub-runner-jms diff --git a/tests/samples-messaging-jms/.springBeans b/tests/samples-messaging-jms/.springBeans new file mode 100644 index 0000000000..b74cfc29b8 --- /dev/null +++ b/tests/samples-messaging-jms/.springBeans @@ -0,0 +1,16 @@ + + + 1 + + + + + + + java:com.example.fraud.CamelMessagingApplication + + + + + + diff --git a/tests/samples-messaging-jms/pom.xml b/tests/samples-messaging-jms/pom.xml new file mode 100644 index 0000000000..b4a1a8e5e3 --- /dev/null +++ b/tests/samples-messaging-jms/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 2.2.0.BUILD-SNAPSHOT + .. + + spring-cloud-contract-sample-jms + jar + Spring Cloud Contract Sample Jms + Spring Cloud Contract Sample Jms + + + org.springframework.boot + spring-boot-starter-activemq + + + org.springframework.cloud + spring-cloud-contract-verifier + test + + + org.spockframework + spock-spring + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java new file mode 100644 index 0000000000..118dd4fd39 --- /dev/null +++ b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java @@ -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; + } + +} diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java new file mode 100644 index 0000000000..32b0877459 --- /dev/null +++ b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java @@ -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 + "]"); + } + +} diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java b/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java new file mode 100644 index 0000000000..81fc0a993e --- /dev/null +++ b/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java @@ -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; + } + +} diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookService.java b/tests/samples-messaging-jms/src/main/java/com/example/BookService.java new file mode 100644 index 0000000000..154cf489b7 --- /dev/null +++ b/tests/samples-messaging-jms/src/main/java/com/example/BookService.java @@ -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; + }); + } + +} diff --git a/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java b/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java new file mode 100644 index 0000000000..d005e41c56 --- /dev/null +++ b/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java @@ -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); + } + +} diff --git a/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy b/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..df67ff15d8 --- /dev/null +++ b/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy @@ -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 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() + } + } + +} diff --git a/tests/samples-messaging-jms/src/test/resources/application.yml b/tests/samples-messaging-jms/src/test/resources/application.yml new file mode 100644 index 0000000000..a26da67063 --- /dev/null +++ b/tests/samples-messaging-jms/src/test/resources/application.yml @@ -0,0 +1,2 @@ +activemq: + broker-url: vm://embedded-broker?broker.persistent=false \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy index 0f28c83194..0d456362f6 100644 --- a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy @@ -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) diff --git a/tests/spring-cloud-contract-stub-runner-jms/pom.xml b/tests/spring-cloud-contract-stub-runner-jms/pom.xml new file mode 100644 index 0000000000..12d01106b3 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 2.2.0.BUILD-SNAPSHOT + .. + + spring-cloud-contract-stub-runner-jms + jar + Spring Cloud Contract Stub Runner JMS + Spring Cloud Contract Stub Runner JMS + + + org.springframework.cloud + spring-cloud-contract-stub-runner + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner-jetty + test + + + org.springframework.boot + spring-boot-starter-activemq + + + junit + junit + test + + + org.codehaus.groovy + groovy + + + org.spockframework + spock-core + test + + + org.spockframework + spock-spring + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + test + + + info.solidsoft.spock + spock-global-unroll + test + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy new file mode 100644 index 0000000000..ba9c1d406f --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy @@ -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 + } +} diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy new file mode 100644 index 0000000000..662b4272a6 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy @@ -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[] +} diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml new file mode 100644 index 0000000000..2782cdc1a5 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml @@ -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 \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy new file mode 100644 index 0000000000..b013bba223 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy @@ -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()') + } +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy new file mode 100644 index 0000000000..0094bc7b1d --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy @@ -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') + } + } +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy new file mode 100644 index 0000000000..e0d53098d5 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy @@ -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') + } + } +} \ No newline at end of file