add spring-amqp/spring-rabbit support for message contracts (#106)
This commit is contained in:
committed by
Marcin Grzejszczak
parent
74ea3fd27d
commit
2b70b450b9
@@ -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<Message> contractVerifierMessageExchange() {
|
||||
return new SpringAmqpStubMessages(this.rabbitTemplate, this.messageListenerAdapter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ContractVerifierMessaging<Message> contractVerifierMessaging(
|
||||
MessageVerifier<Message> exchange) {
|
||||
return new ContractVerifierHelper(exchange, this.rabbitTemplate.getMessageConverter());
|
||||
}
|
||||
}
|
||||
|
||||
class ContractVerifierHelper extends ContractVerifierMessaging<Message> {
|
||||
|
||||
private final MessageConverter messageConverter;
|
||||
|
||||
public ContractVerifierHelper(MessageVerifier<Message> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Message> {
|
||||
|
||||
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 <T> void send(T payload, Map<String, Object> 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<Message> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<Message> 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user