Merge pull request #11027 from irynamatveieva/improvements/gcp-pubsub-node
PubSub node: added tests
This commit is contained in:
commit
a7e81e7ca7
@ -129,7 +129,7 @@ public class TbPubSubNode extends TbAbstractExternalNode {
|
|||||||
return TbMsg.transformMsgMetadata(origMsg, metaData);
|
return TbMsg.transformMsgMetadata(origMsg, metaData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publisher initPubSubClient(TbContext ctx) throws IOException {
|
Publisher initPubSubClient(TbContext ctx) throws IOException {
|
||||||
ProjectTopicName topicName = ProjectTopicName.of(config.getProjectId(), config.getTopicName());
|
ProjectTopicName topicName = ProjectTopicName.of(config.getProjectId(), config.getTopicName());
|
||||||
ServiceAccountCredentials credentials =
|
ServiceAccountCredentials credentials =
|
||||||
ServiceAccountCredentials.fromStream(
|
ServiceAccountCredentials.fromStream(
|
||||||
|
|||||||
@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* 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.gcp.pubsub;
|
||||||
|
|
||||||
|
import com.google.api.core.ApiFuture;
|
||||||
|
import com.google.api.core.ApiFutures;
|
||||||
|
import com.google.cloud.pubsub.v1.Publisher;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.pubsub.v1.PubsubMessage;
|
||||||
|
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.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.id.DeviceId;
|
||||||
|
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.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.BDDMockito.never;
|
||||||
|
import static org.mockito.BDDMockito.spy;
|
||||||
|
import static org.mockito.BDDMockito.then;
|
||||||
|
import static org.mockito.BDDMockito.willReturn;
|
||||||
|
import static org.mockito.BDDMockito.willThrow;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class TbPubSubNodeTest {
|
||||||
|
|
||||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("d29849c2-3f21-48e2-8557-74cdd6403290"));
|
||||||
|
private final ListeningExecutor executor = new TestDbCallbackExecutor();
|
||||||
|
|
||||||
|
private TbPubSubNode node;
|
||||||
|
private TbPubSubNodeConfiguration config;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Publisher pubSubClientMock;
|
||||||
|
@Mock
|
||||||
|
private TbContext ctxMock;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws IOException {
|
||||||
|
node = spy(new TbPubSubNode());
|
||||||
|
config = new TbPubSubNodeConfiguration().defaultConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyDefaultConfig() {
|
||||||
|
assertThat(config.getProjectId()).isEqualTo("my-google-cloud-project-id");
|
||||||
|
assertThat(config.getTopicName()).isEqualTo("my-pubsub-topic-name");
|
||||||
|
assertThat(config.getMessageAttributes()).isEmpty();
|
||||||
|
assertThat(config.getServiceAccountKey()).isNull();
|
||||||
|
assertThat(config.getServiceAccountKeyFileName()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenValidConfig_whenInit_thenOk() throws IOException {
|
||||||
|
willReturn(pubSubClientMock).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
assertThatNoException().isThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenErrorOccursDuringInitClient_whenInit_thenThrowsException() throws IOException {
|
||||||
|
willThrow(new RuntimeException("Could not initialize client!")).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
|
||||||
|
.isInstanceOf(TbNodeException.class).hasMessage("java.lang.RuntimeException: Could not initialize client!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
public void givenForceAckIsTrueAndMessageAttributesPatterns_whenOnMsg_thenEnqueueForTellNext(
|
||||||
|
String attributeName, String attributeValue, TbMsgMetaData metaData, String data) throws IOException, TbNodeException {
|
||||||
|
config.setMessageAttributes(Map.of(attributeName, attributeValue));
|
||||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(true);
|
||||||
|
willReturn(pubSubClientMock).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
String messageId = "2070443601311540";
|
||||||
|
given(pubSubClientMock.publish(any())).willReturn(ApiFutures.immediateFuture(messageId));
|
||||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
|
||||||
|
|
||||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data);
|
||||||
|
node.onMsg(ctxMock, msg);
|
||||||
|
|
||||||
|
then(ctxMock).should().ack(msg);
|
||||||
|
PubsubMessage.Builder pubsubMessageBuilder = PubsubMessage.newBuilder();
|
||||||
|
pubsubMessageBuilder.setData(ByteString.copyFromUtf8(msg.getData()));
|
||||||
|
this.config.getMessageAttributes().forEach((k, v) -> {
|
||||||
|
String name = TbNodeUtils.processPattern(k, msg);
|
||||||
|
String val = TbNodeUtils.processPattern(v, msg);
|
||||||
|
pubsubMessageBuilder.putAttributes(name, val);
|
||||||
|
});
|
||||||
|
then(pubSubClientMock).should().publish(pubsubMessageBuilder.build());
|
||||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
|
||||||
|
then(ctxMock).should().enqueueForTellNext(actualMsg.capture(), eq(TbNodeConnectionType.SUCCESS));
|
||||||
|
metaData.putValue("messageId", messageId);
|
||||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metaData);
|
||||||
|
assertThat(actualMsg.getValue())
|
||||||
|
.usingRecursiveComparison()
|
||||||
|
.ignoringFields("ctx")
|
||||||
|
.isEqualTo(expectedMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> givenForceAckIsTrueAndMessageAttributesPatterns_whenOnMsg_thenEnqueueForTellNext() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("attributeName", "attributeValue", new TbMsgMetaData(), TbMsg.EMPTY_JSON_OBJECT),
|
||||||
|
Arguments.of("${mdAttrName}", "${mdAttrValue}", new TbMsgMetaData(
|
||||||
|
Map.of(
|
||||||
|
"mdAttrName", "mdAttributeName",
|
||||||
|
"mdAttrValue", "mdAttributeValue"
|
||||||
|
)), TbMsg.EMPTY_JSON_OBJECT),
|
||||||
|
Arguments.of("$[msgAttrName]", "$[msgAttrValue]", new TbMsgMetaData(),
|
||||||
|
"{\"msgAttrName\": \"msgAttributeName\", \"msgAttrValue\": \"mdAttributeValue\"}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenForceAckIsFalse_whenOnMsg_thenTellSuccess() throws IOException, TbNodeException {
|
||||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(false);
|
||||||
|
willReturn(pubSubClientMock).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
String messageId = "2070443601311540";
|
||||||
|
given(pubSubClientMock.publish(any())).willReturn(ApiFutures.immediateFuture(messageId));
|
||||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
|
||||||
|
|
||||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||||
|
TbMsgMetaData metadata = new TbMsgMetaData();
|
||||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metadata, TbMsg.EMPTY_JSON_OBJECT);
|
||||||
|
node.onMsg(ctxMock, msg);
|
||||||
|
|
||||||
|
then(ctxMock).should(never()).ack(msg);
|
||||||
|
PubsubMessage.Builder pubsubMessageBuilder = PubsubMessage.newBuilder();
|
||||||
|
pubsubMessageBuilder.setData(ByteString.copyFromUtf8(msg.getData()));
|
||||||
|
then(pubSubClientMock).should().publish(pubsubMessageBuilder.build());
|
||||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
|
||||||
|
then(ctxMock).should().tellSuccess(actualMsg.capture());
|
||||||
|
metadata.putValue("messageId", messageId);
|
||||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metadata);
|
||||||
|
assertThat(actualMsg.getValue())
|
||||||
|
.usingRecursiveComparison()
|
||||||
|
.ignoringFields("ctx")
|
||||||
|
.isEqualTo(expectedMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenForceAckIsFalseAndErrorOccursOnTheGCP_whenOnMsg_thenTellFailure() throws IOException, TbNodeException {
|
||||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(false);
|
||||||
|
willReturn(pubSubClientMock).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
String errorMsg = "Something went wrong!";
|
||||||
|
ApiFuture<String> failedFuture = ApiFutures.immediateFailedFuture(new RuntimeException(errorMsg));
|
||||||
|
given(pubSubClientMock.publish(any())).willReturn(failedFuture);
|
||||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
|
||||||
|
|
||||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||||
|
TbMsgMetaData metaData = new TbMsgMetaData();
|
||||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, TbMsg.EMPTY_JSON_OBJECT);
|
||||||
|
node.onMsg(ctxMock, msg);
|
||||||
|
|
||||||
|
then(ctxMock).should(never()).ack(any());
|
||||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
|
||||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class);
|
||||||
|
then(ctxMock).should().tellFailure(actualMsg.capture(), actualError.capture());
|
||||||
|
metaData.putValue("error", RuntimeException.class + ": " + errorMsg);
|
||||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metaData);
|
||||||
|
assertThat(actualMsg.getValue())
|
||||||
|
.usingRecursiveComparison()
|
||||||
|
.ignoringFields("ctx")
|
||||||
|
.isEqualTo(expectedMsg);
|
||||||
|
assertThat(actualError.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenForceAckIsTrueAndErrorOccursOnTheGCP_whenOnMsg_thenEnqueueForTellFailure() throws IOException, TbNodeException {
|
||||||
|
given(ctxMock.isExternalNodeForceAck()).willReturn(true);
|
||||||
|
willReturn(pubSubClientMock).given(node).initPubSubClient(ctxMock);
|
||||||
|
|
||||||
|
String errorMsg = "Something went wrong!";
|
||||||
|
ApiFuture<String> failedFuture = ApiFutures.immediateFailedFuture(new RuntimeException(errorMsg));
|
||||||
|
given(pubSubClientMock.publish(any())).willReturn(failedFuture);
|
||||||
|
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
|
||||||
|
|
||||||
|
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||||
|
TbMsgMetaData metaData = new TbMsgMetaData();
|
||||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, TbMsg.EMPTY_JSON_OBJECT);
|
||||||
|
node.onMsg(ctxMock, msg);
|
||||||
|
|
||||||
|
then(ctxMock).should().ack(msg);
|
||||||
|
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
|
||||||
|
ArgumentCaptor<Throwable> actualError = ArgumentCaptor.forClass(Throwable.class);
|
||||||
|
then(ctxMock).should().enqueueForTellFailure(actualMsg.capture(), actualError.capture());
|
||||||
|
metaData.putValue("error", RuntimeException.class + ": " + errorMsg);
|
||||||
|
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metaData);
|
||||||
|
assertThat(actualMsg.getValue())
|
||||||
|
.usingRecursiveComparison()
|
||||||
|
.ignoringFields("ctx")
|
||||||
|
.isEqualTo(expectedMsg);
|
||||||
|
assertThat(actualError.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenPubSubClientIsNotNull_whenDestroy_thenShutDownAndAwaitTermination() throws InterruptedException {
|
||||||
|
ReflectionTestUtils.setField(node, "pubSubClient", pubSubClientMock);
|
||||||
|
node.destroy();
|
||||||
|
then(pubSubClientMock).should().shutdown();
|
||||||
|
then(pubSubClientMock).should().awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenPubSubClientIsNull_whenDestroy_thenShutDownAndAwaitTermination() {
|
||||||
|
ReflectionTestUtils.setField(node, "pubSubClient", null);
|
||||||
|
node.destroy();
|
||||||
|
then(pubSubClientMock).shouldHaveNoInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user