From 36409ac400eb071b7096385db4223cfab40fe1eb Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 28 Jun 2017 11:33:53 +0200 Subject: [PATCH] Fixing the way queue binding key gets used for rabbit listener without this change we're very strict in terms of the number of called send methods on the rabbittemplate with this change we accept that the method could have been called more than once fixes #332 --- README.adoc | 8 +- .../JUnitMessagingMethodBodyBuilder.groovy | 4 +- .../amqp/SpringAmqpStubMessages.java | 17 +-- .../main/java/com/example/RabbitManager.java | 116 ++++++++++++++++++ .../AmqpMessagingApplicationSpec.groovy | 67 ++++++++++ 5 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 tests/samples-messaging-amqp/src/main/java/com/example/RabbitManager.java diff --git a/README.adoc b/README.adoc index 075d26c24f..92bf0a059d 100644 --- a/README.adoc +++ b/README.adoc @@ -666,7 +666,7 @@ package com.example.fraud; import org.junit.Before; -import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; +import io.restassured.module.mockmvc.RestAssuredMockMvc; public class FraudBase { @Before @@ -1109,15 +1109,15 @@ Example of a `pom.xml` inside the `server` folder. org.springframework.boot spring-boot-starter-parent - 1.5.4.BUILD-SNAPSHOT + 1.5.4.RELEASE UTF-8 1.8 - 1.1.2.BUILD-SNAPSHOT - Dalston.BUILD-SNAPSHOT + 1.2.0.BUILD-SNAPSHOT + Edgware.BUILD-SNAPSHOT true diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy index 2c7dec04ba..5681566294 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy @@ -171,10 +171,10 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { request = "${request}\n\t\t\t\t\"${StringEscapeUtils.escapeJava(bodyAsString)}\"\n" } if (inputMessage.messageHeaders) { - request = "${request}\t\t\t\t, headers()\n" + request = "${request}\t\t\t\t, headers()" } inputMessage.messageHeaders?.executeForEachHeader { Header header -> - request = "${request}\t\t\t\t\t\t${getHeaderString(header)}" + request = "${request}\n\t\t\t\t\t\t${getHeaderString(header)}" } return "${request}\n\t\t\t)" } 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 index 06e072aa0c..32a0599cee 100644 --- 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 @@ -16,13 +16,6 @@ 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.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -40,6 +33,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; import org.springframework.util.Assert; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.verify; +import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME; + /** * {@link MessageVerifier} implementation to integrate with plain spring-amqp/spring-rabbit. * It is meant to be used without interacting with a running bus. @@ -99,7 +100,7 @@ public class SpringAmqpStubMessages implements @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)); + verify(this.rabbitTemplate, atLeastOnce()).send(eq(destination), anyString(), messageCaptor.capture(), any(CorrelationData.class)); if (messageCaptor.getAllValues().isEmpty()) { log.info("no messages found on destination {}", destination); diff --git a/tests/samples-messaging-amqp/src/main/java/com/example/RabbitManager.java b/tests/samples-messaging-amqp/src/main/java/com/example/RabbitManager.java new file mode 100644 index 0000000000..eddd966995 --- /dev/null +++ b/tests/samples-messaging-amqp/src/main/java/com/example/RabbitManager.java @@ -0,0 +1,116 @@ +package com.example; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.Exchange; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.QueueBinding; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.stereotype.Component; + +@Component +public class RabbitManager { + + public static final Logger LOG = LoggerFactory.getLogger(RabbitManager.class); + + + private BookService service; + private RabbitTemplate rabbitTemplate; + + @Autowired + public RabbitManager(BookService service, RabbitTemplate rabbitTemplate) { + this.service = service; + this.rabbitTemplate = rabbitTemplate; + } + + @RabbitListener(bindings = @QueueBinding( + value = @Queue(), + exchange = @Exchange( + value = "input", + durable="true", + autoDelete="false", + type="topic"), + key = "event" + )) + public void newBook(Book book, @Headers Map headers) { + LOG.info("Received new book with bookname = " + book.getName()); + LOG.info("Headers = " + headers); + service.sendBook(book, headers.get("amqp_replyTo")); + } + + @RabbitListener(bindings = @QueueBinding( + value = @Queue(), + exchange = @Exchange( + value = "input", + durable="true", + autoDelete="false", + type="topic"), + key = "event2" + )) + public void newBook2(Book book, @Headers Map headers) { + LOG.info("newBook2 Received new book with bookname = " + book.getName()); + LOG.info("newBook2 Headers = " + headers); + service.sendBook(book, headers.get("amqp_replyTo")); + } +} + +interface BookService { + void sendBook(Book book, String replyTo); + + void newBook(Book book); + + Book getBook(int index); + + int noOfBooks(); + + List getBooks(); + +} + +@Component +class BookServiceImpl implements BookService { + public static final Logger LOG = LoggerFactory.getLogger(BookServiceImpl.class); + + private List books; + private RabbitTemplate rabbitTemplate; + + @Autowired + public BookServiceImpl(RabbitTemplate rabbitTemplate) { + books = new LinkedList<>(); + this.rabbitTemplate = rabbitTemplate; + } + + @Override + public void sendBook(Book book, String replyTo) { + LOG.info("Received new book with bookname = " + book.getName()); + newBook(book); + rabbitTemplate.convertAndSend("", replyTo, book); + } + + @Override + public void newBook(Book book) { + books.add(book); + } + + @Override + public Book getBook(int index) { + return books.get(index); + } + + @Override + public int noOfBooks() { + return books.size(); + } + + @Override + public List getBooks() { + return books; + } +} 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 index f0bf19e7ee..15b41d427b 100644 --- a/tests/samples-messaging-amqp/src/test/groovy/com/example/AmqpMessagingApplicationSpec.groovy +++ b/tests/samples-messaging-amqp/src/test/groovy/com/example/AmqpMessagingApplicationSpec.groovy @@ -25,6 +25,7 @@ import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.boot.test.context.SpringBootTest import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.messaging.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.test.context.ContextConfiguration @@ -33,6 +34,9 @@ import spock.lang.Specification import javax.inject.Inject +import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson +import static org.assertj.core.api.Assertions.assertThat +import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers // Context configuration would end up in base class @ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader) @AutoConfigureMessageVerifier @@ -79,6 +83,69 @@ class AmqpMessagingApplicationSpec extends Specification { JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some') } + @Issue("332") + def "should work for second scenario"() { + given: + def dsl = + Contract.make { + description(""" +Represents scenario 2 from documentation: +http://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_publisher_side_test_generation + +"The input message triggers an output message." + +``` +given: + rabbit service is running +when: + input message is received +then: + message is send +``` + +""") + label 'some_label2' + input { + messageFrom('input') + messageBody([ + name: 'foo2' + ]) + messageHeaders { + messagingContentType(applicationJson()) + header('amqp_replyTo', 'amq.rabbitmq.reply-to') + header('bill', 'bill') + } + } + + outputMessage { + sentTo('') + body('''{ "name" : "foo2" }''') + headers { + messagingContentType(applicationJson()) + } + } + } + // generated test should look like this: + and: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\"name\":\"foo2\"}" + , headers() + .header("contentType", "application/json") + .header("amqp_replyTo", "amq.rabbitmq.reply-to") + .header("bill", "bill") + ) + when: + contractVerifierMessaging.send(inputMessage, "input") + then: + ContractVerifierMessage response = contractVerifierMessaging.receive("") + assertThat(response).isNotNull() + assertThat(response.getHeader("contentType")).isNotNull() + assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json") + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())) + assertThatJson(parsedJson).field("['name']").isEqualTo("foo2") + } + @Issue("178") def "should work for input/output when bytes are used"() { given: