committed by
GitHub
2 changed files with 428 additions and 1 deletions
@ -0,0 +1,423 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard 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.thingsboard.rule.engine.kafka; |
||||
|
|
||||
|
import org.apache.kafka.clients.producer.Callback; |
||||
|
import org.apache.kafka.clients.producer.KafkaProducer; |
||||
|
import org.apache.kafka.clients.producer.ProducerConfig; |
||||
|
import org.apache.kafka.clients.producer.ProducerRecord; |
||||
|
import org.apache.kafka.clients.producer.RecordMetadata; |
||||
|
import org.apache.kafka.common.header.Headers; |
||||
|
import org.apache.kafka.common.header.internals.RecordHeader; |
||||
|
import org.apache.kafka.common.header.internals.RecordHeaders; |
||||
|
import org.apache.kafka.common.serialization.StringSerializer; |
||||
|
import org.apache.kafka.common.utils.KafkaThread; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||
|
import org.junit.jupiter.params.provider.Arguments; |
||||
|
import org.junit.jupiter.params.provider.MethodSource; |
||||
|
import org.junit.jupiter.params.provider.NullAndEmptySource; |
||||
|
import org.junit.jupiter.params.provider.ValueSource; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.springframework.test.util.ReflectionTestUtils; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.common.util.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.RuleNodeId; |
||||
|
import org.thingsboard.server.common.data.msg.TbMsgType; |
||||
|
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.Map; |
||||
|
import java.util.Properties; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Callable; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.BDDMockito.given; |
||||
|
import static org.mockito.BDDMockito.mock; |
||||
|
import static org.mockito.BDDMockito.never; |
||||
|
import static org.mockito.BDDMockito.spy; |
||||
|
import static org.mockito.BDDMockito.then; |
||||
|
import static org.mockito.BDDMockito.times; |
||||
|
import static org.mockito.BDDMockito.willAnswer; |
||||
|
import static org.mockito.BDDMockito.willReturn; |
||||
|
import static org.mockito.BDDMockito.willThrow; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbKafkaNodeTest { |
||||
|
|
||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5f2eac08-bd1f-4635-a6c2-437369f996cf")); |
||||
|
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("d46bb666-ecab-4d89-a28f-5abdca23ac29")); |
||||
|
private final ListeningExecutor executor = new TestDbCallbackExecutor(); |
||||
|
|
||||
|
private final long OFFSET = 1; |
||||
|
private final int PARTITION = 0; |
||||
|
|
||||
|
private final String SERVICE_ID_STR = "test-service-id"; |
||||
|
private final String TEST_TOPIC = "test-topic"; |
||||
|
private final String TEST_KEY = "test-key"; |
||||
|
|
||||
|
private TbKafkaNode node; |
||||
|
private TbKafkaNodeConfiguration config; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private KafkaProducer<String, String> producerMock; |
||||
|
@Mock |
||||
|
private KafkaThread ioThreadMock; |
||||
|
@Mock |
||||
|
private RecordMetadata recordMetadataMock; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
node = spy(new TbKafkaNode()); |
||||
|
config = new TbKafkaNodeConfiguration().defaultConfiguration(); |
||||
|
config.setTopicPattern(TEST_TOPIC); |
||||
|
config.setKeyPattern(TEST_KEY); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void verifyDefaultConfig() { |
||||
|
config = new TbKafkaNodeConfiguration().defaultConfiguration(); |
||||
|
assertThat(config.getTopicPattern()).isEqualTo("my-topic"); |
||||
|
assertThat(config.getKeyPattern()).isNull(); |
||||
|
assertThat(config.getBootstrapServers()).isEqualTo("localhost:9092"); |
||||
|
assertThat(config.getRetries()).isEqualTo(0); |
||||
|
assertThat(config.getBatchSize()).isEqualTo(16384); |
||||
|
assertThat(config.getLinger()).isEqualTo(0); |
||||
|
assertThat(config.getBufferMemory()).isEqualTo(33554432); |
||||
|
assertThat(config.getAcks()).isEqualTo("-1"); |
||||
|
assertThat(config.getKeySerializer()).isEqualTo(StringSerializer.class.getName()); |
||||
|
assertThat(config.getValueSerializer()).isEqualTo(StringSerializer.class.getName()); |
||||
|
assertThat(config.getOtherProperties()).isEmpty(); |
||||
|
assertThat(config.isAddMetadataKeyValuesAsKafkaHeaders()).isFalse(); |
||||
|
assertThat(config.getKafkaHeadersCharset()).isEqualTo("UTF-8"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenExceptionDuringKafkaInitialization_whenInit_thenDestroy() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID); |
||||
|
ReflectionTestUtils.setField(producerMock, "ioThread", ioThreadMock); |
||||
|
willAnswer(invocationOnMock -> { |
||||
|
Thread.UncaughtExceptionHandler exceptionHandler = invocationOnMock.getArgument(0); |
||||
|
exceptionHandler.uncaughtException(ioThreadMock, new ThingsboardKafkaClientError("Error during init")); |
||||
|
return null; |
||||
|
}).given(ioThreadMock).setUncaughtExceptionHandler(any()); |
||||
|
willReturn(producerMock).given(node).getKafkaProducer(any()); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
|
||||
|
// THEN
|
||||
|
then(producerMock).should().close(); |
||||
|
then(producerMock).shouldHaveNoMoreInteractions(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void verifyKafkaProperties() throws TbNodeException { |
||||
|
String sslKeyStoreCertificateChain = "cbdvch\\nfwrg\nvgwg\\n"; |
||||
|
String sslKeyStoreKey = "nghmh\\nhmmnh\\\\ngreg\nvgwg\\n"; |
||||
|
String sslTruststoreCertificates = "grthrt\fd\\nfwrg\nvgwg\\n"; |
||||
|
config.setOtherProperties(Map.of( |
||||
|
"ssl.keystore.certificate.chain", sslKeyStoreCertificateChain, |
||||
|
"ssl.keystore.key", sslKeyStoreKey, |
||||
|
"ssl.truststore.certificates", sslTruststoreCertificates, |
||||
|
"ssl.protocol", "TLSv1.2" |
||||
|
)); |
||||
|
|
||||
|
mockSuccessfulInit(); |
||||
|
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
|
||||
|
Properties expectedProperties = new Properties(); |
||||
|
expectedProperties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + RULE_NODE_ID.getId() + "-" + SERVICE_ID_STR); |
||||
|
expectedProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); |
||||
|
expectedProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); |
||||
|
expectedProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); |
||||
|
expectedProperties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); |
||||
|
expectedProperties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); |
||||
|
expectedProperties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); |
||||
|
expectedProperties.put(ProducerConfig.LINGER_MS_CONFIG, config.getLinger()); |
||||
|
expectedProperties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, config.getBufferMemory()); |
||||
|
expectedProperties.put("ssl.keystore.certificate.chain", sslKeyStoreCertificateChain.replace("\\n", "\n")); |
||||
|
expectedProperties.put("ssl.keystore.key", sslKeyStoreKey.replace("\\n", "\n")); |
||||
|
expectedProperties.put("ssl.truststore.certificates", sslTruststoreCertificates.replace("\\n", "\n")); |
||||
|
expectedProperties.put("ssl.protocol", "TLSv1.2"); |
||||
|
|
||||
|
ArgumentCaptor<Properties> properties = ArgumentCaptor.forClass(Properties.class); |
||||
|
then(node).should().getKafkaProducer(properties.capture()); |
||||
|
assertThat(properties.getValue()).isEqualTo(expectedProperties); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenInitErrorIsNotNull_whenOnMsg_thenTellFailure() { |
||||
|
// GIVEN
|
||||
|
String errorMsg = "Error during kafka initialization!"; |
||||
|
ReflectionTestUtils.setField(node, "config", config); |
||||
|
ReflectionTestUtils.setField(node, "initError", new ThingsboardKafkaClientError(errorMsg)); |
||||
|
|
||||
|
// WHEN
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); |
||||
|
assertThat(actualError.getValue()) |
||||
|
.isInstanceOf(RuntimeException.class) |
||||
|
.hasMessage("Failed to initialize Kafka rule node producer: " + errorMsg); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@ValueSource(booleans = {true, false}) |
||||
|
public void givenForceAckAndExceptionWasThrown_whenOnMsg_thenTellFailure(boolean forceAck) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(forceAck); |
||||
|
mockSuccessfulInit(); |
||||
|
ListeningExecutor executorMock = mock(ListeningExecutor.class); |
||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executorMock); |
||||
|
String errorMsg = "Something went wrong!"; |
||||
|
willThrow(new RuntimeException(errorMsg)).given(executorMock).executeAsync(any(Callable.class)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
then(ctxMock).should(forceAck ? times(1) : never()).ack(msg); |
||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().tellFailure(actualMsg.capture(), actualError.capture()); |
||||
|
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(msg); |
||||
|
assertThat(actualError.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
public void givenForceAckIsTrueTopicAndKeyPatternsAndAddMetadataKeyValuesAsKafkaHeadersIsFalse_whenOnMsg_thenEnqueueForTellNext( |
||||
|
String topicPattern, String keyPattern, TbMsgMetaData metaData, String data |
||||
|
) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setTopicPattern(topicPattern); |
||||
|
config.setKeyPattern(keyPattern); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); |
||||
|
String topic = TbNodeUtils.processPattern(topicPattern, msg); |
||||
|
String key = TbNodeUtils.processPattern(keyPattern, msg); |
||||
|
|
||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(true); |
||||
|
mockSuccessfulInit(); |
||||
|
mockSuccessfulPublishingRequest(topic); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
then(ctxMock).should().ack(msg); |
||||
|
verifyProducerRecord(topic, key, msg.getData()); |
||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
then(ctxMock).should().enqueueForTellNext(actualMsg.capture(), eq(TbNodeConnectionType.SUCCESS)); |
||||
|
verifyOutgoingSuccessMsg(topic, actualMsg.getValue(), msg); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenForceAckIsTrueTopicAndKeyPatternsAndAddMetadataKeyValuesAsKafkaHeadersIsFalse_whenOnMsg_thenEnqueueForTellNext() { |
||||
|
return Stream.of( |
||||
|
Arguments.of("test-topic", "test-key", new TbMsgMetaData(), TbMsg.EMPTY_JSON_OBJECT), |
||||
|
Arguments.of("${mdTopicPattern}", "${mdKeyPattern}", new TbMsgMetaData( |
||||
|
Map.of( |
||||
|
"mdTopicPattern", "md-test-topic", |
||||
|
"mdKeyPattern", "md-test-key" |
||||
|
)), TbMsg.EMPTY_JSON_OBJECT), |
||||
|
Arguments.of("$[msgTopicPattern]", "$[msgKeyPattern]", new TbMsgMetaData(), |
||||
|
"{\"msgTopicPattern\":\"msg-test-topic\",\"msgKeyPattern\":\"msg-test-key\"}") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@NullAndEmptySource |
||||
|
public void givenForceAckIsFalseAndKeyIsNullOrEmptyAndErrorOccursDuringPublishing_whenOnMsg_thenTellFailure(String key) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setKeyPattern(key); |
||||
|
|
||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(false); |
||||
|
mockSuccessfulInit(); |
||||
|
String errorMsg = "Something went wrong!"; |
||||
|
mockFailedPublishingRequest(new RuntimeException(errorMsg)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verifyProducerRecord(TEST_TOPIC, null, msg.getData()); |
||||
|
then(ctxMock).should(never()).ack(msg); |
||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().tellFailure(actualMsg.capture(), actualError.capture()); |
||||
|
verifyOutgoingFailureMsg(errorMsg, actualMsg.getValue(), msg); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForceAckIsTrueAndAddKafkaHeadersIsTrueAndToBytesCharsetIsNullAndErrorOccursDuringPublishing_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setAddMetadataKeyValuesAsKafkaHeaders(true); |
||||
|
config.setKafkaHeadersCharset(null); |
||||
|
|
||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(true); |
||||
|
mockSuccessfulInit(); |
||||
|
String errorMsg = "Something went wrong!"; |
||||
|
mockFailedPublishingRequest(new RuntimeException(errorMsg)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
then(ctxMock).should().ack(msg); |
||||
|
Headers expectedHeaders = new RecordHeaders(); |
||||
|
msg.getMetaData().values().forEach((k, v) -> expectedHeaders.add(new RecordHeader("tb_msg_md_" + k, v.getBytes(StandardCharsets.UTF_8)))); |
||||
|
verifyProducerRecord(TEST_TOPIC, TEST_KEY, msg.getData(), expectedHeaders); |
||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().enqueueForTellFailure(actualMsg.capture(), actualError.capture()); |
||||
|
verifyOutgoingFailureMsg(errorMsg, actualMsg.getValue(), msg); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenForceAckIsFalseAndAddMetadataKeyValuesAsKafkaHeadersIsTrueAndToBytesCharsetIsSet_whenOnMsg_thenTellSuccess() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setAddMetadataKeyValuesAsKafkaHeaders(true); |
||||
|
config.setKafkaHeadersCharset("UTF-16"); |
||||
|
|
||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(false); |
||||
|
mockSuccessfulInit(); |
||||
|
mockSuccessfulPublishingRequest(TEST_TOPIC); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); |
||||
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("key", "value"); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
then(ctxMock).should(never()).ack(msg); |
||||
|
Headers expectedHeaders = new RecordHeaders(); |
||||
|
msg.getMetaData().values().forEach((k, v) -> expectedHeaders.add(new RecordHeader("tb_msg_md_" + k, v.getBytes(StandardCharsets.UTF_16)))); |
||||
|
verifyProducerRecord(TEST_TOPIC, TEST_KEY, msg.getData(), expectedHeaders); |
||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
then(ctxMock).should().tellSuccess(actualMsg.capture()); |
||||
|
verifyOutgoingSuccessMsg(TEST_TOPIC, actualMsg.getValue(), msg); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenProducerIsNotNull_whenDestroy_thenShouldClose() { |
||||
|
ReflectionTestUtils.setField(node, "producer", producerMock); |
||||
|
node.destroy(); |
||||
|
then(producerMock).should().close(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenProducerIsNull_whenDestroy_thenDoNothing() { |
||||
|
node.destroy(); |
||||
|
then(producerMock).shouldHaveNoInteractions(); |
||||
|
} |
||||
|
|
||||
|
private void mockSuccessfulInit() { |
||||
|
given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID); |
||||
|
given(ctxMock.getServiceId()).willReturn(SERVICE_ID_STR); |
||||
|
ReflectionTestUtils.setField(producerMock, "ioThread", ioThreadMock); |
||||
|
willReturn(producerMock).given(node).getKafkaProducer(any()); |
||||
|
} |
||||
|
|
||||
|
private void mockSuccessfulPublishingRequest(String topic) { |
||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor); |
||||
|
willAnswer(invocation -> { |
||||
|
Callback callback = invocation.getArgument(1); |
||||
|
callback.onCompletion(recordMetadataMock, null); |
||||
|
return null; |
||||
|
}).given(producerMock).send(any(), any(Callback.class)); |
||||
|
given(recordMetadataMock.offset()).willReturn(OFFSET); |
||||
|
given(recordMetadataMock.partition()).willReturn(PARTITION); |
||||
|
given(recordMetadataMock.topic()).willReturn(topic); |
||||
|
} |
||||
|
|
||||
|
private void mockFailedPublishingRequest(Exception exception) { |
||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor); |
||||
|
willAnswer(invocation -> { |
||||
|
Callback callback = invocation.getArgument(1); |
||||
|
callback.onCompletion(recordMetadataMock, exception); |
||||
|
return null; |
||||
|
}).given(producerMock).send(any(), any(Callback.class)); |
||||
|
} |
||||
|
|
||||
|
private void verifyProducerRecord(String expectedTopic, String expectedKey, String expectedValue) { |
||||
|
verifyProducerRecord(expectedTopic, expectedKey, expectedValue, null); |
||||
|
} |
||||
|
|
||||
|
private void verifyProducerRecord(String expectedTopic, String expectedKey, String expectedValue, Headers expectedHeaders) { |
||||
|
ArgumentCaptor<ProducerRecord<String, String>> actualRecordCaptor = ArgumentCaptor.forClass(ProducerRecord.class); |
||||
|
then(producerMock).should().send(actualRecordCaptor.capture(), any()); |
||||
|
ProducerRecord<String, String> actualRecord = actualRecordCaptor.getValue(); |
||||
|
assertThat(actualRecord.topic()).isEqualTo(expectedTopic); |
||||
|
assertThat(actualRecord.key()).isEqualTo(expectedKey); |
||||
|
assertThat(actualRecord.value()).isEqualTo(expectedValue); |
||||
|
if (expectedHeaders != null) { |
||||
|
assertThat(actualRecord.headers()).isEqualTo(expectedHeaders); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void verifyOutgoingSuccessMsg(String expectedTopic, TbMsg actualMsg, TbMsg originalMsg) { |
||||
|
TbMsgMetaData metaData = originalMsg.getMetaData().copy(); |
||||
|
metaData.putValue("offset", String.valueOf(OFFSET)); |
||||
|
metaData.putValue("partition", String.valueOf(PARTITION)); |
||||
|
metaData.putValue("topic", expectedTopic); |
||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(originalMsg, metaData); |
||||
|
assertThat(actualMsg) |
||||
|
.usingRecursiveComparison() |
||||
|
.ignoringFields("ctx") |
||||
|
.isEqualTo(expectedMsg); |
||||
|
} |
||||
|
|
||||
|
private void verifyOutgoingFailureMsg(String errorMsg, TbMsg actualMsg, TbMsg originalMsg) { |
||||
|
TbMsgMetaData metaData = originalMsg.getMetaData(); |
||||
|
metaData.putValue("error", RuntimeException.class + ": " + errorMsg); |
||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(originalMsg, metaData); |
||||
|
assertThat(actualMsg).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue