Added support for Spring Kafka
fixes gh-877
This commit is contained in:
@@ -75,14 +75,7 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
Assert.isTrue(
|
||||
mockingDetails(rabbitTemplate).isSpy()
|
||||
|| mockingDetails(rabbitTemplate).isMock(),
|
||||
"StubRunner AMQP will work only if RabbiTemplate is a spy"); // we get
|
||||
// send
|
||||
// messages
|
||||
// by
|
||||
// capturing
|
||||
// arguments
|
||||
// on the
|
||||
// spy
|
||||
"StubRunner AMQP will work only if RabbiTemplate is a spy");
|
||||
this.rabbitTemplate = rabbitTemplate;
|
||||
this.messageListenerAccessor = messageListenerAccessor;
|
||||
}
|
||||
@@ -95,14 +88,7 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
|
||||
Assert.isTrue(
|
||||
mockingDetails(rabbitTemplate).isSpy()
|
||||
|| mockingDetails(rabbitTemplate).isMock(),
|
||||
"StubRunner AMQP will work only if RabbiTemplate is a spy"); // we get
|
||||
// send
|
||||
// messages
|
||||
// by
|
||||
// capturing
|
||||
// arguments
|
||||
// on the
|
||||
// spy
|
||||
"StubRunner AMQP will work only if RabbiTemplate is a spy");
|
||||
this.rabbitTemplate = rabbitTemplate;
|
||||
this.messageListenerAccessor = messageListenerAccessor;
|
||||
this.rabbitProperties = rabbitProperties;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.kafka;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
|
||||
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.noop.NoOpContractVerifierAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass({ KafkaTemplate.class, EmbeddedKafkaBroker.class })
|
||||
@ConditionalOnProperty(name = "stubrunner.kafka.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
@AutoConfigureBefore({ ContractVerifierIntegrationConfiguration.class,
|
||||
NoOpContractVerifierAutoConfiguration.class })
|
||||
@ConditionalOnBean(EmbeddedKafkaBroker.class)
|
||||
public class ContractVerifierKafkaConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
MessageVerifier<Message<?>> contractVerifierKafkaMessageExchange(
|
||||
KafkaTemplate kafkaTemplate, EmbeddedKafkaBroker broker,
|
||||
KafkaProperties kafkaProperties, KafkaStubMessagesInitializer initializer) {
|
||||
return new KafkaStubMessages(kafkaTemplate, broker, kafkaProperties, initializer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
KafkaStubMessagesInitializer contractVerifierKafkaStubMessagesInitializer() {
|
||||
return new ContractVerifierKafkaStubMessagesInitializer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ContractVerifierMessaging<Message<?>> contractVerifierKafkaMessaging(
|
||||
MessageVerifier<Message<?>> exchange) {
|
||||
return new ContractVerifierKafkaHelper(exchange);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ContractVerifierKafkaHelper extends ContractVerifierMessaging<Message<?>> {
|
||||
|
||||
ContractVerifierKafkaHelper(MessageVerifier<Message<?>> exchange) {
|
||||
super(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContractVerifierMessage convert(Message<?> message) {
|
||||
return new ContractVerifierMessage(message.getPayload(), message.getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.kafka;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
|
||||
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
|
||||
class ContractVerifierKafkaStubMessagesInitializer
|
||||
implements KafkaStubMessagesInitializer {
|
||||
|
||||
private static final Log log = LogFactory
|
||||
.getLog(ContractVerifierKafkaStubMessagesInitializer.class);
|
||||
|
||||
@Override
|
||||
public Map<String, Consumer> initialize(EmbeddedKafkaBroker broker,
|
||||
KafkaProperties kafkaProperties) {
|
||||
Map<String, Consumer> map = new HashMap<>();
|
||||
for (String topic : broker.getTopics()) {
|
||||
map.put(topic, prepareListener(broker, topic, kafkaProperties));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private Consumer prepareListener(EmbeddedKafkaBroker broker, String destination,
|
||||
KafkaProperties kafkaProperties) {
|
||||
Map<String, Object> consumerProperties = KafkaTestUtils.consumerProps(
|
||||
kafkaProperties.getConsumer().getGroupId(), "false", broker);
|
||||
consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
DefaultKafkaConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<>(
|
||||
consumerProperties);
|
||||
Consumer<String, String> consumer = consumerFactory.createConsumer();
|
||||
broker.consumeFromAnEmbeddedTopic(consumer, destination);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Prepared consumer for destination [" + destination + "]");
|
||||
}
|
||||
return consumer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 org.springframework.cloud.contract.verifier.messaging.kafka;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
import net.minidev.json.parser.JSONParser;
|
||||
import net.minidev.json.parser.ParseException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.common.header.Header;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
|
||||
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
class KafkaStubMessages implements MessageVerifier<Message<?>> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(KafkaStubMessages.class);
|
||||
|
||||
private final KafkaTemplate kafkaTemplate;
|
||||
|
||||
private final Receiver receiver;
|
||||
|
||||
KafkaStubMessages(KafkaTemplate kafkaTemplate, EmbeddedKafkaBroker broker,
|
||||
KafkaProperties kafkaProperties, KafkaStubMessagesInitializer initializer) {
|
||||
this.kafkaTemplate = kafkaTemplate;
|
||||
Map<String, Consumer> topicToConsumer = initializer.initialize(broker,
|
||||
kafkaProperties);
|
||||
this.receiver = new Receiver(topicToConsumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message<?> message, String destination) {
|
||||
String defaultTopic = this.kafkaTemplate.getDefaultTopic();
|
||||
try {
|
||||
this.kafkaTemplate.setDefaultTopic(destination);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will send a message [" + message + "] to destination ["
|
||||
+ destination + "]");
|
||||
}
|
||||
this.kafkaTemplate.send(message).get(5, TimeUnit.SECONDS);
|
||||
this.kafkaTemplate.flush();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
finally {
|
||||
this.kafkaTemplate.setDefaultTopic(defaultTopic);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
|
||||
return this.receiver.receive(destination, timeout, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String destination) {
|
||||
return receive(destination, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object payload, Map headers, String destination) {
|
||||
Message<?> message = MessageBuilder.createMessage(payload,
|
||||
new MessageHeaders(headers));
|
||||
send(message, destination);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Receiver {
|
||||
|
||||
private final Map<String, Consumer> consumers;
|
||||
|
||||
private static final Log log = LogFactory.getLog(Receiver.class);
|
||||
|
||||
Receiver(Map<String, Consumer> consumers) {
|
||||
this.consumers = consumers;
|
||||
}
|
||||
|
||||
Message receive(String topic, long timeout, TimeUnit timeUnit) {
|
||||
Consumer consumer = this.consumers.get(topic);
|
||||
if (consumer == null) {
|
||||
throw new IllegalStateException(
|
||||
"No consumer set up for topic [" + topic + "]");
|
||||
}
|
||||
ConsumerRecord<String, String> record = KafkaTestUtils.getSingleRecord(consumer,
|
||||
topic, timeUnit.toMillis(timeout));
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Got a single record for destination [" + topic + "]");
|
||||
}
|
||||
return new Record(record).toMessage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Record {
|
||||
|
||||
private final ConsumerRecord record;
|
||||
|
||||
Record(ConsumerRecord record) {
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(Headers headers) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (Header header : headers) {
|
||||
map.put(header.key(), header.value());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
Message toMessage() {
|
||||
Object textPayload = record.value();
|
||||
// sometimes it's a message sometimes just payload
|
||||
MessageHeaders headers = new MessageHeaders(toMap(record.headers()));
|
||||
if (textPayload instanceof String && ((String) textPayload).contains("payload")
|
||||
&& ((String) textPayload).contains("headers")) {
|
||||
try {
|
||||
Object object = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE)
|
||||
.parse((String) textPayload);
|
||||
JSONObject jo = (JSONObject) object;
|
||||
String payload = (String) jo.get("payload");
|
||||
JSONObject headersInJson = (JSONObject) jo.get("headers");
|
||||
Map newHeaders = new HashMap(headers);
|
||||
newHeaders.putAll(headersInJson);
|
||||
return MessageBuilder.createMessage(unquoted(payload),
|
||||
new MessageHeaders(newHeaders));
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
return MessageBuilder.createMessage(unquoted(textPayload), headers);
|
||||
}
|
||||
|
||||
private Object unquoted(Object value) {
|
||||
String textPayload = value instanceof byte[] ? new String((byte[]) value)
|
||||
: value.toString();
|
||||
if (textPayload.startsWith("\"") && textPayload.endsWith("\"")) {
|
||||
return textPayload.substring(1, textPayload.length() - 1).replace("\\\"",
|
||||
"\"");
|
||||
}
|
||||
return textPayload;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.kafka;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
|
||||
/**
|
||||
* Logic used to initialize {@link KafkaStubMessages}. This interface might have a
|
||||
* different implementation for the producer side and for the consumer side. That's
|
||||
* because you can't poll for a single message by different consumers.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public interface KafkaStubMessagesInitializer {
|
||||
|
||||
/**
|
||||
* @param broker - embedded Kafka broker
|
||||
* @param kafkaProperties - kafka properties
|
||||
* @return topic to initialized consumer mapping
|
||||
*/
|
||||
Map<String, Consumer> initialize(EmbeddedKafkaBroker broker,
|
||||
KafkaProperties kafkaProperties);
|
||||
|
||||
}
|
||||
@@ -6,4 +6,5 @@ org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpA
|
||||
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.kafka.ContractVerifierKafkaConfiguration,\
|
||||
org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration
|
||||
|
||||
Reference in New Issue
Block a user