diff --git a/README.adoc b/README.adoc index fc263f9606..16d4f9b610 100644 --- a/README.adoc +++ b/README.adoc @@ -305,7 +305,7 @@ with __Contract Definition Language__ (DSL). Contract definitions are used to pr * JSON stub definitions to be used by WireMock when doing integration testing on the client code (__client tests__). Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier. -* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to +* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream, Spring AMQP and Apache Camel. You can however set your own integrations if you want to * Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). Full test is generated by Spring Cloud Contract Verifier. diff --git a/docs/src/main/asciidoc/verifier/introduction.adoc b/docs/src/main/asciidoc/verifier/introduction.adoc index babec72446..60ae5a6b6e 100644 --- a/docs/src/main/asciidoc/verifier/introduction.adoc +++ b/docs/src/main/asciidoc/verifier/introduction.adoc @@ -11,7 +11,7 @@ with __Contract Definition Language__ (DSL). Contract definitions are used to pr * JSON stub definitions to be used by WireMock when doing integration testing on the client code (__client tests__). Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier. -* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to +* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream, Spring AMQP and Apache Camel. You can however set your own integrations if you want to * Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). Full test is generated by Spring Cloud Contract Verifier. diff --git a/docs/src/main/asciidoc/verifier/messaging.adoc b/docs/src/main/asciidoc/verifier/messaging.adoc index e51b4a1d18..19ed1da0ae 100644 --- a/docs/src/main/asciidoc/verifier/messaging.adoc +++ b/docs/src/main/asciidoc/verifier/messaging.adoc @@ -5,11 +5,12 @@ All of our integrations are working with Spring but you can also create one your ==== Integrations -You can use one of the three integration configurations: +You can use one of the four integration configurations: - Apache Camel - Spring Integration - Spring Cloud Stream +- Spring AMQP Since we're using Spring Boot then if you have added one of the aforementioned libraries to the classpath then automatically all the messaging configuration will be set up. diff --git a/docs/src/main/asciidoc/verifier/stubrunner_msg.adoc b/docs/src/main/asciidoc/verifier/stubrunner_msg.adoc index 4202b35b3c..776bdff84c 100644 --- a/docs/src/main/asciidoc/verifier/stubrunner_msg.adoc +++ b/docs/src/main/asciidoc/verifier/stubrunner_msg.adoc @@ -5,6 +5,7 @@ Stub Runner has the functionality to run the published stubs in memory. It can i - Spring Integration - Spring Cloud Stream - Apache Camel +- Spring AMQP It also provides points of entry to integrate with any other solution on the market. @@ -53,4 +54,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-camel/README.adoc[] include::{tests_path}/spring-cloud-contract-stub-runner-integration/README.adoc[] -include::{tests_path}/spring-cloud-contract-stub-runner-stream/README.adoc[] \ No newline at end of file +include::{tests_path}/spring-cloud-contract-stub-runner-stream/README.adoc[] + +include::{tests_path}/spring-cloud-contract-stub-runner-amqp/README.adoc[] \ No newline at end of file diff --git a/pom.xml b/pom.xml index c7c962e46d..97c67f4642 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,16 @@ spock-global-unroll 0.5.0 + + org.springframework.amqp + spring-rabbit + 1.6.2.RELEASE + + + org.mockito + mockito-core + 1.10.19 + org.springframework.cloud spring-cloud-contract-dependencies diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index a879c0c37d..8c97973a73 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -36,6 +36,11 @@ spring-cloud-stream-test-support true + + org.springframework.amqp + spring-rabbit + true + javax.inject javax.inject @@ -90,7 +95,6 @@ org.springframework.boot spring-boot-starter-test - test org.slf4j diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierAmqpAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierAmqpAutoConfiguration.java new file mode 100644 index 0000000000..1e05c32cb6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierAmqpAutoConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.amqp; + +import org.mockito.Mockito; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import org.springframework.amqp.support.SimpleAmqpHeaderMapper; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.MessagingMessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; +import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration; +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging; +import org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration setting up {@link MessageVerifier} for use with plain spring-rabbit/spring-amqp + * + * @author Mathias Düsterhöft + * @since 1.0.2 + */ +@Configuration +@ConditionalOnClass({Message.class, RabbitTemplate.class, Mockito.class}) +@ConditionalOnMissingClass("org.springframework.integration.core.MessageSource") +@AutoConfigureBefore(ContractVerifierIntegrationConfiguration.class) +@AutoConfigureAfter(ContractVerifierStreamAutoConfiguration.class) +public class ContractVerifierAmqpAutoConfiguration { + + @SpyBean + private RabbitTemplate rabbitTemplate; + + @Autowired(required = false) + private MessageListenerAdapter messageListenerAdapter; + + @Bean + @ConditionalOnMissingBean + public MessageVerifier contractVerifierMessageExchange() { + return new SpringAmqpStubMessages(this.rabbitTemplate, this.messageListenerAdapter); + } + + @Bean + @ConditionalOnMissingBean + public ContractVerifierMessaging contractVerifierMessaging( + MessageVerifier exchange) { + return new ContractVerifierHelper(exchange, this.rabbitTemplate.getMessageConverter()); + } +} + +class ContractVerifierHelper extends ContractVerifierMessaging { + + private final MessageConverter messageConverter; + + public ContractVerifierHelper(MessageVerifier exchange, MessageConverter messageConverter) { + super(exchange); + this.messageConverter = messageConverter; + } + + @Override + protected ContractVerifierMessage convert(Message message) { + MessagingMessageConverter messageConverter = new MessagingMessageConverter(this.messageConverter, new SimpleAmqpHeaderMapper()); + org.springframework.messaging.Message messagingMessage = (org.springframework.messaging.Message) messageConverter.fromMessage(message); + return new ContractVerifierMessage(messagingMessage.getPayload(), messagingMessage.getHeaders()); + } +} + diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/RabbitMockConnectionFactoryAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/RabbitMockConnectionFactoryAutoConfiguration.java new file mode 100644 index 0000000000..9f33d5b49d --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/RabbitMockConnectionFactoryAutoConfiguration.java @@ -0,0 +1,52 @@ +package org.springframework.cloud.contract.verifier.messaging.amqp; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ExecutorService; + +import org.mockito.Mockito; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; + +/** + * Spring rabbit test utility that provides a mock ConnectionFactory to avoid having to connect against a running broker. + * + * Set verifier.amqp.mockConnection=true to enable the mocked ConnectionFactory + * + * @author Mathias Düsterhöft + * @since 1.0.2 + */ +@Configuration +@ConditionalOnClass({Message.class, RabbitTemplate.class, Mockito.class}) +@ConditionalOnMissingClass("org.springframework.integration.core.MessageSource") +@AutoConfigureAfter(ContractVerifierAmqpAutoConfiguration.class) +@ConditionalOnProperty(value = "verifier.amqp.mockConnection", havingValue = "true", matchIfMissing = true) +public class RabbitMockConnectionFactoryAutoConfiguration { + + @Bean + public ConnectionFactory connectionFactory() { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); + Connection mockConnection = mock(Connection.class); + Channel mockChannel = mock(Channel.class); + try { + when(mockConnectionFactory.newConnection((ExecutorService) null)).thenReturn(mockConnection); + when(mockConnection.isOpen()).thenReturn(true); + when(mockConnection.createChannel()).thenReturn(mockChannel); + } catch (Exception e) { + throw new RuntimeException(e); + } + return new CachingConnectionFactory(mockConnectionFactory); + } +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessages.java new file mode 100644 index 0000000000..9329f631cc --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessages.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.amqp; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.verify; +import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.mockito.ArgumentCaptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import org.springframework.amqp.rabbit.support.CorrelationData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; +import org.springframework.util.Assert; + +/** + * {@link MessageVerifier} implementation to integrate with plain spring-amqp/spring-rabbit. + * It is meant to be used without interacting with a running bus. + * + * It relies on the RabbitTemplate to be a spy to be able to capture send messages. + * + * Messages are not sent to the bus - but are handed over to the {@link MessageListenerAdapter} which + * allows us to test the full deserialization and listener invocation. + * + * @author Mathias Düsterhöft + * @since 1.0.2 + */ +public class SpringAmqpStubMessages implements + MessageVerifier { + + private static final Logger log = LoggerFactory.getLogger(SpringAmqpStubMessages.class); + + private final RabbitTemplate rabbitTemplate; + + private final MessageListenerAdapter messageListenerAdapter; + + @Autowired + public SpringAmqpStubMessages(RabbitTemplate rabbitTemplate, MessageListenerAdapter messageListenerAdapter) { + Assert.notNull(rabbitTemplate); + Assert.isTrue(mockingDetails(rabbitTemplate).isSpy() || mockingDetails(rabbitTemplate).isMock()); //we get send messages by capturing arguments on the spy + this.rabbitTemplate = rabbitTemplate; + this.messageListenerAdapter = messageListenerAdapter; + } + + @Override + public void send(T payload, Map headers, String destination) { + Message message = org.springframework.amqp.core.MessageBuilder + .withBody(((String) payload).getBytes()) + .andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType((String) headers.get("contentType")) + .copyHeaders(headers).build()) + .build(); + if (headers.containsKey(DEFAULT_CLASSID_FIELD_NAME)) { + message.getMessageProperties().setHeader(DEFAULT_CLASSID_FIELD_NAME, headers.get(DEFAULT_CLASSID_FIELD_NAME)); + } + send(message, destination); + } + + @Override + public void send(Message message, String destination) { + if (this.messageListenerAdapter == null) { + throw new IllegalStateException("no MessageListenerAdapter wired - cannot send message"); + } + this.messageListenerAdapter.onMessage(message); + } + + @Override + public Message receive(String destination, long timeout, TimeUnit timeUnit) { + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(this.rabbitTemplate).send(eq(destination), anyString(), messageCaptor.capture(), any(CorrelationData.class)); + + if (messageCaptor.getAllValues().isEmpty()) { + log.info("no messages found on destination {}", destination); + return null; + } else if (messageCaptor.getAllValues().size() > 1) { + log.info("multiple messages found on destination {} returning last one - {}", destination); + return messageCaptor.getValue(); + } + return messageCaptor.getValue(); + } + + @Override + public Message receive(String destination) { + return receive(destination, 5, TimeUnit.SECONDS); + } + +} 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 0d1daef226..0e6be1c75d 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 @@ -2,5 +2,7 @@ org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier=\ org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration,\ org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration,\ +org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration,\ +org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration,\ org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration,\ org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierHelperSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierHelperSpec.groovy new file mode 100644 index 0000000000..6665f1e503 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/ContractVerifierHelperSpec.groovy @@ -0,0 +1,33 @@ +package org.springframework.cloud.contract.verifier.messaging.amqp + +import org.springframework.amqp.core.Message +import org.springframework.amqp.core.MessageBuilder +import org.springframework.amqp.core.MessagePropertiesBuilder +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage +import spock.lang.Specification + +import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON +/** + * @author Mathias Düsterhöft + */ +class ContractVerifierHelperSpec extends Specification { + + def "should convert message"() { + given: + String payload = '''{"name":"some"}''' + Message message = MessageBuilder + .withBody(payload.bytes) + .andProperties(MessagePropertiesBuilder.newInstance() + .setHeader("my-header", "some") + .setContentType(CONTENT_TYPE_JSON) + .build()).build() + ContractVerifierHelper contractVerifierHelper = new ContractVerifierHelper(null, new Jackson2JsonMessageConverter()) + when: + ContractVerifierMessage contractVerifierMessage = contractVerifierHelper.convert(message) + then: + ((Map) contractVerifierMessage.payload).containsKey("name") + contractVerifierMessage.headers.containsKey("contentType") + contractVerifierMessage.headers.containsKey("my-header") + } +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy new file mode 100644 index 0000000000..776c464bc3 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy @@ -0,0 +1,39 @@ +package org.springframework.cloud.contract.verifier.messaging.amqp + +import com.google.common.collect.ImmutableMap +import org.mockito.ArgumentCaptor +import org.springframework.amqp.core.Message +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter +import spock.lang.Specification + +import static org.mockito.Mockito.mock +import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON +import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME +/** + * @author Mathias Düsterhöft + */ +class SpringAmqpStubMessagesSpec extends Specification { + + RabbitTemplate rabbitTemplate = mock(RabbitTemplate.class) + MessageListenerAdapter messageListenerAdapter = Mock(MessageListenerAdapter.class) + + def "should send amqp message with type id"() { + given: + String payload = '''{"name":"some"}''' + ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class) + SpringAmqpStubMessages messageVerifier = new SpringAmqpStubMessages(rabbitTemplate, messageListenerAdapter) + when: + messageVerifier.send(payload, + ImmutableMap.builder() + .put(DEFAULT_CLASSID_FIELD_NAME, "org.example.Some") + .put("contentType", CONTENT_TYPE_JSON) + .build(), + "test-exchange") + then: + 1 * messageListenerAdapter.onMessage({ + it.getMessageProperties().getContentType() == CONTENT_TYPE_JSON && + it.getMessageProperties().getHeaders().get(DEFAULT_CLASSID_FIELD_NAME) == "org.example.Some" + }) + } +} diff --git a/tests/pom.xml b/tests/pom.xml index ef64f01973..da9baafb6e 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -22,12 +22,14 @@ samples-messaging-spring samples-messaging-stream samples-messaging-integration + samples-messaging-amqp spring-cloud-contract-stub-runner-boot-eureka spring-cloud-contract-stub-runner-boot-zookeeper spring-cloud-contract-stub-runner-camel spring-cloud-contract-stub-runner-context-path spring-cloud-contract-stub-runner-integration spring-cloud-contract-stub-runner-stream + spring-cloud-contract-stub-runner-amqp diff --git a/tests/samples-messaging-amqp/pom.xml b/tests/samples-messaging-amqp/pom.xml new file mode 100644 index 0000000000..44efa49dce --- /dev/null +++ b/tests/samples-messaging-amqp/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 1.0.1.BUILD-SNAPSHOT + .. + + spring-cloud-contract-sample-amqp + jar + Spring Cloud Contract Sample Spring Amqp + Spring Cloud Contract Sample Spring Amqp Rabbit + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-amqp + + + 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-amqp/src/main/java/com/example/AmqpMessagingApplication.java b/tests/samples-messaging-amqp/src/main/java/com/example/AmqpMessagingApplication.java new file mode 100644 index 0000000000..251f9f738d --- /dev/null +++ b/tests/samples-messaging-amqp/src/main/java/com/example/AmqpMessagingApplication.java @@ -0,0 +1,46 @@ +package com.example; + +import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON; + +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@SpringBootApplication +public class AmqpMessagingApplication { + + public static void main(String[] args) { + SpringApplication.run(AmqpMessagingApplication.class, args); + } + + @Bean + public MessageConverter messageConverter(ObjectMapper objectMapper) { + final Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter(); + jsonMessageConverter.setJsonObjectMapper(objectMapper); + jsonMessageConverter.setCreateMessageIds(true); + final ContentTypeDelegatingMessageConverter messageConverter = new ContentTypeDelegatingMessageConverter(jsonMessageConverter); + messageConverter.addDelegate(CONTENT_TYPE_JSON, jsonMessageConverter); + return messageConverter; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(messageConverter); + return rabbitTemplate; + } + + @Bean + public Exchange testExchange() { + return new TopicExchange("test-exchange"); + } +} diff --git a/tests/samples-messaging-amqp/src/main/java/com/example/Book.java b/tests/samples-messaging-amqp/src/main/java/com/example/Book.java new file mode 100644 index 0000000000..5e0832cb90 --- /dev/null +++ b/tests/samples-messaging-amqp/src/main/java/com/example/Book.java @@ -0,0 +1,17 @@ +package com.example; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class Book { + + private String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Book(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/tests/samples-messaging-amqp/src/main/java/com/example/MessagePublisher.java b/tests/samples-messaging-amqp/src/main/java/com/example/MessagePublisher.java new file mode 100644 index 0000000000..36890870a3 --- /dev/null +++ b/tests/samples-messaging-amqp/src/main/java/com/example/MessagePublisher.java @@ -0,0 +1,23 @@ +package com.example; + +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MessagePublisher { + + private final RabbitTemplate rabbitTemplate; + + private final Exchange exchange; + @Autowired + public MessagePublisher(RabbitTemplate rabbitTemplate, Exchange exchange) { + this.rabbitTemplate = rabbitTemplate; + this.exchange = exchange; + } + + public void sendMessage(Book book) { + this.rabbitTemplate.convertAndSend(this.exchange.getName(), "routingkey", book); + } +} diff --git a/tests/samples-messaging-amqp/src/test/groovy/com/example/AmqpMessagingApplicationSpec.groovy b/tests/samples-messaging-amqp/src/test/groovy/com/example/AmqpMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..19f8ce482a --- /dev/null +++ b/tests/samples-messaging-amqp/src/test/groovy/com/example/AmqpMessagingApplicationSpec.groovy @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2016 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 + * + * http://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 com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +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.boot.AutoConfigureMessageVerifier +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import javax.inject.Inject +// Context configuration would end up in base class +@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader) +@DirtiesContext +@AutoConfigureMessageVerifier +public class AmqpMessagingApplicationSpec extends Specification { + + // ALL CASES + @Inject ContractVerifierMessaging contractVerifierMessaging + @Inject ContractVerifierObjectMapper contractVerifierObjectMapper + + def "should work for triggered based messaging"() { + given: + def dsl = Contract.make { + // Human readable description + description 'Some description' + // Label by means of which the output message can be triggered + label 'some_label' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('publishBook()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('test-exchange') + // the body of the output message + body('''{ "name" : "some" }''') + // the headers of the output message + headers { + header('contentType', 'application/json') + header('__TypeId__', 'com.example.Book') + } + } + } + // generated test should look like this: + when: + publishBook() + then: + def response = contractVerifierMessaging.receive('test-exchange') + response.headers.get('contentType') == 'application/json' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some') + } + + // BASE CLASS WOULD HAVE THIS: + + @Autowired MessagePublisher messagePublisher + + void publishBook() { + this.messagePublisher.sendMessage(new Book("some")) + } +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-amqp/README.adoc b/tests/spring-cloud-contract-stub-runner-amqp/README.adoc new file mode 100644 index 0000000000..deb3b59373 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/README.adoc @@ -0,0 +1,84 @@ +=== Stub Runner Spring AMQP + +Spring Cloud Contract Verifier Stub Runner's messaging module provides an easy way to integrate with Spring AMQP's Rabbit Template. +For the provided artifacts it will automatically download the stubs and register the required +routes. + +The integration tries to work standalone, that is without interaction with a running RabbitMQ message broker. It expects a `RabbitTemplate` on the application context and uses it as a spring boot test `@SpyBean`. +Thus it can use the mockito spy functionality to verify and introspect messages sent by the application. + +IMPORTANT: In the current status of the implementation tries to find a `MessageListenerAdapter` on the application context to send messages into the application, because we want to avoid interacting with a running bus. +Using annotation driven listener endpoints (e.g. `@RabbitListener` annotated listener methods) is currently not supported. + +==== Adding it to the project + +It's enough to have both Spring AMQP and Spring Cloud Contract Stub Runner on classpath. +Remember to annotate your test class with `@AutoConfigureMessageVerifier`. + +==== Examples + +===== Stubs structure + +Let us assume that we have the following Maven repository with a deployed stubs for the +`spring-cloud-contract-amqp-test` application. + +[source,bash,indent=0] +---- +└── .m2 + └── repository + └── com + └── example + └── spring-cloud-contract-amqp-test + ├── 0.4.0-SNAPSHOT + │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom + │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +---- + +And the stubs contain the following structure: + +[source,bash,indent=0] +---- +├── META-INF +│   └── MANIFEST.MF +└── contracts + └── shouldProduceValidPersonData.groovy +---- + +Let's consider the following contract: + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=amqp_contract,indent=0] +---- + + +and the following Spring configuration: + +[source,yaml] +---- +include::src/test/resources/application.yml[] +---- + +===== Triggering the message + +So to trigger a message using the contract above we'll use the `StubTrigger` interface as follows. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +===== Spring AMQP Test Configuration + +In order to avoid that Spring AMQP is trying to connect to a running broker during our tests we configure a mock `ConnectionFactory`. + +To disable the mocked ConnectionFactory set the property `verifier.amqp.mockConnection=false` + +[source,yaml] +---- +verifier: + amqp: + mockConnection: false +---- diff --git a/tests/spring-cloud-contract-stub-runner-amqp/pom.xml b/tests/spring-cloud-contract-stub-runner-amqp/pom.xml new file mode 100644 index 0000000000..c55173d089 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 1.0.1.BUILD-SNAPSHOT + .. + + spring-cloud-contract-stub-runner-amqp + jar + Spring Cloud Contract Stub Runner AMQP + Spring Cloud Contract Stub Runner AMQP + + 1.8 + + + + org.springframework.cloud + spring-cloud-contract-stub-runner + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner-jetty + test + + + org.springframework.cloud + spring-cloud-contract-verifier + + + org.springframework.amqp + spring-rabbit + + + junit + junit + test + + + org.spockframework + spock-core + test + + + org.spockframework + spock-spring + test + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework.boot + spring-boot-starter-web + test + + + info.solidsoft.spock + spock-global-unroll + test + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + testCompile + + + + + + + diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java new file mode 100644 index 0000000000..0ea154052b --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java @@ -0,0 +1,52 @@ +package org.springframework.cloud.contract.stubrunner.messaging.amqp; + +import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON; + +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@SpringBootApplication +public class AmqpMessagingApplication { + + public static void main(String[] args) { + SpringApplication.run(AmqpMessagingApplication.class, args); + } + + @Bean + public MessageConverter messageConverter(ObjectMapper objectMapper) { + final Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter(); + jsonMessageConverter.setJsonObjectMapper(objectMapper); + jsonMessageConverter.setCreateMessageIds(true); + final ContentTypeDelegatingMessageConverter messageConverter = new ContentTypeDelegatingMessageConverter(jsonMessageConverter); + messageConverter.addDelegate(CONTENT_TYPE_JSON, jsonMessageConverter); + return messageConverter; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(messageConverter); + return rabbitTemplate; + } + + @Bean + public Exchange testExchange() { + return new TopicExchange("test-exchange"); + } + + @Bean + public MessageListenerAdapter messageListenerAdapter(MessageSubscriber messageSubscriber, MessageConverter messageConverter) { + return new MessageListenerAdapter(messageSubscriber, messageConverter); + } +} diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/MessageSubscriber.java b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/MessageSubscriber.java new file mode 100644 index 0000000000..2722bde351 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/MessageSubscriber.java @@ -0,0 +1,11 @@ +package org.springframework.cloud.contract.stubrunner.messaging.amqp; + +import org.springframework.stereotype.Component; + +@Component +public class MessageSubscriber { + + public void handleMessage(Person person) { + + } +} diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/Person.java b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/Person.java new file mode 100644 index 0000000000..87f6286491 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/Person.java @@ -0,0 +1,23 @@ +package org.springframework.cloud.contract.stubrunner.messaging.amqp; + +public class Person { + + private Integer id; + private String name; + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy new file mode 100644 index 0000000000..b941ca4785 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy @@ -0,0 +1,67 @@ +package org.springframework.cloud.contract.stubrunner.messaging.amqp + +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubTrigger +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import static org.mockito.BDDMockito.then + +@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader) +@AutoConfigureStubRunner +class AmqpStubRunnerSpec extends Specification { + + @Autowired + StubTrigger stubTrigger + + @SpyBean + MessageSubscriber messageSubscriber + + @Captor + ArgumentCaptor personArgumentCaptor + + def "should trigger stub amqp message"() { + given: + // tag::amqp_contract[] + + Contract.make { + // Human readable description + description 'Should produce valid person data' + // Label by means of which the output message can be triggered + label 'contract-test.person.created.event' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createPerson()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'contract-test.exchange' + headers { + header('contentType': 'application/json') + header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person') + } + // the body of the output message + body ([ + id: $(consumer(9), producer(regex("[0-9]+"))), + name: "me" + ]) + } + } + // end::amqp_contract[] + when: + // tag::client_trigger[] + stubTrigger.trigger("contract-test.person.created.event") + // end::client_trigger[] + then: + then(messageSubscriber).should().handleMessage(personArgumentCaptor.capture()) + personArgumentCaptor.value.name != null + } +} diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/application.yml b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/application.yml new file mode 100644 index 0000000000..4a5df9496a --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/application.yml @@ -0,0 +1,2 @@ +stubrunner.repositoryRoot: classpath:m2repo/repository/ +stubrunner.ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/maven-metadata-local.xml b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..91c6038b31 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + org.springframework.cloud.contract.verifier.stubs.amqp + spring-cloud-contract-amqp-test + 0.4.0-SNAPSHOT + + + true + + 20161007155639 + + diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000..2fa845f66b Binary files /dev/null and b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar differ diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom new file mode 100644 index 0000000000..48e29044d6 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/0.4.0-SNAPSHOT/spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs.amqp + spring-cloud-contract-amqp-test + 0.4.0-SNAPSHOT + pom + diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata-local.xml b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata-local.xml new file mode 100644 index 0000000000..3df4f51931 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs.amqp + spring-cloud-contract-amqp-test + 0.4.0-SNAPSHOT + + + 0.4.0-SNAPSHOT + + 20160409062112 + + diff --git a/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata.xml b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata.xml new file mode 100644 index 0000000000..3df4f51931 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-amqp/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/amqp/spring-cloud-contract-amqp-test/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs.amqp + spring-cloud-contract-amqp-test + 0.4.0-SNAPSHOT + + + 0.4.0-SNAPSHOT + + 20160409062112 + +