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: