From 9c740e147d4bc9c8e60604db28c17c523066aed4 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Apr 2024 13:46:41 +0300 Subject: [PATCH 01/34] added tests for copy to view node --- .../TbCopyAttributesToEntityViewNodeTest.java | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java new file mode 100644 index 0000000000..a08872ba02 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -0,0 +1,224 @@ +/** + * 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.action; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +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.EnumSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.objects.AttributesEntityView; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.entityview.EntityViewService; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; + +@ExtendWith(MockitoExtension.class) +public class TbCopyAttributesToEntityViewNodeTest { + + private final TenantId TENANT_ID = new TenantId(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1d453dc9-9333-476a-a51f-093cf2176e59")); + + private TbCopyAttributesToEntityViewNode node; + private EmptyNodeConfiguration config; + + @Mock + private TbContext ctxMock; + @Mock + private EntityViewService entityViewServiceMock; + @Mock + private RuleEngineTelemetryService telemetryServiceMock; + + @BeforeEach + void setUp() throws TbNodeException { + node = new TbCopyAttributesToEntityViewNode(); + config = new EmptyNodeConfiguration().defaultConfiguration(); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, configuration); + } + + @Test + public void givenExistingAttributes_whenOnMsg_thenCopyAttributeValuesToView() { + EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + + TbMsg msg = TbMsg.newMsg( + TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + "{\"attribute1\": 100, \"attribute2\": \"value2\"}"); + + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + doAnswer(invocation -> { + FutureCallback callback = invocation.getArgument(4); + callback.onSuccess(null); + return null; + }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); + doAnswer(invocation -> { + TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + return newMsg; + }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + + node.onMsg(ctxMock, msg); + + verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + verify(ctxMock).ack(eq(msg)); + TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + } + + @Test + void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributeValuesFromView() { + EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + + TbMsg msg = TbMsg.newMsg( + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + "{\"attributes\": [\"attribute1\"]}"); + + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + doAnswer(invocation -> { + FutureCallback callback = invocation.getArgument(4); + callback.onSuccess(null); + return null; + }).when(telemetryServiceMock).deleteAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); + doAnswer(invocation -> { + TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + return newMsg; + }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + + node.onMsg(ctxMock, msg); + + verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); + verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + verify(ctxMock).ack(eq(msg)); + TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + } + + @Test + void givenAttributesDateOutOfStartDateAndEndDate_whenOnMsg_thenThrowsException() { + EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli()); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + + TbMsg msg = TbMsg.newMsg( + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + "{\"attributes\": [\"attribute1\"]}"); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + } + + @Test + void givenEmptyMetadata_whenOnMsg_thenThrowsException() { + TbMsg msg = TbMsg.newMsg( + ATTRIBUTES_UPDATED, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable throwable = captor.getValue(); + assertThat(throwable.getClass()).isEqualTo(IllegalArgumentException.class); + String expectedExceptionMessage = "java.lang.IllegalArgumentException: Message metadata is empty"; + assertThat(throwable.toString()).isEqualTo(expectedExceptionMessage); + } + + @ParameterizedTest + @EnumSource(TbMsgType.class) + public void givenUnsupportedMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { + TbMsg msg = TbMsg.newMsg( + msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + if (msg.isTypeOneOf(ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, + ACTIVITY_EVENT, INACTIVITY_EVENT, POST_ATTRIBUTES_REQUEST)) { + return; + } + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable throwable = captor.getValue(); + assertThat(throwable.getClass()).isEqualTo(IllegalArgumentException.class); + String expectedExceptionMessage = "java.lang.IllegalArgumentException: Unsupported msg type [" + msg.getType() + "]"; + assertThat(throwable.toString()).isEqualTo(expectedExceptionMessage); + } +} \ No newline at end of file From 5a3e16f28dddf3113564199a1a324c9dbde09045 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 10 Apr 2024 13:40:36 +0300 Subject: [PATCH 02/34] added test to check node when there are no matching attributes --- .../TbCopyAttributesToEntityViewNodeTest.java | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java index a08872ba02..042f768d79 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -56,6 +56,7 @@ import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; @@ -89,7 +90,7 @@ public class TbCopyAttributesToEntityViewNodeTest { } @Test - public void givenExistingAttributes_whenOnMsg_thenCopyAttributeValuesToView() { + public void givenExistingAttributes_whenOnMsg_thenCopyAttributesToView() { EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); EntityView entityView = new EntityView(entityViewId); entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); @@ -126,7 +127,7 @@ public class TbCopyAttributesToEntityViewNodeTest { } @Test - void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributeValuesFromView() { + public void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() { EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); EntityView entityView = new EntityView(entityViewId); entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); @@ -163,7 +164,69 @@ public class TbCopyAttributesToEntityViewNodeTest { } @Test - void givenAttributesDateOutOfStartDateAndEndDate_whenOnMsg_thenThrowsException() { + public void givenNonMatchedAttributesAndMsgTypeIsAttributesDeleted_whenOnMsg_thenNoAttributesDeleteFromView() { + EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + + TbMsg msg = TbMsg.newMsg( + TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + "{\"attributes\": [\"anotherAttribute\"]}"); + + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + + node.onMsg(ctxMock, msg); + + verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); + verify(ctxMock).ack(eq(msg)); + verify(ctxMock, never()).getTelemetryService(); + } + + @Test + public void givenNonMatchedAttributesAndMsgTypeIsPostAttributesRequest_whenOnMsg_thenCopyNoAttributesToView() { + EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + + TbMsg msg = TbMsg.newMsg( + TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + "{\"attribute2\": \"value2\"}"); + + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + doAnswer(invocation -> { + FutureCallback callback = invocation.getArgument(4); + callback.onSuccess(null); + return null; + }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); + doAnswer(invocation -> { + TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + return newMsg; + }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + + node.onMsg(ctxMock, msg); + + verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); + verify(ctxMock).ack(eq(msg)); + TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); + verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + } + + @Test + public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenThrowsException() { EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); EntityView entityView = new EntityView(entityViewId); entityView.setStartTimeMs(Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli()); @@ -187,7 +250,7 @@ public class TbCopyAttributesToEntityViewNodeTest { } @Test - void givenEmptyMetadata_whenOnMsg_thenThrowsException() { + public void givenEmptyMetadata_whenOnMsg_thenThrowsException() { TbMsg msg = TbMsg.newMsg( ATTRIBUTES_UPDATED, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); @@ -221,4 +284,4 @@ public class TbCopyAttributesToEntityViewNodeTest { String expectedExceptionMessage = "java.lang.IllegalArgumentException: Unsupported msg type [" + msg.getType() + "]"; assertThat(throwable.toString()).isEqualTo(expectedExceptionMessage); } -} \ No newline at end of file +} From 59d75bf2f131fe091d33f9e519c51a2fb8bfed64 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 11 Apr 2024 19:27:09 +0300 Subject: [PATCH 03/34] added tests for send rpc request and reply nodes --- .../engine/rpc/TbSendRPCReplyNodeTest.java | 91 +++++++-- .../engine/rpc/TbSendRPCRequestNodeTest.java | 184 ++++++++++++++++++ 2 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java index 454c1a9fe6..f628db07e6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -16,12 +16,14 @@ package org.thingsboard.rule.engine.rpc; import com.google.common.util.concurrent.SettableFuture; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +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.EnumSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.RuleEngineRpcService; @@ -29,7 +31,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; @@ -39,11 +43,14 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import java.util.UUID; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class TbSendRPCReplyNodeTest { private static final String DUMMY_SERVICE_ID = "testServiceId"; @@ -68,7 +75,7 @@ public class TbSendRPCReplyNodeTest { @Mock private ListeningExecutor listeningExecutor; - @Before + @BeforeEach public void setUp() throws TbNodeException { node = new TbSendRPCReplyNode(); TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); @@ -77,8 +84,7 @@ public class TbSendRPCReplyNodeTest { @Test public void sendReplyToTransport() { - Mockito.when(ctx.getRpcService()).thenReturn(rpcService); - + when(ctx.getRpcService()).thenReturn(rpcService); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(), TbMsgDataType.JSON, DUMMY_DATA, null, null); @@ -91,10 +97,10 @@ public class TbSendRPCReplyNodeTest { @Test public void sendReplyToEdgeQueue() { - Mockito.when(ctx.getTenantId()).thenReturn(tenantId); - Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); - Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create()); - Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor); + when(ctx.getTenantId()).thenReturn(tenantId); + when(ctx.getEdgeEventService()).thenReturn(edgeEventService); + when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create()); + when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor); TbMsgMetaData defaultMetadata = getDefaultMetadata(); defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString()); @@ -108,6 +114,65 @@ public class TbSendRPCReplyNodeTest { verify(rpcService, never()).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA); } + @ParameterizedTest + @EnumSource(EntityType.class) + public void testOriginatorEntityTypes(EntityType entityType) throws TbNodeException { + if (entityType == EntityType.DEVICE) return; + EntityId entityId = new EntityId() { + @Override + public UUID getId() { + return UUID.randomUUID(); + } + + @Override + public EntityType getEntityType() { + return entityType; + } + }; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + node.onMsg(ctx, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx).tellFailure(eq(msg), captor.capture()); + Throwable value = captor.getValue(); + assertThat(value.getClass()).isEqualTo(RuntimeException.class); + assertThat(value.getMessage()).isEqualTo("Message originator is not a device entity!"); + } + + @Test + public void testForAvailabilityOfMetadataAndDataValues2() { + //without requestId + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + verifyFailure(msg, "Request id is not present in the metadata!"); + + //without serviceId + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue("requestId", Integer.toString(DUMMY_REQUEST_ID)); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_JSON_OBJECT); + verifyFailure(msg, "Service id is not present in the metadata!"); + + //without sessionId + metadata.putValue("serviceId", DUMMY_SERVICE_ID); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_JSON_OBJECT); + verifyFailure(msg, "Session id is not present in the metadata!"); + + //with empty data + metadata.putValue("sessionId", DUMMY_SESSION_ID.toString()); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_STRING); + verifyFailure(msg, "Request body is empty!"); + } + + private void verifyFailure(TbMsg msg, String expectedErrorMessage) { + node.onMsg(ctx, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx).tellFailure(eq(msg), captor.capture()); + Throwable value = captor.getValue(); + assertThat(value.getClass()).isEqualTo(RuntimeException.class); + assertThat(value.getMessage()).isEqualTo(expectedErrorMessage); + } + private TbMsgMetaData getDefaultMetadata() { TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); TbMsgMetaData metadata = new TbMsgMetaData(); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java new file mode 100644 index 0000000000..0a2f0a0bf8 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java @@ -0,0 +1,184 @@ +/** + * 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.rpc; + +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.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; +import org.thingsboard.rule.engine.api.RuleEngineRpcService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.rpc.RpcError; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Optional; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TbSendRPCRequestNodeTest { + + private TbSendRPCRequestNode node; + private TbSendRpcRequestNodeConfiguration config; + + @Mock + private TbContext ctxMock; + @Mock + private RuleEngineRpcService rpcServiceMock; + + @BeforeEach + public void setUp() throws TbNodeException { + node = new TbSendRPCRequestNode(); + config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, configuration); + } + + @Test + public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { + DeviceId deviceId = new DeviceId(UUID.fromString("dda00a40-9d9c-4464-a759-488b9617319c")); + TenantId tenantId = new TenantId(UUID.fromString("81622599-afb3-4b52-9b47-f930f11ee963")); + String data = """ + { + "method": "setGpio", + "params": { + "pin": "23", + "value": 1 + } + } + """; + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, data); + TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); + when(ctxMock.getTenantId()).thenReturn(tenantId); + when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); + + doAnswer(invocation -> { + Consumer callback = invocation.getArgument(1); + RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); + when(rpcResponseMock.getError()).thenReturn(Optional.empty()); + when(rpcResponseMock.getResponse()).thenReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT)); + callback.accept(rpcResponseMock); + return null; + }).when(rpcServiceMock).sendRpcRequestToDevice(any(), any()); + + node.onMsg(ctxMock, msg); + + verify(ctxMock).enqueueForTellNext(eq(outMsg), eq(TbNodeConnectionType.SUCCESS)); + verify(ctxMock).ack(eq(msg)); + } + + @Test + public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { + DeviceId deviceId = new DeviceId(UUID.fromString("dda00a40-9d9c-4464-a759-488b9617319c")); + TenantId tenantId = new TenantId(UUID.fromString("81622599-afb3-4b52-9b47-f930f11ee963")); + String data = """ + { + "method": "setGpio", + "params": { + "pin": "23", + "value": 1 + } + } + """; + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, data); + TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); + when(ctxMock.getTenantId()).thenReturn(tenantId); + when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); + + doAnswer(invocation -> { + Consumer callback = invocation.getArgument(1); + RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); + when(rpcResponseMock.getError()).thenReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION)); + callback.accept(rpcResponseMock); + return null; + }).when(rpcServiceMock).sendRpcRequestToDevice(any(), any()); + + node.onMsg(ctxMock, msg); + + verify(ctxMock).enqueueForTellFailure(eq(outMsg), eq("NO_ACTIVE_CONNECTION")); + verify(ctxMock).ack(eq(msg)); + } + + @ParameterizedTest + @EnumSource(EntityType.class) + public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { + if (entityType == EntityType.DEVICE) return; + EntityId entityId = new EntityId() { + @Override + public UUID getId() { + return UUID.randomUUID(); + } + + @Override + public EntityType getEntityType() { + return entityType; + } + }; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable value = captor.getValue(); + assertThat(value.getClass()).isEqualTo(RuntimeException.class); + assertThat(value.getMessage()).isEqualTo("Message originator is not a device entity!"); + } + + @ParameterizedTest + @ValueSource(strings = {"method", "params"}) + public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable value = captor.getValue(); + assertThat(value.getClass()).isEqualTo(RuntimeException.class); + assertThat(value.getMessage()).isEqualTo( + key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!"); + } +} From ae273114adce5dc04af29e884f8f4ddb8aa37362 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 12 Apr 2024 13:50:07 +0300 Subject: [PATCH 04/34] added tests for save timeseries node --- .../telemetry/TbMsgTimeseriesNodeTest.java | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java new file mode 100644 index 0000000000..f3d2486027 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -0,0 +1,169 @@ +/** + * 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.telemetry; + +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.EnumSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TbMsgTimeseriesNodeTest { + + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("e5095e9a-04f4-44c9-b443-1cf1b97d3384")); + private final TenantId TENANT_ID = new TenantId(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377")); + + private TbMsgTimeseriesNode node; + private TbMsgTimeseriesNodeConfiguration config; + private long tenantProfileDefaultStorageTtl; + + @Mock + private TbContext ctxMock; + @Mock + private RuleEngineTelemetryService telemetryServiceMock; + + @BeforeEach + public void setUp() throws TbNodeException { + node = new TbMsgTimeseriesNode(); + config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration(); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + TenantProfile tenantProfile = new TenantProfile(new TenantProfileId(UUID.fromString("8c45d0fe-d437-40e9-8c31-b695b315bf40"))); + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); + tenantProfileData.setConfiguration(tenantProfileConfiguration); + tenantProfile.setProfileData(tenantProfileData); + when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); + doAnswer(invocation -> { + invocation.getArgument(0); + return null; + }).when(ctxMock).addTenantProfileListener(any()); + node.init(ctxMock, configuration); + tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(tenantProfileConfiguration.getDefaultStorageTtlDays()); + } + + @ParameterizedTest + @EnumSource(TbMsgType.class) + void givenUnsupportedMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { + TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + + if (TbMsgType.POST_TELEMETRY_REQUEST.equals(msgType)) { + return; + } + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable throwable = captor.getValue(); + assertThat(throwable).isInstanceOf(IllegalArgumentException.class); + assertThat(throwable.getMessage()).isEqualTo("Unsupported msg type: " + msgType); + } + + @Test + void givenEmptyMsgData_whenOnMsg_thenTellFailure() { + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable throwable = captor.getValue(); + assertThat(throwable).isInstanceOf(IllegalArgumentException.class); + assertThat(throwable.getMessage()).isEqualTo("Msg body is empty: " + msg.getData()); + } + + @Test + void givenSkipLatestPersistenceIsFalse_whenOnMsg_thenSaveTimeseries() { + String data = """ + { + "temp": 45, + "humidity": 77 + } + """; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); + + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + doAnswer(invocation -> { + TelemetryNodeCallback callback = invocation.getArgument(5); + callback.onSuccess(null); + return null; + }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(), anyList(), anyLong(), any()); + + node.onMsg(ctxMock, msg); + + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(null), eq(DEVICE_ID), anyList(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + verify(ctxMock).tellSuccess(eq(msg)); + } + + @Test + void givenSkipLatestPersistenceIsTrue_whenOnMsg_thenSaveTimeseries() throws TbNodeException { + config.setSkipLatestPersistence(true); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, configuration); + + String data = """ + { + "temp": 45, + "humidity": 77 + } + """; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); + + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + doAnswer(invocation -> { + TelemetryNodeCallback callback = invocation.getArgument(5); + callback.onSuccess(null); + return null; + }).when(telemetryServiceMock).saveWithoutLatestAndNotify(any(), any(), any(), anyList(), anyLong(), any()); + + node.onMsg(ctxMock, msg); + + verify(telemetryServiceMock).saveWithoutLatestAndNotify(eq(TENANT_ID), eq(null), eq(DEVICE_ID), anyList(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + verify(ctxMock).tellSuccess(eq(msg)); + } +} From 2a18f0c2b655b883744cde2995cf4da912fa5696 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 15 Apr 2024 18:36:13 +0300 Subject: [PATCH 05/34] moved creation of entity view to a separate method --- .../TbCopyAttributesToEntityViewNodeTest.java | 90 ++++++++----------- 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java index 042f768d79..952cc21c14 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.msg.TbMsg; @@ -54,7 +55,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -70,6 +70,8 @@ public class TbCopyAttributesToEntityViewNodeTest { private final TenantId TENANT_ID = new TenantId(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1d453dc9-9333-476a-a51f-093cf2176e59")); + private final long FROM_DATE = Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(); + private final long TO_DATE = Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli(); private TbCopyAttributesToEntityViewNode node; private EmptyNodeConfiguration config; @@ -91,12 +93,8 @@ public class TbCopyAttributesToEntityViewNodeTest { @Test public void givenExistingAttributes_whenOnMsg_thenCopyAttributesToView() { - EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + EntityView entityView = getEntityView(); + EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), @@ -112,28 +110,21 @@ public class TbCopyAttributesToEntityViewNodeTest { callback.onSuccess(null); return null; }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); - doAnswer(invocation -> { - TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - return newMsg; - }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId()); + doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); - TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @Test public void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() { - EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + EntityView entityView = getEntityView(); + EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), @@ -149,28 +140,20 @@ public class TbCopyAttributesToEntityViewNodeTest { callback.onSuccess(null); return null; }).when(telemetryServiceMock).deleteAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); - doAnswer(invocation -> { - TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - return newMsg; - }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId()); + doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); - TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @Test public void givenNonMatchedAttributesAndMsgTypeIsAttributesDeleted_whenOnMsg_thenNoAttributesDeleteFromView() { - EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + EntityView entityView = getEntityView(); TbMsg msg = TbMsg.newMsg( TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), @@ -190,12 +173,8 @@ public class TbCopyAttributesToEntityViewNodeTest { @Test public void givenNonMatchedAttributesAndMsgTypeIsPostAttributesRequest_whenOnMsg_thenCopyNoAttributesToView() { - EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + EntityView entityView = getEntityView(); + EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), @@ -211,28 +190,23 @@ public class TbCopyAttributesToEntityViewNodeTest { callback.onSuccess(null); return null; }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); - doAnswer(invocation -> { - TbMsg newMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - return newMsg; - }).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); + TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId()); + doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); - TbMsg expectedNewMsg = TbMsg.newMsg(msg.getQueueName(), msg.getType(), entityViewId, msg.getCustomerId(), msg.getMetaData(), msg.getData()); - verify(ctxMock).enqueueForTellNext(refEq(expectedNewMsg, "ts", "id", "ctx"), eq("Success")); + verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @Test - public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenThrowsException() { + public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenDoNothing() { EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); EntityView entityView = new EntityView(entityViewId); entityView.setStartTimeMs(Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli()); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + entityView.setEndTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -245,8 +219,8 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); + verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); + verify(ctxMock).ack(eq(msg)); } @Test @@ -259,9 +233,7 @@ public class TbCopyAttributesToEntityViewNodeTest { ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock).tellFailure(eq(msg), captor.capture()); Throwable throwable = captor.getValue(); - assertThat(throwable.getClass()).isEqualTo(IllegalArgumentException.class); - String expectedExceptionMessage = "java.lang.IllegalArgumentException: Message metadata is empty"; - assertThat(throwable.toString()).isEqualTo(expectedExceptionMessage); + assertThat(throwable).isInstanceOf(IllegalArgumentException.class).hasMessage("Message metadata is empty"); } @ParameterizedTest @@ -280,8 +252,16 @@ public class TbCopyAttributesToEntityViewNodeTest { ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock).tellFailure(eq(msg), captor.capture()); Throwable throwable = captor.getValue(); - assertThat(throwable.getClass()).isEqualTo(IllegalArgumentException.class); - String expectedExceptionMessage = "java.lang.IllegalArgumentException: Unsupported msg type [" + msg.getType() + "]"; - assertThat(throwable.toString()).isEqualTo(expectedExceptionMessage); + assertThat(throwable).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type [" + msgType + "]"); + } + + private EntityView getEntityView() { + EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); + EntityView entityView = new EntityView(entityViewId); + entityView.setStartTimeMs(FROM_DATE); + entityView.setEndTimeMs(TO_DATE); + AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); + entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + return entityView; } } From 2f452e2193b08b76a3f2a9aea846bd111a69d28c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 6 May 2024 09:27:39 +0300 Subject: [PATCH 06/34] used AttributesScope value names instead of strings --- .../action/TbCopyAttributesToEntityViewNodeTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java index 952cc21c14..2f7d681630 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -97,7 +97,7 @@ public class TbCopyAttributesToEntityViewNodeTest { EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( - TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attribute1\": 100, \"attribute2\": \"value2\"}"); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); @@ -127,7 +127,7 @@ public class TbCopyAttributesToEntityViewNodeTest { EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( - ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attributes\": [\"attribute1\"]}"); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); @@ -156,7 +156,7 @@ public class TbCopyAttributesToEntityViewNodeTest { EntityView entityView = getEntityView(); TbMsg msg = TbMsg.newMsg( - TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attributes\": [\"anotherAttribute\"]}"); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); @@ -177,7 +177,7 @@ public class TbCopyAttributesToEntityViewNodeTest { EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( - TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attribute2\": \"value2\"}"); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); @@ -214,7 +214,7 @@ public class TbCopyAttributesToEntityViewNodeTest { .thenReturn(Futures.immediateFuture(List.of(entityView))); TbMsg msg = TbMsg.newMsg( - ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", "CLIENT_SCOPE")), + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attributes\": [\"attribute1\"]}"); node.onMsg(ctxMock, msg); From 15defc34c3e50fe4b8becf41f5e02299511ceb6b Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 09:46:38 +0300 Subject: [PATCH 07/34] removed deprecated useRedisQueueForMessagePersistence parameter from config --- .../rule/engine/rest/TbRestApiCallNode.java | 10 +-- .../rest/TbRestApiCallNodeConfiguration.java | 2 - .../engine/rest/TbRestApiCallNodeTest.java | 64 +++++++++++++------ 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index f0cb102778..4b34131975 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.msg.TbMsg; type = ComponentType.EXTERNAL, name = "rest api call", configClazz = TbRestApiCallNodeConfiguration.class, - version = 1, + version = 2, nodeDescription = "Invoke REST API calls to external REST server", nodeDetails = "Will invoke REST API call GET | POST | PUT | DELETE to external REST server. " + "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + @@ -58,9 +58,6 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { super.init(ctx); TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); - if (config.isUseRedisQueueForMsgPersistence()) { - log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId()); - } } @Override @@ -88,6 +85,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { ((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue()); ((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES); } + case 1: + if (oldConfiguration.has("useRedisQueueForMsgPersistence")) { + hasChanges = true; + ((ObjectNode) oldConfiguration).remove("useRedisQueueForMsgPersistence"); + } break; default: break; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 7f159bd6a9..7d2ff7167d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -36,7 +36,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration upgrade = node.upgrade(0, configJson); - Assertions.assertTrue(upgrade.getFirst()); - Assertions.assertTrue(JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()).isParseToPlainText()); + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + // config for version 2 with upgrade from version 0 + Arguments.of(0, + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," + + "\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," + + "\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," + + "\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," + + "\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," + + "\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":false}", + true, + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + + "\"trimQueue\": null,\"maxQueueSize\": null,\"credentials\": {\"type\": \"anonymous\"}}"), + // config for version 2 with upgrade from version 1 + Arguments.of(1, + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + + "\"useRedisQueueForMsgPersistence\": false,\"trimQueue\": null,\"maxQueueSize\": null," + + "\"credentials\": {\"type\": \"anonymous\"}}", + true, + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + + "\"trimQueue\": null,\"maxQueueSize\": null,\"credentials\": {\"type\": \"anonymous\"}}") + ); + } + + @Override + protected TbNode getTestNode() { + return restNode; } } From 7de6bb764783de2f86d52d4a65c2d5ba99a42f0d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 11:08:23 +0300 Subject: [PATCH 08/34] removed trimQueue and maxQueueSize parameters --- .../org/thingsboard/rule/engine/rest/TbRestApiCallNode.java | 4 +++- .../thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 4b34131975..81de1ded43 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; +import java.util.List; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -88,7 +90,7 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { case 1: if (oldConfiguration.has("useRedisQueueForMsgPersistence")) { hasChanges = true; - ((ObjectNode) oldConfiguration).remove("useRedisQueueForMsgPersistence"); + ((ObjectNode) oldConfiguration).remove(List.of("useRedisQueueForMsgPersistence", "trimQueue", "maxQueueSize")); } break; default: diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 6feaf7d5d9..635bfac356 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -233,7 +233,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + - "\"trimQueue\": null,\"maxQueueSize\": null,\"credentials\": {\"type\": \"anonymous\"}}"), + "\"credentials\": {\"type\": \"anonymous\"}}"), // config for version 2 with upgrade from version 1 Arguments.of(1, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + @@ -249,7 +249,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + - "\"trimQueue\": null,\"maxQueueSize\": null,\"credentials\": {\"type\": \"anonymous\"}}") + "\"credentials\": {\"type\": \"anonymous\"}}") ); } From de7b6dddea10477a49d7967d44a6e435df67d285 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 11:49:07 +0300 Subject: [PATCH 09/34] refactored tests --- .../TbCopyAttributesToEntityViewNodeTest.java | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java index 2f7d681630..76b5b0f446 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -68,13 +68,10 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R @ExtendWith(MockitoExtension.class) public class TbCopyAttributesToEntityViewNodeTest { - private final TenantId TENANT_ID = new TenantId(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533")); + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1d453dc9-9333-476a-a51f-093cf2176e59")); - private final long FROM_DATE = Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(); - private final long TO_DATE = Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli(); private TbCopyAttributesToEntityViewNode node; - private EmptyNodeConfiguration config; @Mock private TbContext ctxMock; @@ -86,7 +83,7 @@ public class TbCopyAttributesToEntityViewNodeTest { @BeforeEach void setUp() throws TbNodeException { node = new TbCopyAttributesToEntityViewNode(); - config = new EmptyNodeConfiguration().defaultConfiguration(); + var config = new EmptyNodeConfiguration().defaultConfiguration(); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, configuration); } @@ -94,16 +91,12 @@ public class TbCopyAttributesToEntityViewNodeTest { @Test public void givenExistingAttributes_whenOnMsg_thenCopyAttributesToView() { EntityView entityView = getEntityView(); - EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attribute1\": 100, \"attribute2\": \"value2\"}"); - when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) - .thenReturn(Futures.immediateFuture(List.of(entityView))); + mockEntityViewService(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -116,7 +109,7 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @@ -124,16 +117,12 @@ public class TbCopyAttributesToEntityViewNodeTest { @Test public void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() { EntityView entityView = getEntityView(); - EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attributes\": [\"attribute1\"]}"); - when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) - .thenReturn(Futures.immediateFuture(List.of(entityView))); + mockEntityViewService(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -146,7 +135,7 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @@ -159,10 +148,7 @@ public class TbCopyAttributesToEntityViewNodeTest { TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attributes\": [\"anotherAttribute\"]}"); - when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) - .thenReturn(Futures.immediateFuture(List.of(entityView))); + mockEntityViewService(entityView); node.onMsg(ctxMock, msg); @@ -174,16 +160,12 @@ public class TbCopyAttributesToEntityViewNodeTest { @Test public void givenNonMatchedAttributesAndMsgTypeIsPostAttributesRequest_whenOnMsg_thenCopyNoAttributesToView() { EntityView entityView = getEntityView(); - EntityViewId entityViewId = entityView.getId(); TbMsg msg = TbMsg.newMsg( TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), "{\"attribute2\": \"value2\"}"); - when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) - .thenReturn(Futures.immediateFuture(List.of(entityView))); + mockEntityViewService(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -196,22 +178,19 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityViewId), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); } @Test public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenDoNothing() { - EntityViewId entityViewId = EntityViewId.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925"); + EntityViewId entityViewId = new EntityViewId(UUID.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925")); EntityView entityView = new EntityView(entityViewId); entityView.setStartTimeMs(Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli()); entityView.setEndTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) - .thenReturn(Futures.immediateFuture(List.of(entityView))); + mockEntityViewService(entityView); TbMsg msg = TbMsg.newMsg( ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), @@ -230,10 +209,9 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable throwable = captor.getValue(); - assertThat(throwable).isInstanceOf(IllegalArgumentException.class).hasMessage("Message metadata is empty"); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Message metadata is empty"); } @ParameterizedTest @@ -249,19 +227,25 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable throwable = captor.getValue(); - assertThat(throwable).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type [" + msgType + "]"); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type [" + msgType + "]"); } private EntityView getEntityView() { - EntityViewId entityViewId = EntityViewId.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8"); + EntityViewId entityViewId = new EntityViewId(UUID.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8")); EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(FROM_DATE); - entityView.setEndTimeMs(TO_DATE); + entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); + entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); return entityView; } + + private void mockEntityViewService(EntityView entityView) { + when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) + .thenReturn(Futures.immediateFuture(List.of(entityView))); + } } From ba4fb71486d5082a37cf75c3a9d64d730a4128cf Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 15:34:43 +0300 Subject: [PATCH 10/34] refactored tests --- .../engine/rpc/TbSendRPCReplyNodeTest.java | 65 ++++++----- .../engine/rpc/TbSendRPCRequestNodeTest.java | 101 +++++++++--------- 2 files changed, 79 insertions(+), 87 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java index f628db07e6..17d53802a1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -20,7 +20,9 @@ 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.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -41,7 +43,9 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.edge.EdgeEventService; +import java.util.Map; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -116,7 +120,7 @@ public class TbSendRPCReplyNodeTest { @ParameterizedTest @EnumSource(EntityType.class) - public void testOriginatorEntityTypes(EntityType entityType) throws TbNodeException { + public void testOriginatorEntityTypes(EntityType entityType) { if (entityType == EntityType.DEVICE) return; EntityId entityId = new EntityId() { @Override @@ -133,44 +137,37 @@ public class TbSendRPCReplyNodeTest { node.onMsg(ctx, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctx).tellFailure(eq(msg), captor.capture()); - Throwable value = captor.getValue(); - assertThat(value.getClass()).isEqualTo(RuntimeException.class); - assertThat(value.getMessage()).isEqualTo("Message originator is not a device entity!"); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) + .hasMessage("Message originator is not a device entity!"); } - @Test - public void testForAvailabilityOfMetadataAndDataValues2() { - //without requestId - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); - verifyFailure(msg, "Request id is not present in the metadata!"); - - //without serviceId - TbMsgMetaData metadata = new TbMsgMetaData(); - metadata.putValue("requestId", Integer.toString(DUMMY_REQUEST_ID)); - msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_JSON_OBJECT); - verifyFailure(msg, "Service id is not present in the metadata!"); - - //without sessionId - metadata.putValue("serviceId", DUMMY_SERVICE_ID); - msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_JSON_OBJECT); - verifyFailure(msg, "Session id is not present in the metadata!"); - - //with empty data - metadata.putValue("sessionId", DUMMY_SESSION_ID.toString()); - msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metadata, TbMsg.EMPTY_STRING); - verifyFailure(msg, "Request body is empty!"); - } + @ParameterizedTest + @MethodSource + public void testForAvailabilityOfMetadataAndDataValues(TbMsgMetaData metaData, String errorMsg) { + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metaData, TbMsg.EMPTY_STRING); - private void verifyFailure(TbMsg msg, String expectedErrorMessage) { node.onMsg(ctx, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctx).tellFailure(eq(msg), captor.capture()); - Throwable value = captor.getValue(); - assertThat(value.getClass()).isEqualTo(RuntimeException.class); - assertThat(value.getMessage()).isEqualTo(expectedErrorMessage); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); + } + + private static Stream testForAvailabilityOfMetadataAndDataValues() { + return Stream.of( + Arguments.of(TbMsgMetaData.EMPTY, "Request id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of( + "requestId", Integer.toString(DUMMY_REQUEST_ID))), "Service id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of( + "requestId", Integer.toString(DUMMY_REQUEST_ID), + "serviceId", DUMMY_SERVICE_ID)), "Session id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of( + "requestId", Integer.toString(DUMMY_REQUEST_ID), + "serviceId", DUMMY_SERVICE_ID, "sessionId", + DUMMY_SESSION_ID.toString())), "Request body is empty!") + ); } private TbMsgMetaData getDefaultMetadata() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java index 0a2f0a0bf8..969aecb4d5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java @@ -25,6 +25,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; @@ -54,10 +55,12 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbSendRPCRequestNodeTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d3a47f8b-d863-4c1f-b6f0-2c946b43f21c")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b052ae59-b9b4-47e8-ac71-39e7124bbd66")); private TbSendRPCRequestNode node; - private TbSendRpcRequestNodeConfiguration config; - + @Mock private TbContext ctxMock; @Mock @@ -66,50 +69,28 @@ public class TbSendRPCRequestNodeTest { @BeforeEach public void setUp() throws TbNodeException { node = new TbSendRPCRequestNode(); - config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); + var config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, configuration); } @Test public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { - DeviceId deviceId = new DeviceId(UUID.fromString("dda00a40-9d9c-4464-a759-488b9617319c")); - TenantId tenantId = new TenantId(UUID.fromString("81622599-afb3-4b52-9b47-f930f11ee963")); - String data = """ - { - "method": "setGpio", - "params": { - "pin": "23", - "value": 1 - } - } - """; - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, data); - TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); - when(ctxMock.getTenantId()).thenReturn(tenantId); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + // TODO: replace deprecated method newMsg() when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); - doAnswer(invocation -> { - Consumer callback = invocation.getArgument(1); + Consumer consumer = invocation.getArgument(1); RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); when(rpcResponseMock.getError()).thenReturn(Optional.empty()); when(rpcResponseMock.getResponse()).thenReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT)); - callback.accept(rpcResponseMock); + consumer.accept(rpcResponseMock); return null; - }).when(rpcServiceMock).sendRpcRequestToDevice(any(), any()); - - node.onMsg(ctxMock, msg); + }).when(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); - verify(ctxMock).enqueueForTellNext(eq(outMsg), eq(TbNodeConnectionType.SUCCESS)); - verify(ctxMock).ack(eq(msg)); - } - - @Test - public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { - DeviceId deviceId = new DeviceId(UUID.fromString("dda00a40-9d9c-4464-a759-488b9617319c")); - TenantId tenantId = new TenantId(UUID.fromString("81622599-afb3-4b52-9b47-f930f11ee963")); String data = """ { "method": "setGpio", @@ -119,24 +100,42 @@ public class TbSendRPCRequestNodeTest { } } """; - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, data); - TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, data); + node.onMsg(ctxMock, msg); + + verify(ctxMock).enqueueForTellNext(eq(outMsg), eq(TbNodeConnectionType.SUCCESS)); + verify(ctxMock).ack(eq(msg)); + } + + @Test + public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { + TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); - when(ctxMock.getTenantId()).thenReturn(tenantId); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + // TODO: replace deprecated method newMsg() when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); - doAnswer(invocation -> { - Consumer callback = invocation.getArgument(1); + Consumer consumer = invocation.getArgument(1); RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); when(rpcResponseMock.getError()).thenReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION)); - callback.accept(rpcResponseMock); + consumer.accept(rpcResponseMock); return null; - }).when(rpcServiceMock).sendRpcRequestToDevice(any(), any()); + }).when(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); + String data = """ + { + "method": "setGpio", + "params": { + "pin": "23", + "value": 1 + } + } + """; + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, data); node.onMsg(ctxMock, msg); - verify(ctxMock).enqueueForTellFailure(eq(outMsg), eq("NO_ACTIVE_CONNECTION")); + verify(ctxMock).enqueueForTellFailure(eq(outMsg), eq(RpcError.NO_ACTIVE_CONNECTION.name())); verify(ctxMock).ack(eq(msg)); } @@ -155,30 +154,26 @@ public class TbSendRPCRequestNodeTest { return entityType; } }; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); - node.onMsg(ctxMock, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable value = captor.getValue(); - assertThat(value.getClass()).isEqualTo(RuntimeException.class); - assertThat(value.getMessage()).isEqualTo("Message originator is not a device entity!"); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) + .hasMessage("Message originator is not a device entity!"); } @ParameterizedTest @ValueSource(strings = {"method", "params"}) public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); node.onMsg(ctxMock, msg); - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable value = captor.getValue(); - assertThat(value.getClass()).isEqualTo(RuntimeException.class); - assertThat(value.getMessage()).isEqualTo( - key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!"); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) + .hasMessage(key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!"); } } From 4e7170090f8549b9fe63929e747d19d0486bfa47 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 17:50:09 +0300 Subject: [PATCH 11/34] refactored tests to cover more test cases --- .../TbCopyAttributesToEntityViewNodeTest.java | 138 ++++++++++-------- 1 file changed, 80 insertions(+), 58 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java index 76b5b0f446..4166c05a7a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java @@ -32,10 +32,12 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.objects.AttributesEntityView; @@ -56,8 +58,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; @@ -70,6 +72,18 @@ public class TbCopyAttributesToEntityViewNodeTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1d453dc9-9333-476a-a51f-093cf2176e59")); + private final EntityViewId ENTITY_VIEW_ID = new EntityViewId(UUID.fromString("65636806-453d-4bb4-b513-92b833970753")); + + private final AttributesEntityView CLIENT_ATTRIBUTES = new AttributesEntityView(List.of("clientAttribute1"), Collections.emptyList(), Collections.emptyList()); + private final AttributesEntityView SERVER_ATTRIBUTES = new AttributesEntityView(Collections.emptyList(), List.of("serverAttribute1"), Collections.emptyList()); + private final AttributesEntityView SHARED_ATTRIBUTES = new AttributesEntityView(Collections.emptyList(), Collections.emptyList(), List.of("sharedAttribute1")); + + private final TelemetryEntityView CLIENT_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), CLIENT_ATTRIBUTES); + private final TelemetryEntityView SERVER_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), SERVER_ATTRIBUTES); + private final TelemetryEntityView SHARED_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), SHARED_ATTRIBUTES); + + private final long ENTITY_VIEW_START_TS = Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(); + private final long ENTITY_VIEW_END_TS = Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli(); private TbCopyAttributesToEntityViewNode node; @@ -89,14 +103,14 @@ public class TbCopyAttributesToEntityViewNodeTest { } @Test - public void givenExistingAttributes_whenOnMsg_thenCopyAttributesToView() { - EntityView entityView = getEntityView(); + public void givenExistingClientAttributes_whenOnMsg_thenCopyAttributesToView() { + EntityView entityView = getEntityView(CLIENT_TELEMETRY_ENTITY_VIEW); - TbMsg msg = TbMsg.newMsg( - TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), - "{\"attribute1\": 100, \"attribute2\": \"value2\"}"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, + new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())), + "{\"clientAttribute1\": 100, \"clientAttribute2\": \"value2\"}"); - mockEntityViewService(entityView); + mockEntityViewLookup(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -104,25 +118,33 @@ public class TbCopyAttributesToEntityViewNodeTest { return null; }).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class)); TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId()); + // TODO: use newMsg() with any(TbMsgType.class), replace in other tests as well. doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any()); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + ArgumentCaptor> filteredAttributesCaptor = ArgumentCaptor.forClass(List.class); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.CLIENT_SCOPE), + filteredAttributesCaptor.capture(), any(FutureCallback.class)); + List filteredAttributesCaptorValue = filteredAttributesCaptor.getValue(); + assertThat(filteredAttributesCaptorValue.size()).isEqualTo(1); + assertThat(filteredAttributesCaptorValue.get(0).getKey()).isEqualTo("clientAttribute1"); + assertThat(filteredAttributesCaptorValue.get(0).getValue()).isEqualTo(100L); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); + verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock); } @Test - public void givenExistingAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() { - EntityView entityView = getEntityView(); + public void givenExistingServerAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() { + EntityView entityView = getEntityView(SERVER_TELEMETRY_ENTITY_VIEW); TbMsg msg = TbMsg.newMsg( - ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), - "{\"attributes\": [\"attribute1\"]}"); + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())), + "{\"attributes\": [\"serverAttribute1\"]}"); - mockEntityViewService(entityView); + mockEntityViewLookup(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -135,37 +157,42 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), anyList(), any(FutureCallback.class)); + ArgumentCaptor> filteredAttributesCaptor = ArgumentCaptor.forClass(List.class); + verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.SERVER_SCOPE), filteredAttributesCaptor.capture(), any(FutureCallback.class)); + List filteredAttributesCaptorValue = filteredAttributesCaptor.getValue(); + assertThat(filteredAttributesCaptorValue.size()).isEqualTo(1); + assertThat(filteredAttributesCaptorValue.get(0)).isEqualTo("serverAttribute1"); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); + verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock); } @Test - public void givenNonMatchedAttributesAndMsgTypeIsAttributesDeleted_whenOnMsg_thenNoAttributesDeleteFromView() { - EntityView entityView = getEntityView(); + public void givenNonMatchedSharedAttributesAndMsgTypeIsAttributesDeleted_whenOnMsg_thenNoAttributesDeleteFromView() { + EntityView entityView = getEntityView(SHARED_TELEMETRY_ENTITY_VIEW); TbMsg msg = TbMsg.newMsg( - TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), + TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SHARED_SCOPE.name())), "{\"attributes\": [\"anotherAttribute\"]}"); - mockEntityViewService(entityView); + mockEntityViewLookup(entityView); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); verify(ctxMock).ack(eq(msg)); - verify(ctxMock, never()).getTelemetryService(); + verifyNoMoreInteractions(ctxMock, entityViewServiceMock); } @Test public void givenNonMatchedAttributesAndMsgTypeIsPostAttributesRequest_whenOnMsg_thenCopyNoAttributesToView() { - EntityView entityView = getEntityView(); + EntityView entityView = getEntityView(CLIENT_TELEMETRY_ENTITY_VIEW); TbMsg msg = TbMsg.newMsg( - TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), - "{\"attribute2\": \"value2\"}"); + TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())), + "{\"clientAttribute2\": \"value2\"}"); - mockEntityViewService(entityView); + mockEntityViewLookup(entityView); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); doAnswer(invocation -> { FutureCallback callback = invocation.getArgument(4); @@ -178,71 +205,66 @@ public class TbCopyAttributesToEntityViewNodeTest { node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); - verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(entityView.getId()), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class)); verify(ctxMock).ack(eq(msg)); verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS)); + verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock); } @Test public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenDoNothing() { - EntityViewId entityViewId = new EntityViewId(UUID.fromString("d117f1a4-24ea-4fdd-b94e-5a472e99d925")); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - - mockEntityViewService(entityView); + EntityView entityView = getEntityView( + SERVER_TELEMETRY_ENTITY_VIEW, + Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli(), + Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli() + ); + mockEntityViewLookup(entityView); TbMsg msg = TbMsg.newMsg( - ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of("scope", AttributeScope.CLIENT_SCOPE.name())), - "{\"attributes\": [\"attribute1\"]}"); - + ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())), + "{\"attributes\": [\"serverAttribute1\"]}"); node.onMsg(ctxMock, msg); verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID)); verify(ctxMock).ack(eq(msg)); + verifyNoMoreInteractions(ctxMock, entityViewServiceMock); } - @Test - public void givenEmptyMetadata_whenOnMsg_thenThrowsException() { - TbMsg msg = TbMsg.newMsg( - ATTRIBUTES_UPDATED, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + @ParameterizedTest + @EnumSource(TbMsgType.class) + public void givenMsgTypeAndEmptyMetadata_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) { + TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); - assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Message metadata is empty"); - } - - @ParameterizedTest - @EnumSource(TbMsgType.class) - public void givenUnsupportedMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { - TbMsg msg = TbMsg.newMsg( - msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); if (msg.isTypeOneOf(ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, ACTIVITY_EVENT, INACTIVITY_EVENT, POST_ATTRIBUTES_REQUEST)) { + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Message metadata is empty"); return; } + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported msg type [" + msgType + "]"); - node.onMsg(ctxMock, msg); - - ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); - assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type [" + msgType + "]"); + verifyNoMoreInteractions(ctxMock); } - private EntityView getEntityView() { - EntityViewId entityViewId = new EntityViewId(UUID.fromString("a2109747-d1f4-475a-baaa-55f5d4897ad8")); - EntityView entityView = new EntityView(entityViewId); - entityView.setStartTimeMs(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()); - entityView.setEndTimeMs(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - AttributesEntityView attributes = new AttributesEntityView(List.of("attribute1"), Collections.emptyList(), Collections.emptyList()); - entityView.setKeys(new TelemetryEntityView(Collections.emptyList(), attributes)); + private EntityView getEntityView(TelemetryEntityView attributesEntityView, long startTimeMs, long endTimeMs) { + EntityView entityView = new EntityView(ENTITY_VIEW_ID); + entityView.setStartTimeMs(startTimeMs); + entityView.setEndTimeMs(endTimeMs); + entityView.setKeys(attributesEntityView); return entityView; } - private void mockEntityViewService(EntityView entityView) { + private EntityView getEntityView(TelemetryEntityView attributesEntityView) { + return getEntityView(attributesEntityView, ENTITY_VIEW_START_TS, ENTITY_VIEW_END_TS); + } + + private void mockEntityViewLookup(EntityView entityView) { when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any())) From 6339f0ce633509bed1ae7805628e714c4c3deab5 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 28 May 2024 18:48:56 +0300 Subject: [PATCH 12/34] refactored tests --- .../telemetry/TbMsgTimeseriesNodeTest.java | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java index f3d2486027..d4bdcae878 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.telemetry; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,12 +33,14 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -46,15 +49,17 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbMsgTimeseriesNodeTest { + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("e5095e9a-04f4-44c9-b443-1cf1b97d3384")); - private final TenantId TENANT_ID = new TenantId(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377")); private TbMsgTimeseriesNode node; private TbMsgTimeseriesNodeConfiguration config; @@ -70,9 +75,9 @@ public class TbMsgTimeseriesNodeTest { node = new TbMsgTimeseriesNode(); config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration(); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - TenantProfile tenantProfile = new TenantProfile(new TenantProfileId(UUID.fromString("8c45d0fe-d437-40e9-8c31-b695b315bf40"))); - TenantProfileData tenantProfileData = new TenantProfileData(); - DefaultTenantProfileConfiguration tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); + var tenantProfile = new TenantProfile(new TenantProfileId(UUID.fromString("8c45d0fe-d437-40e9-8c31-b695b315bf40"))); + var tenantProfileData = new TenantProfileData(); + var tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); tenantProfileData.setConfiguration(tenantProfileConfiguration); tenantProfile.setProfileData(tenantProfileData); when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); @@ -86,33 +91,18 @@ public class TbMsgTimeseriesNodeTest { @ParameterizedTest @EnumSource(TbMsgType.class) - void givenUnsupportedMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { + void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) { TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + node.onMsg(ctxMock, msg); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); if (TbMsgType.POST_TELEMETRY_REQUEST.equals(msgType)) { + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Msg body is empty: " + msg.getData()); return; } - - node.onMsg(ctxMock, msg); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable throwable = captor.getValue(); - assertThat(throwable).isInstanceOf(IllegalArgumentException.class); - assertThat(throwable.getMessage()).isEqualTo("Unsupported msg type: " + msgType); - } - - @Test - void givenEmptyMsgData_whenOnMsg_thenTellFailure() { - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); - - node.onMsg(ctxMock, msg); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), captor.capture()); - Throwable throwable = captor.getValue(); - assertThat(throwable).isInstanceOf(IllegalArgumentException.class); - assertThat(throwable.getMessage()).isEqualTo("Msg body is empty: " + msg.getData()); + assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type: " + msgType); + verifyNoMoreInteractions(ctxMock); } @Test @@ -135,8 +125,14 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); - verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(null), eq(DEVICE_ID), anyList(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); + verify(telemetryServiceMock).saveAndNotify( + eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + List entryListCaptorValue = entryListCaptor.getValue(); + assertThat(entryListCaptorValue.size()).isEqualTo(2); + verifyTimeseriesToSave(entryListCaptorValue, msg); verify(ctxMock).tellSuccess(eq(msg)); + verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } @Test @@ -163,7 +159,24 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); - verify(telemetryServiceMock).saveWithoutLatestAndNotify(eq(TENANT_ID), eq(null), eq(DEVICE_ID), anyList(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); + verify(telemetryServiceMock).saveWithoutLatestAndNotify( + eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + List entryListCaptorValue = entryListCaptor.getValue(); + assertThat(entryListCaptorValue.size()).isEqualTo(2); + verifyTimeseriesToSave(entryListCaptorValue, msg); verify(ctxMock).tellSuccess(eq(msg)); + verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } + + private void verifyTimeseriesToSave(List tsKvEntryList, TbMsg incomingMsg) { + JsonNode msgData = JacksonUtil.toJsonNode(incomingMsg.getData()); + tsKvEntryList.forEach(tsKvEntry -> { + String key = tsKvEntry.getKey(); + assertThat(msgData.has(key)).isTrue(); + String value = tsKvEntry.getValueAsString(); + assertThat(value).isEqualTo(msgData.findValue(key).asText()); + }); + } + } From 19820d396884c4471410c5950589a5ba2919c042 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 3 Jun 2024 19:29:23 +0300 Subject: [PATCH 13/34] created tests for aws nodes --- .../rule/engine/aws/sns/TbSnsNodeTest.java | 149 +++++++++++++ .../rule/engine/aws/sqs/TbSqsNodeTest.java | 199 ++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java new file mode 100644 index 0000000000..5bbfe8304b --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java @@ -0,0 +1,149 @@ +/** + * 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.aws.sns; + +import com.amazonaws.ResponseMetadata; +import com.amazonaws.services.sns.AmazonSNS; +import com.amazonaws.services.sns.model.PublishRequest; +import com.amazonaws.services.sns.model.PublishResult; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +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.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.TbContext; +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.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TbSnsNodeTest { + + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("fccfdf2e-6a88-4a94-81dd-5cbb557019cf")); + private final ListeningExecutor executor = new TestDbCallbackExecutor(); + + private TbSnsNode node; + private TbSnsNodeConfiguration config; + + @Mock + private TbContext ctxMock; + @Mock + private AmazonSNS snsClientMock; + @Mock + private PublishResult publishResultMock; + @Mock + private ResponseMetadata responseMetadataMock; + + @BeforeEach + void setUp() { + node = new TbSnsNode(); + config = new TbSnsNodeConfiguration().defaultConfiguration(); + ReflectionTestUtils.setField(node, "snsClient", snsClientMock); + ReflectionTestUtils.setField(node, "config", config); + } + + @Test + void verifyDefaultConfig() { + assertThat(config.getTopicArnPattern()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyNewTopic"); + assertThat(config.getAccessKeyId()).isNull(); + assertThat(config.getSecretAccessKey()).isNull(); + assertThat(config.getRegion()).isEqualTo("us-east-1"); + } + + @ParameterizedTest + @MethodSource + void givenTopicNamePattern_whenOnMsg_thenTellSuccess(String topicName, TbMsgMetaData metaData, String data) + throws TbNodeException, ExecutionException, InterruptedException { + config.setAccessKeyId("accessKeyId"); + config.setSecretAccessKey("secretAccessKey"); + config.setTopicArnPattern(topicName); + String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7"; + String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31"; + + when(ctxMock.getExternalCallExecutor()).thenReturn(executor); + when(snsClientMock.publish(any(PublishRequest.class))).thenReturn(publishResultMock); + when(publishResultMock.getMessageId()).thenReturn(messageId); + when(publishResultMock.getSdkResponseMetadata()).thenReturn(responseMetadataMock); + when(responseMetadataMock.getRequestId()).thenReturn(requestId); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); + node.onMsg(ctxMock, msg); + + PublishRequest publishRequest = new PublishRequest() + .withTopicArn(TbNodeUtils.processPattern(topicName, msg)) + .withMessage(data); + verify(snsClientMock).publish(publishRequest); + ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctxMock).tellSuccess(msgArgumentCaptor.capture()); + assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + .hasFieldOrPropertyWithValue("messageId", messageId) + .hasFieldOrPropertyWithValue("requestId", requestId); + verifyNoMoreInteractions(ctxMock, snsClientMock, publishResultMock, responseMetadataMock); + } + + private static Stream givenTopicNamePattern_whenOnMsg_thenTellSuccess() { + return Stream.of( + Arguments.of("arn:aws:sns:us-east-1:123456789012:NewTopic", TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT), + Arguments.of("arn:aws:sns:us-east-1:123456789012:$[msgTopicName]", TbMsgMetaData.EMPTY, "{\"msgTopicName\":\"msg-topic-name\"}"), + Arguments.of("arn:aws:sns:us-east-1:123456789012:${mdTopicName}", new TbMsgMetaData(Map.of("mdTopicName", "md-topic-name")), TbMsg.EMPTY_JSON_OBJECT) + ); + } + + @Test + void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() throws TbNodeException, ExecutionException, InterruptedException { + ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); + when(ctxMock.getExternalCallExecutor()).thenReturn(listeningExecutor); + String errorMsg = "Something went wrong"; + ListenableFuture failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg)); + when(listeningExecutor.executeAsync(any(Callable.class))).thenReturn(failedFuture); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + node.onMsg(ctxMock, msg); + + ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); + assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); + verifyNoMoreInteractions(ctxMock, snsClientMock); + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java new file mode 100644 index 0000000000..ed08f8badb --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java @@ -0,0 +1,199 @@ +/** + * 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.aws.sqs; + +import com.amazonaws.ResponseMetadata; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.MessageAttributeValue; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.services.sqs.model.SendMessageResult; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +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.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.TbContext; +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.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TbSqsNodeTest { + + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("764de824-929f-4114-95ea-0ea0401ffa3d")); + private final ListeningExecutor executor = new TestDbCallbackExecutor(); + + private TbSqsNode node; + private TbSqsNodeConfiguration config; + + @Mock + private TbContext ctxMock; + @Mock + private AmazonSQS sqsClientMock; + @Mock + private SendMessageResult sendMessageResultMock; + @Mock + private ResponseMetadata responseMetadataMock; + + @BeforeEach + void setUp() { + node = new TbSqsNode(); + config = new TbSqsNodeConfiguration().defaultConfiguration(); + ReflectionTestUtils.setField(node, "sqsClient", sqsClientMock); + ReflectionTestUtils.setField(node, "config", config); + } + + @Test + void verifyDefaultConfig() { + assertThat(config.getQueueType()).isEqualTo(TbSqsNodeConfiguration.QueueType.STANDARD); + assertThat(config.getQueueUrlPattern()).isEqualTo("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue-name"); + assertThat(config.getDelaySeconds()).isEqualTo(0); + assertThat(config.getMessageAttributes()).isEqualTo(Collections.emptyMap()); + assertThat(config.getAccessKeyId()).isNull(); + assertThat(config.getSecretAccessKey()).isNull(); + assertThat(config.getRegion()).isEqualTo("us-east-1"); + } + + @ParameterizedTest + @MethodSource + void givenQueueUrlAndMsgAttributesPatternsAndQueueTypes_whenOnMsg_thenTellSuccess(String queueUrl, + TbMsgMetaData metaData, + String data, + Map attributes, + TbSqsNodeConfiguration.QueueType queueType) { + config.setAccessKeyId("accessKeyId"); + config.setSecretAccessKey("secretAccessKey"); + config.setQueueUrlPattern(queueUrl); + config.setQueueType(queueType); + config.setMessageAttributes(attributes); + + String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7"; + String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31"; + String messageBodyMd5 = "msgBodyMd5-55fb8ba2-2b71-4673-a82a-969756764761"; + String messageAttributesMd5 = "msgAttrMd5-e3ba3eef-52ae-436a-bec1-0c2c2252d1f1"; + String sequenceNumber = "seqNum-bb5ddce0-cf4e-4295-b015-524bdb6a332f"; + + when(ctxMock.getExternalCallExecutor()).thenReturn(executor); + when(sqsClientMock.sendMessage(any(SendMessageRequest.class))).thenReturn(sendMessageResultMock); + when(sendMessageResultMock.getMessageId()).thenReturn(messageId); + when(sendMessageResultMock.getSdkResponseMetadata()).thenReturn(responseMetadataMock); + when(responseMetadataMock.getRequestId()).thenReturn(requestId); + when(sendMessageResultMock.getMD5OfMessageBody()).thenReturn(messageBodyMd5); + when(sendMessageResultMock.getMD5OfMessageAttributes()).thenReturn(messageAttributesMd5); + when(sendMessageResultMock.getSequenceNumber()).thenReturn(sequenceNumber); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); + node.onMsg(ctxMock, msg); + + Map messageAttributes = new HashMap<>(); + this.config.getMessageAttributes().forEach((k,v) -> { + String name = TbNodeUtils.processPattern(k, msg); + String val = TbNodeUtils.processPattern(v, msg); + messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val)); + }); + SendMessageRequest sendMsgRequest = new SendMessageRequest() + .withQueueUrl(TbNodeUtils.processPattern(queueUrl, msg)) + .withMessageBody(data) + .withMessageAttributes(messageAttributes); + if (queueType == TbSqsNodeConfiguration.QueueType.STANDARD) { + sendMsgRequest.setDelaySeconds(0); + } else { + sendMsgRequest.withMessageDeduplicationId(msg.getId().toString()); + sendMsgRequest.withMessageGroupId(DEVICE_ID.toString()); + } + verify(sqsClientMock).sendMessage(sendMsgRequest); + ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctxMock).tellSuccess(msgArgumentCaptor.capture()); + assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + .hasFieldOrPropertyWithValue("messageId", messageId) + .hasFieldOrPropertyWithValue("requestId", requestId) + .hasFieldOrPropertyWithValue("messageBodyMd5", messageBodyMd5) + .hasFieldOrPropertyWithValue("messageAttributesMd5", messageAttributesMd5) + .hasFieldOrPropertyWithValue("sequenceNumber", sequenceNumber); + verifyNoMoreInteractions(ctxMock, sqsClientMock, sendMessageResultMock, responseMetadataMock); + } + + private static Stream givenQueueUrlAndMsgAttributesPatternsAndQueueTypes_whenOnMsg_thenTellSuccess() { + return Stream.of( + Arguments.of( + "https://sqs.us-east-1.amazonaws.com/123456789012/new-queue-name", + TbMsgMetaData.EMPTY, + TbMsg.EMPTY_JSON_OBJECT, + Map.of("attributeName", "attributeValue"), + TbSqsNodeConfiguration.QueueType.STANDARD), + Arguments.of( + "https://sqs.us-east-1.amazonaws.com/123456789012/$[msgQueueName]", + TbMsgMetaData.EMPTY, + "{\"msgQueueName\":\"msg-queue-name\",\"msgAttrNamePattern\":\"msgAttrName\",\"msgAttrValuePattern\":\"msgAttrValue\"}", + Map.of("$[msgAttrNamePattern]", "$[msgAttrValuePattern]"), + TbSqsNodeConfiguration.QueueType.FIFO), + Arguments.of("https://sqs.us-east-1.amazonaws.com/123456789012/${mdQueueName}", + new TbMsgMetaData(Map.of("mdQueueName", "md-queue-name", "mdAttrNamePattern", "mdAttrName", "mdAttrValuePattern", "mdAttrValue")), + TbMsg.EMPTY_JSON_OBJECT, + Map.of("${mdAttrNamePattern}", "${mdAttrValuePattern}"), + TbSqsNodeConfiguration.QueueType.STANDARD) + ); + } + + @Test + void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() throws TbNodeException, ExecutionException, InterruptedException { + ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); + when(ctxMock.getExternalCallExecutor()).thenReturn(listeningExecutor); + String errorMsg = "Something went wrong"; + + ListenableFuture failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg)); + when(listeningExecutor.executeAsync(any(Callable.class))).thenReturn(failedFuture); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + node.onMsg(ctxMock, msg); + + ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); + assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); + assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); + verifyNoMoreInteractions(ctxMock, sqsClientMock); + } + +} From 6be2c617177f9f5d989b7356d85661ff070f8820 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 5 Jun 2024 15:23:59 +0300 Subject: [PATCH 14/34] added tests to verify request that is passed to sendRpcRequestToDevice() method --- .../engine/rpc/TbSendRPCReplyNodeTest.java | 39 +- .../engine/rpc/TbSendRPCRequestNodeTest.java | 365 +++++++++++++++--- 2 files changed, 321 insertions(+), 83 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java index 17d53802a1..9cab33fcdd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; @@ -47,7 +48,7 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Stream; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -59,13 +60,14 @@ public class TbSendRPCReplyNodeTest { private static final String DUMMY_SERVICE_ID = "testServiceId"; private static final int DUMMY_REQUEST_ID = 0; - private static final UUID DUMMY_SESSION_ID = UUID.randomUUID(); - private static final String DUMMY_DATA = "{\"key\":\"value\"}"; + private static final UUID DUMMY_SESSION_ID = UUID.fromString("4f1d94aa-f6ee-4078-8499-b8e68443f8ad"); + private final String DUMMY_DATA = "{\"key\":\"value\"}"; - TbSendRPCReplyNode node; + private TbSendRPCReplyNode node; + private TbSendRpcReplyNodeConfiguration config; - private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); - private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); + private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("4e2e2336-3376-4238-ba0a-c669b412ca66")); + private final DeviceId deviceId = new DeviceId(UUID.fromString("af64d1b9-8635-47e1-8738-6389df7fe57e")); @Mock private TbContext ctx; @@ -82,7 +84,7 @@ public class TbSendRPCReplyNodeTest { @BeforeEach public void setUp() throws TbNodeException { node = new TbSendRPCReplyNode(); - TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); + config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); } @@ -121,18 +123,7 @@ public class TbSendRPCReplyNodeTest { @ParameterizedTest @EnumSource(EntityType.class) public void testOriginatorEntityTypes(EntityType entityType) { - if (entityType == EntityType.DEVICE) return; - EntityId entityId = new EntityId() { - @Override - public UUID getId() { - return UUID.randomUUID(); - } - - @Override - public EntityType getEntityType() { - return entityType; - } - }; + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "0f386739-210f-4e23-8739-23f84a172adc"); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctx, msg); @@ -140,7 +131,8 @@ public class TbSendRPCReplyNodeTest { ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctx).tellFailure(eq(msg), throwableCaptor.capture()); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) - .hasMessage("Message originator is not a device entity!"); + .hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!" + : "Request id is not present in the metadata!"); } @ParameterizedTest @@ -155,6 +147,13 @@ public class TbSendRPCReplyNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); } + @Test + public void verifyDefaultConfig() { + assertThat(config.getServiceIdMetaDataAttribute()).isEqualTo("serviceId"); + assertThat(config.getSessionIdMetaDataAttribute()).isEqualTo("sessionId"); + assertThat(config.getRequestIdMetaDataAttribute()).isEqualTo("requestId"); + } + private static Stream testForAvailabilityOfMetadataAndDataValues() { return Stream.of( Arguments.of(TbMsgMetaData.EMPTY, "Request id is not present in the metadata!"), diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java index 969aecb4d5..585638a1d9 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java @@ -19,7 +19,9 @@ 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.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -31,9 +33,11 @@ import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; @@ -41,25 +45,40 @@ import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; +import java.util.stream.Stream; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbSendRPCRequestNodeTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d3a47f8b-d863-4c1f-b6f0-2c946b43f21c")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b052ae59-b9b4-47e8-ac71-39e7124bbd66")); - + + private final String MSG_DATA = """ + { + "method": "setGpio", + "params": { + "pin": "23", + "value": 1 + }, + "additionalInfo": "information" + } + """; + private TbSendRPCRequestNode node; + private TbSendRpcRequestNodeConfiguration config; @Mock private TbContext ctxMock; @@ -67,112 +86,332 @@ public class TbSendRPCRequestNodeTest { private RuleEngineRpcService rpcServiceMock; @BeforeEach - public void setUp() throws TbNodeException { + void setUp() throws TbNodeException { node = new TbSendRPCRequestNode(); - var config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); + config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, configuration); } @Test - public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { - TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + void verifyDefaultConfig() { + assertThat(config.getTimeoutInSeconds()).isEqualTo(60); + } - when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - // TODO: replace deprecated method newMsg() - when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); - doAnswer(invocation -> { - Consumer consumer = invocation.getArgument(1); - RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); - when(rpcResponseMock.getError()).thenReturn(Optional.empty()); - when(rpcResponseMock.getResponse()).thenReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT)); - consumer.accept(rpcResponseMock); - return null; - }).when(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); + @ParameterizedTest + @MethodSource + void givenOneway_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetadata = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenOneway_whenOnMsg_thenVerifyRequest() { + var metadata = new HashMap<>(); + metadata.put("oneway", null); + return Stream.of( + Arguments.of(Map.of("oneway", "true"), (Consumer) req -> + assertThat(req.isOneway()).isTrue()), + Arguments.of(null, (Consumer) req -> + assertThat(req.isOneway()).isFalse()), + Arguments.of(Map.of("oneway", ""), (Consumer) req -> + assertThat(req.isOneway()).isFalse()), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.isOneway()).isFalse()) + ); + } + + @Test + void givenMsgBody_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class); + then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class)); + assertThat(requestCaptor.getValue()) + .hasFieldOrPropertyWithValue("method", "setGpio") + .hasFieldOrPropertyWithValue("body", "{\"pin\":\"23\",\"value\":1}") + .hasFieldOrPropertyWithValue("deviceId", DEVICE_ID) + .hasFieldOrPropertyWithValue("tenantId", TENANT_ID) + .hasFieldOrPropertyWithValue("additionalInfo", "information"); + } + + @ParameterizedTest + @MethodSource + void givenRequestId_whenOnMsg_thenVerifyRequest(String requestId, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); - String data = """ + String data = String.format(""" { "method": "setGpio", "params": { "pin": "23", "value": 1 - } + }%s%s } - """; - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, data); + """, requestId != null ? ",\"requestId\":" : "", requestId != null ? requestId : ""); + TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); node.onMsg(ctxMock, msg); - verify(ctxMock).enqueueForTellNext(eq(outMsg), eq(TbNodeConnectionType.SUCCESS)); - verify(ctxMock).ack(eq(msg)); + verifyRequest(requestConsumer); + } + + private static Stream givenRequestId_whenOnMsg_thenVerifyRequest() { + return Stream.of( + Arguments.of("12345", (Consumer) req -> + assertThat(req.getRequestId()).isEqualTo(12345)), + Arguments.of(null, (Consumer) req -> + assertThat(req.getRequestId()).isNotNull()) + ); + } + + @ParameterizedTest + @MethodSource + void givenRequestUUID_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetadata = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenRequestUUID_whenOnMsg_thenVerifyRequest() { + var metadata= new HashMap<>(); + metadata.put("requestUUID", null); + return Stream.of( + Arguments.of(Map.of("requestUUID", "1c4ef338-ea1b-495f-8e2b-67981f27cf35"), (Consumer) req -> + assertThat(req.getRequestUUID()).isEqualTo(UUID.fromString("1c4ef338-ea1b-495f-8e2b-67981f27cf35"))), + Arguments.of(null, (Consumer) req -> + assertThat(req.getRequestUUID()).isNotNull()), + Arguments.of(Map.of("requestUUID", ""), (Consumer) req -> + assertThat(req.getRequestUUID()).isNotNull()), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.getRequestUUID()).isNotNull()) + ); + } + + @ParameterizedTest + @MethodSource + void givenOriginServiceId_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenOriginServiceId_whenOnMsg_thenVerifyRequest() { + var metadata= new HashMap<>(); + metadata.put("originServiceId", null); + return Stream.of( + Arguments.of(Map.of("originServiceId", "service-id-123"), (Consumer) req -> + assertThat(req.getOriginServiceId()).isEqualTo("service-id-123")), + Arguments.of(null, (Consumer) req -> + assertThat(req.getOriginServiceId()).isNull()), + Arguments.of(Map.of("originServiceId", ""), (Consumer) req -> + assertThat(req.getOriginServiceId()).isNull()), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.getOriginServiceId()).isNull()) + ); + } + + @ParameterizedTest + @MethodSource + void givenExpirationTime_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenExpirationTime_whenOnMsg_thenVerifyRequest() { + var metadata= new HashMap<>(); + metadata.put(DataConstants.EXPIRATION_TIME, null); + return Stream.of( + Arguments.of(Map.of(DataConstants.EXPIRATION_TIME, "2000000000000"), (Consumer) req -> + assertThat(req.getExpirationTime()).isEqualTo(2000000000000L)), + Arguments.of(null, (Consumer) req -> + assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())), + Arguments.of(Map.of(DataConstants.EXPIRATION_TIME, ""), (Consumer) req -> + assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())) + ); + } + + @ParameterizedTest + @MethodSource + void givenRetries_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenRetries_whenOnMsg_thenVerifyRequest() { + var metadata= new HashMap<>(); + metadata.put(DataConstants.RETRIES, null); + return Stream.of( + Arguments.of(Map.of(DataConstants.RETRIES, "3"), (Consumer) req -> + assertThat(req.getRetries()).isEqualTo(3)), + Arguments.of(null, (Consumer) req -> + assertThat(req.getRetries()).isNull()), + Arguments.of(Map.of(DataConstants.RETRIES,""), (Consumer) req -> + assertThat(req.getRetries()).isNull()), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.getRetries()).isNull()) + ); + } + + @ParameterizedTest + @MethodSource + void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenTbMsgType_whenOnMsg_thenVerifyRequest() { + return Stream.of( + Arguments.of(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, (Consumer) req -> + assertThat(req.isRestApiCall()).isTrue()), + Arguments.of(TbMsgType.TO_SERVER_RPC_REQUEST, (Consumer) req -> + assertThat(req.isRestApiCall()).isFalse()) + ); + } + + @ParameterizedTest + @MethodSource + void givenPersistent_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + node.onMsg(ctxMock, msg); + + verifyRequest(requestConsumer); + } + + private static Stream givenPersistent_whenOnMsg_thenVerifyRequest() { + var metadata= new HashMap<>(); + metadata.put(DataConstants.PERSISTENT, null); + return Stream.of( + Arguments.of(Map.of(DataConstants.PERSISTENT, "true"), (Consumer) req -> + assertThat(req.isPersisted()).isTrue()), + Arguments.of(null, (Consumer) req -> + assertThat(req.isPersisted()).isFalse()), + Arguments.of(Map.of(DataConstants.PERSISTENT, ""), (Consumer) req -> + assertThat(req.isPersisted()).isFalse()), + Arguments.of(metadata, (Consumer) req -> + assertThat(req.isPersisted()).isFalse()) + ); + } + + private void verifyRequest(Consumer requestConsumer) { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class); + then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class)); + requestConsumer.accept(requestCaptor.getValue()); } @Test - public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { + void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); - when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); // TODO: replace deprecated method newMsg() - when(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).thenReturn(outMsg); - doAnswer(invocation -> { + given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg); + willAnswer(invocation -> { Consumer consumer = invocation.getArgument(1); RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); - when(rpcResponseMock.getError()).thenReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION)); + given(rpcResponseMock.getError()).willReturn(Optional.empty()); + given(rpcResponseMock.getResponse()).willReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT)); consumer.accept(rpcResponseMock); return null; - }).when(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); + }).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); - String data = """ - { - "method": "setGpio", - "params": { - "pin": "23", - "value": 1 - } - } - """; - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, data); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); + node.onMsg(ctxMock, msg); + + then(ctxMock).should().enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS); + then(ctxMock).should().ack(msg); + } + + @Test + void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { + TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + // TODO: replace deprecated method newMsg() + given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg); + willAnswer(invocation -> { + Consumer consumer = invocation.getArgument(1); + RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); + given(rpcResponseMock.getError()).willReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION)); + consumer.accept(rpcResponseMock); + return null; + }).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); + + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); node.onMsg(ctxMock, msg); - verify(ctxMock).enqueueForTellFailure(eq(outMsg), eq(RpcError.NO_ACTIVE_CONNECTION.name())); - verify(ctxMock).ack(eq(msg)); + then(ctxMock).should().enqueueForTellFailure(outMsg, RpcError.NO_ACTIVE_CONNECTION.name()); + then(ctxMock).should().ack(msg); } @ParameterizedTest @EnumSource(EntityType.class) - public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { - if (entityType == EntityType.DEVICE) return; - EntityId entityId = new EntityId() { - @Override - public UUID getId() { - return UUID.randomUUID(); - } - - @Override - public EntityType getEntityType() { - return entityType; - } - }; + void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "ac21a1bb-eabf-4463-8313-24bea1f498d9"); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture()); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) - .hasMessage("Message originator is not a device entity!"); + .hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!" + : "Method is not present in the message!"); } @ParameterizedTest @ValueSource(strings = {"method", "params"}) - public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { + void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); node.onMsg(ctxMock, msg); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture()); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) .hasMessage(key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!"); } From 7ed5b3c1e18d5133f4b44ba4011badf98bf2dc46 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 5 Jun 2024 18:38:50 +0300 Subject: [PATCH 15/34] added tests for ttl usage --- .../telemetry/TbMsgTimeseriesNodeTest.java | 106 ++++++++++++++---- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java index d4bdcae878..445f09b5ee 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -20,7 +20,9 @@ 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.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -40,9 +42,12 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -60,6 +65,7 @@ public class TbMsgTimeseriesNodeTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("e5095e9a-04f4-44c9-b443-1cf1b97d3384")); + private final TenantProfileId TENANT_PROFILE_ID = new TenantProfileId(UUID.fromString("ab78dd78-83d0-43fa-869f-d42ec9ed1744")); private TbMsgTimeseriesNode node; private TbMsgTimeseriesNodeConfiguration config; @@ -74,24 +80,12 @@ public class TbMsgTimeseriesNodeTest { public void setUp() throws TbNodeException { node = new TbMsgTimeseriesNode(); config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration(); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - var tenantProfile = new TenantProfile(new TenantProfileId(UUID.fromString("8c45d0fe-d437-40e9-8c31-b695b315bf40"))); - var tenantProfileData = new TenantProfileData(); - var tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); - tenantProfileData.setConfiguration(tenantProfileConfiguration); - tenantProfile.setProfileData(tenantProfileData); - when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); - doAnswer(invocation -> { - invocation.getArgument(0); - return null; - }).when(ctxMock).addTenantProfileListener(any()); - node.init(ctxMock, configuration); - tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(tenantProfileConfiguration.getDefaultStorageTtlDays()); } @ParameterizedTest @EnumSource(TbMsgType.class) - void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) { + void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) throws TbNodeException { + init(); TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctxMock, msg); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); @@ -106,7 +100,9 @@ public class TbMsgTimeseriesNodeTest { } @Test - void givenSkipLatestPersistenceIsFalse_whenOnMsg_thenSaveTimeseries() { + void givenTtlFromConfigIsZeroAndUseServiceTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException { + config.setUseServerTs(true); + init(); String data = """ { "temp": 45, @@ -126,19 +122,23 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); - verify(telemetryServiceMock).saveAndNotify( - eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), + eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); List entryListCaptorValue = entryListCaptor.getValue(); assertThat(entryListCaptorValue.size()).isEqualTo(2); verifyTimeseriesToSave(entryListCaptorValue, msg); - verify(ctxMock).tellSuccess(eq(msg)); + verify(ctxMock).tellSuccess(msg); verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } @Test - void givenSkipLatestPersistenceIsTrue_whenOnMsg_thenSaveTimeseries() throws TbNodeException { + void givenSkipLatestPersistenceIsTrueAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException { + long ttlFromConfig = 5L; + config.setDefaultTTL(ttlFromConfig); config.setSkipLatestPersistence(true); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + var tenantProfile = getTenantProfile(); + when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); node.init(ctxMock, configuration); String data = """ @@ -147,7 +147,9 @@ public class TbMsgTimeseriesNodeTest { "humidity": 77 } """; - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); + long ts = System.currentTimeMillis(); + var metadata = Map.of("ts", String.valueOf(ts)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, new TbMsgMetaData(metadata), data); when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -159,19 +161,58 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); + verify(ctxMock).addTenantProfileListener(any()); ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); verify(telemetryServiceMock).saveWithoutLatestAndNotify( - eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); + eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(ttlFromConfig), any(TelemetryNodeCallback.class)); List entryListCaptorValue = entryListCaptor.getValue(); assertThat(entryListCaptorValue.size()).isEqualTo(2); - verifyTimeseriesToSave(entryListCaptorValue, msg); - verify(ctxMock).tellSuccess(eq(msg)); + verifyTimeseriesToSave(entryListCaptorValue, msg, ts); + verify(ctxMock).tellSuccess(msg); verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } + @ParameterizedTest + @MethodSource + void givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl(String ttlFromMd, long ttlFromConfig, long expectedTtl) throws TbNodeException { + config.setDefaultTTL(ttlFromConfig); + init(); + + when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + + String data = """ + { + "temp": 45, + "humidity": 77 + } + """; + var metadata = new HashMap(); + metadata.put("TTL", ttlFromMd); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, new TbMsgMetaData(metadata), data); + node.onMsg(ctxMock, msg); + + verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), anyList(), eq(expectedTtl), any(TelemetryNodeCallback.class)); + } + + private static Stream givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl() { + return Stream.of( + Arguments.of("5", 1L, 5L), + Arguments.of("", 3L, 3L), + Arguments.of(null, 8L, 8L) + ); + } + private void verifyTimeseriesToSave(List tsKvEntryList, TbMsg incomingMsg) { + verifyTimeseriesToSave(tsKvEntryList, incomingMsg, null); + } + + private void verifyTimeseriesToSave(List tsKvEntryList, TbMsg incomingMsg, Long ts) { JsonNode msgData = JacksonUtil.toJsonNode(incomingMsg.getData()); tsKvEntryList.forEach(tsKvEntry -> { + if (ts != null) { + assertThat(tsKvEntry.getTs()).isEqualTo(ts); + } String key = tsKvEntry.getKey(); assertThat(msgData.has(key)).isTrue(); String value = tsKvEntry.getValueAsString(); @@ -179,4 +220,23 @@ public class TbMsgTimeseriesNodeTest { }); } + private void init() throws TbNodeException { + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + var tenantProfile = getTenantProfile(); + when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); + node.init(ctxMock, configuration); + tenantProfile.getProfileConfiguration().ifPresent(profileConfiguration -> + tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(profileConfiguration.getDefaultStorageTtlDays())); + verify(ctxMock).addTenantProfileListener(any()); + } + + private TenantProfile getTenantProfile() { + var tenantProfile = new TenantProfile(TENANT_PROFILE_ID); + var tenantProfileData = new TenantProfileData(); + var tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); + tenantProfileData.setConfiguration(tenantProfileConfiguration); + tenantProfile.setProfileData(tenantProfileData); + return tenantProfile; + } + } From f9a126df31cfa70a9f17092be93a7d3c4e4fd89f Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Jun 2024 19:10:09 +0300 Subject: [PATCH 16/34] refactored parametrized tests --- .../engine/rpc/TbSendRPCRequestNodeTest.java | 275 +++++++++--------- 1 file changed, 139 insertions(+), 136 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java index 585638a1d9..859157454f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java @@ -22,10 +22,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; 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.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; @@ -45,9 +47,8 @@ import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; +import java.util.Random; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Stream; @@ -100,34 +101,30 @@ public class TbSendRPCRequestNodeTest { @ParameterizedTest @MethodSource - void givenOneway_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + public void givenOneway_whenOnMsg_thenVerifyRequest(String mdKeyValue, boolean expectedResult) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetadata = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); + TbMsgMetaData msgMetadata = new TbMsgMetaData(); + msgMetadata.putValue("oneway", mdKeyValue); TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + var ruleEngineDeviceRpcRequestCaptor = captureRequest(); + assertThat(ruleEngineDeviceRpcRequestCaptor.getValue().isOneway()).isEqualTo(expectedResult); } private static Stream givenOneway_whenOnMsg_thenVerifyRequest() { - var metadata = new HashMap<>(); - metadata.put("oneway", null); return Stream.of( - Arguments.of(Map.of("oneway", "true"), (Consumer) req -> - assertThat(req.isOneway()).isTrue()), - Arguments.of(null, (Consumer) req -> - assertThat(req.isOneway()).isFalse()), - Arguments.of(Map.of("oneway", ""), (Consumer) req -> - assertThat(req.isOneway()).isFalse()), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.isOneway()).isFalse()) + Arguments.of("true", true), + Arguments.of("false", false), + Arguments.of(null, false), + Arguments.of("", false) ); } @Test - void givenMsgBody_whenOnMsg_thenVerifyRequest() { + public void givenMsgBody_whenOnMsg_thenVerifyRequest() { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); @@ -144,205 +141,211 @@ public class TbSendRPCRequestNodeTest { .hasFieldOrPropertyWithValue("additionalInfo", "information"); } - @ParameterizedTest - @MethodSource - void givenRequestId_whenOnMsg_thenVerifyRequest(String requestId, Consumer requestConsumer) { + @Test + public void givenRequestIdIsNotSet_whenOnMsg_thenVerifyRequest() { + Random randomMock = mock(Random.class); + given(randomMock.nextInt()).willReturn(123); + ReflectionTestUtils.setField(node, "random", randomMock); given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - String data = String.format(""" + TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(123); + } + + @Test + public void givenRequestId_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + String data = """ { "method": "setGpio", "params": { "pin": "23", "value": 1 - }%s%s + }, + "requestId": 12345 } - """, requestId != null ? ",\"requestId\":" : "", requestId != null ? requestId : ""); + """; TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(12345); } - private static Stream givenRequestId_whenOnMsg_thenVerifyRequest() { - return Stream.of( - Arguments.of("12345", (Consumer) req -> - assertThat(req.getRequestId()).isEqualTo(12345)), - Arguments.of(null, (Consumer) req -> - assertThat(req.getRequestId()).isNotNull()) - ); + @Test + public void givenRequestUUID_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + String requestUUID = "b795a241-5a30-48fb-92d5-46b864d47130"; + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue("requestUUID", requestUUID); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRequestUUID()).isEqualTo(UUID.fromString(requestUUID)); } @ParameterizedTest - @MethodSource - void givenRequestUUID_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + @NullAndEmptySource + public void givenInvalidRequestUUID_whenOnMsg_thenVerifyRequest(String requestUUID) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetadata = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue("requestUUID", requestUUID); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRequestUUID()).isNotNull(); } - private static Stream givenRequestUUID_whenOnMsg_thenVerifyRequest() { - var metadata= new HashMap<>(); - metadata.put("requestUUID", null); - return Stream.of( - Arguments.of(Map.of("requestUUID", "1c4ef338-ea1b-495f-8e2b-67981f27cf35"), (Consumer) req -> - assertThat(req.getRequestUUID()).isEqualTo(UUID.fromString("1c4ef338-ea1b-495f-8e2b-67981f27cf35"))), - Arguments.of(null, (Consumer) req -> - assertThat(req.getRequestUUID()).isNotNull()), - Arguments.of(Map.of("requestUUID", ""), (Consumer) req -> - assertThat(req.getRequestUUID()).isNotNull()), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.getRequestUUID()).isNotNull()) - ); + @Test + public void givenOriginServiceId_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + String originServiceId = "service-id-123"; + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue("originServiceId", originServiceId); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getOriginServiceId()).isEqualTo(originServiceId); } @ParameterizedTest - @MethodSource - void givenOriginServiceId_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + @NullAndEmptySource + public void givenInvalidOriginServiceId_whenOnMsg_thenVerifyRequest(String originServiceId) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue("originServiceId", originServiceId); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getOriginServiceId()).isNull(); } - private static Stream givenOriginServiceId_whenOnMsg_thenVerifyRequest() { - var metadata= new HashMap<>(); - metadata.put("originServiceId", null); - return Stream.of( - Arguments.of(Map.of("originServiceId", "service-id-123"), (Consumer) req -> - assertThat(req.getOriginServiceId()).isEqualTo("service-id-123")), - Arguments.of(null, (Consumer) req -> - assertThat(req.getOriginServiceId()).isNull()), - Arguments.of(Map.of("originServiceId", ""), (Consumer) req -> - assertThat(req.getOriginServiceId()).isNull()), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.getOriginServiceId()).isNull()) - ); + @Test + public void givenExpirationTime_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + String expirationTime = "2000000000000"; + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getExpirationTime()).isEqualTo(Long.parseLong(expirationTime)); } @ParameterizedTest - @MethodSource - void givenExpirationTime_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + @NullAndEmptySource + public void givenInvalidExpirationTime_whenOnMsg_thenVerifyRequest(String expirationTime) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getExpirationTime()).isGreaterThan(System.currentTimeMillis()); } - private static Stream givenExpirationTime_whenOnMsg_thenVerifyRequest() { - var metadata= new HashMap<>(); - metadata.put(DataConstants.EXPIRATION_TIME, null); - return Stream.of( - Arguments.of(Map.of(DataConstants.EXPIRATION_TIME, "2000000000000"), (Consumer) req -> - assertThat(req.getExpirationTime()).isEqualTo(2000000000000L)), - Arguments.of(null, (Consumer) req -> - assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())), - Arguments.of(Map.of(DataConstants.EXPIRATION_TIME, ""), (Consumer) req -> - assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.getExpirationTime()).isGreaterThan(System.currentTimeMillis())) - ); + @Test + public void givenRetries_whenOnMsg_thenVerifyRequest() { + given(ctxMock.getRpcService()).willReturn(rpcServiceMock); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + Integer retries = 3; + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.RETRIES, String.valueOf(retries)); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); + node.onMsg(ctxMock, msg); + + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRetries()).isEqualTo(retries); } @ParameterizedTest - @MethodSource - void givenRetries_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + @NullAndEmptySource + public void givenInvalidRetriesValue_whenOnMsg_thenVerifyRequest(String retries) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.RETRIES, retries); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); - } - - private static Stream givenRetries_whenOnMsg_thenVerifyRequest() { - var metadata= new HashMap<>(); - metadata.put(DataConstants.RETRIES, null); - return Stream.of( - Arguments.of(Map.of(DataConstants.RETRIES, "3"), (Consumer) req -> - assertThat(req.getRetries()).isEqualTo(3)), - Arguments.of(null, (Consumer) req -> - assertThat(req.getRetries()).isNull()), - Arguments.of(Map.of(DataConstants.RETRIES,""), (Consumer) req -> - assertThat(req.getRetries()).isNull()), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.getRetries()).isNull()) - ); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().getRetries()).isNull(); } @ParameterizedTest - @MethodSource - void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType, Consumer requestConsumer) { + @EnumSource(TbMsgType.class) + public void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); - } - - private static Stream givenTbMsgType_whenOnMsg_thenVerifyRequest() { - return Stream.of( - Arguments.of(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, (Consumer) req -> - assertThat(req.isRestApiCall()).isTrue()), - Arguments.of(TbMsgType.TO_SERVER_RPC_REQUEST, (Consumer) req -> - assertThat(req.isRestApiCall()).isFalse()) - ); + ArgumentCaptor requestCaptor = captureRequest(); + if (msgType == TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE) { + assertThat(requestCaptor.getValue().isRestApiCall()).isTrue(); + return; + } + assertThat(requestCaptor.getValue().isRestApiCall()).isFalse(); } @ParameterizedTest @MethodSource - void givenPersistent_whenOnMsg_thenVerifyRequest(Map metadata, Consumer requestConsumer) { + public void givenPersistent_whenOnMsg_thenVerifyRequest(String isPersisted, boolean expectedPersistence) { given(ctxMock.getRpcService()).willReturn(rpcServiceMock); given(ctxMock.getTenantId()).willReturn(TENANT_ID); - TbMsgMetaData msgMetaData = metadata == null ? TbMsgMetaData.EMPTY : new TbMsgMetaData(metadata); - TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetaData, MSG_DATA); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.PERSISTENT, isPersisted); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); node.onMsg(ctxMock, msg); - verifyRequest(requestConsumer); + ArgumentCaptor requestCaptor = captureRequest(); + assertThat(requestCaptor.getValue().isPersisted()).isEqualTo(expectedPersistence); } private static Stream givenPersistent_whenOnMsg_thenVerifyRequest() { - var metadata= new HashMap<>(); - metadata.put(DataConstants.PERSISTENT, null); return Stream.of( - Arguments.of(Map.of(DataConstants.PERSISTENT, "true"), (Consumer) req -> - assertThat(req.isPersisted()).isTrue()), - Arguments.of(null, (Consumer) req -> - assertThat(req.isPersisted()).isFalse()), - Arguments.of(Map.of(DataConstants.PERSISTENT, ""), (Consumer) req -> - assertThat(req.isPersisted()).isFalse()), - Arguments.of(metadata, (Consumer) req -> - assertThat(req.isPersisted()).isFalse()) + Arguments.of("true", true), + Arguments.of("false", false), + Arguments.of(null, false), + Arguments.of("", false) ); } - private void verifyRequest(Consumer requestConsumer) { + private ArgumentCaptor captureRequest() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class); then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class)); - requestConsumer.accept(requestCaptor.getValue()); + return requestCaptor; } @Test - void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { + public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); given(ctxMock.getRpcService()).willReturn(rpcServiceMock); @@ -366,7 +369,7 @@ public class TbSendRPCRequestNodeTest { } @Test - void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { + public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); given(ctxMock.getRpcService()).willReturn(rpcServiceMock); @@ -390,7 +393,7 @@ public class TbSendRPCRequestNodeTest { @ParameterizedTest @EnumSource(EntityType.class) - void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { + public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "ac21a1bb-eabf-4463-8313-24bea1f498d9"); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); @@ -405,7 +408,7 @@ public class TbSendRPCRequestNodeTest { @ParameterizedTest @ValueSource(strings = {"method", "params"}) - void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { + public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); node.onMsg(ctxMock, msg); From dce51e9e2d5836e766bacd5c5cb4c1a64b4a50da Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Jun 2024 19:12:21 +0300 Subject: [PATCH 17/34] made methods public --- .../thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java index 859157454f..e9d25e0d5e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java @@ -87,7 +87,7 @@ public class TbSendRPCRequestNodeTest { private RuleEngineRpcService rpcServiceMock; @BeforeEach - void setUp() throws TbNodeException { + public void setUp() throws TbNodeException { node = new TbSendRPCRequestNode(); config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); @@ -95,7 +95,7 @@ public class TbSendRPCRequestNodeTest { } @Test - void verifyDefaultConfig() { + public void verifyDefaultConfig() { assertThat(config.getTimeoutInSeconds()).isEqualTo(60); } From 4fc69299222c487e890faa8e875568fb3fb68f4b Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Jun 2024 10:47:27 +0300 Subject: [PATCH 18/34] added test for default config and set different values for check logic of setting ttl --- .../telemetry/TbMsgTimeseriesNodeTest.java | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java index 445f09b5ee..1fb4e9eefd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.telemetry; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonParser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,10 +31,13 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -42,14 +45,14 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; @@ -82,17 +85,26 @@ public class TbMsgTimeseriesNodeTest { config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration(); } + @Test + public void verifyDefaultConfig() { + assertThat(config.getDefaultTTL()).isEqualTo(0L); + assertThat(config.isSkipLatestPersistence()).isFalse(); + assertThat(config.isUseServerTs()).isFalse(); + } + @ParameterizedTest @EnumSource(TbMsgType.class) - void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) throws TbNodeException { + public void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) throws TbNodeException { init(); TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctxMock, msg); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); if (TbMsgType.POST_TELEMETRY_REQUEST.equals(msgType)) { assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Msg body is empty: " + msg.getData()); + verifyNoMoreInteractions(ctxMock); return; } assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type: " + msgType); @@ -100,9 +112,10 @@ public class TbMsgTimeseriesNodeTest { } @Test - void givenTtlFromConfigIsZeroAndUseServiceTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException { + public void givenTtlFromConfigIsZeroAndUseServiceTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException { config.setUseServerTs(true); init(); + String data = """ { "temp": 45, @@ -121,25 +134,22 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); + List expectedList = getTsKvEntriesListWithTs(data, System.currentTimeMillis()); ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class)); - List entryListCaptorValue = entryListCaptor.getValue(); - assertThat(entryListCaptorValue.size()).isEqualTo(2); - verifyTimeseriesToSave(entryListCaptorValue, msg); + assertThat(entryListCaptor.getValue()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("ts") + .containsExactlyElementsOf(expectedList); verify(ctxMock).tellSuccess(msg); verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } @Test - void givenSkipLatestPersistenceIsTrueAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException { + public void givenSkipLatestPersistenceIsTrueAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException { long ttlFromConfig = 5L; config.setDefaultTTL(ttlFromConfig); config.setSkipLatestPersistence(true); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - var tenantProfile = getTenantProfile(); - when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); - node.init(ctxMock, configuration); + init(); String data = """ { @@ -161,20 +171,18 @@ public class TbMsgTimeseriesNodeTest { node.onMsg(ctxMock, msg); - verify(ctxMock).addTenantProfileListener(any()); + List expectedList = getTsKvEntriesListWithTs(data, ts); ArgumentCaptor> entryListCaptor = ArgumentCaptor.forClass(List.class); verify(telemetryServiceMock).saveWithoutLatestAndNotify( eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(ttlFromConfig), any(TelemetryNodeCallback.class)); - List entryListCaptorValue = entryListCaptor.getValue(); - assertThat(entryListCaptorValue.size()).isEqualTo(2); - verifyTimeseriesToSave(entryListCaptorValue, msg, ts); + assertThat(entryListCaptor.getValue()).containsExactlyElementsOf(expectedList); verify(ctxMock).tellSuccess(msg); verifyNoMoreInteractions(ctxMock, telemetryServiceMock); } @ParameterizedTest @MethodSource - void givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl(String ttlFromMd, long ttlFromConfig, long expectedTtl) throws TbNodeException { + public void givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl(String ttlFromMd, long ttlFromConfig, long expectedTtl) throws TbNodeException { config.setDefaultTTL(ttlFromConfig); init(); @@ -187,9 +195,9 @@ public class TbMsgTimeseriesNodeTest { "humidity": 77 } """; - var metadata = new HashMap(); - metadata.put("TTL", ttlFromMd); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, new TbMsgMetaData(metadata), data); + var metadata = new TbMsgMetaData(); + metadata.putValue("TTL", ttlFromMd); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metadata, data); node.onMsg(ctxMock, msg); verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), anyList(), eq(expectedTtl), any(TelemetryNodeCallback.class)); @@ -197,36 +205,23 @@ public class TbMsgTimeseriesNodeTest { private static Stream givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl() { return Stream.of( - Arguments.of("5", 1L, 5L), + // when ttl is present in metadata and it is not zero then ttl = ttl from metadata + Arguments.of("1", 2L, 1L), + // when ttl is absent in metadata and present in config and it is not zero then ttl = ttl from config Arguments.of("", 3L, 3L), - Arguments.of(null, 8L, 8L) + Arguments.of(null, 4L, 4L), + // when ttl is present in metadata or config but it is zero then ttl = default ttl from tenant profile + Arguments.of("0", 0L, TimeUnit.DAYS.toSeconds(5L)) ); } - private void verifyTimeseriesToSave(List tsKvEntryList, TbMsg incomingMsg) { - verifyTimeseriesToSave(tsKvEntryList, incomingMsg, null); - } - - private void verifyTimeseriesToSave(List tsKvEntryList, TbMsg incomingMsg, Long ts) { - JsonNode msgData = JacksonUtil.toJsonNode(incomingMsg.getData()); - tsKvEntryList.forEach(tsKvEntry -> { - if (ts != null) { - assertThat(tsKvEntry.getTs()).isEqualTo(ts); - } - String key = tsKvEntry.getKey(); - assertThat(msgData.has(key)).isTrue(); - String value = tsKvEntry.getValueAsString(); - assertThat(value).isEqualTo(msgData.findValue(key).asText()); - }); - } - private void init() throws TbNodeException { var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); var tenantProfile = getTenantProfile(); when(ctxMock.getTenantProfile()).thenReturn(tenantProfile); - node.init(ctxMock, configuration); tenantProfile.getProfileConfiguration().ifPresent(profileConfiguration -> tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(profileConfiguration.getDefaultStorageTtlDays())); + node.init(ctxMock, configuration); verify(ctxMock).addTenantProfileListener(any()); } @@ -234,9 +229,21 @@ public class TbMsgTimeseriesNodeTest { var tenantProfile = new TenantProfile(TENANT_PROFILE_ID); var tenantProfileData = new TenantProfileData(); var tenantProfileConfiguration = new DefaultTenantProfileConfiguration(); + tenantProfileConfiguration.setDefaultStorageTtlDays(5); tenantProfileData.setConfiguration(tenantProfileConfiguration); tenantProfile.setProfileData(tenantProfileData); return tenantProfile; } + private static List getTsKvEntriesListWithTs(String data, long ts) { + Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(data), ts); + List expectedList = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + expectedList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + return expectedList; + } + } From 72bf05af8f93991bd27bb71ebf45224f05cfdd06 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Jun 2024 12:28:26 +0300 Subject: [PATCH 19/34] added test to verify request --- .../rule/engine/aws/sns/TbSnsNode.java | 16 +- .../rule/engine/aws/sns/TbSnsNodeTest.java | 33 ++-- .../rule/engine/aws/sqs/TbSqsNodeTest.java | 169 +++++++++++------- 3 files changed, 124 insertions(+), 94 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java index 9ac2770b9d..6516a5e4b3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java @@ -35,8 +35,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import java.util.concurrent.ExecutionException; - import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @@ -81,34 +79,34 @@ public class TbSnsNode extends TbAbstractExternalNode { } @Override - public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { + public void onMsg(TbContext ctx, TbMsg msg) { var tbMsg = ackIfNeeded(ctx, msg); withCallback(publishMessageAsync(ctx, tbMsg), m -> tellSuccess(ctx, m), - t -> tellFailure(ctx, processException(ctx, tbMsg, t), t)); + t -> tellFailure(ctx, processException(tbMsg, t), t)); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { - return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg)); + return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg)); } - private TbMsg publishMessage(TbContext ctx, TbMsg msg) { + private TbMsg publishMessage(TbMsg msg) { String topicArn = TbNodeUtils.processPattern(this.config.getTopicArnPattern(), msg); PublishRequest publishRequest = new PublishRequest() .withTopicArn(topicArn) .withMessage(msg.getData()); PublishResult result = this.snsClient.publish(publishRequest); - return processPublishResult(ctx, msg, result); + return processPublishResult(msg, result); } - private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, PublishResult result) { + private TbMsg processPublishResult(TbMsg origMsg, PublishResult result) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, result.getMessageId()); metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId()); return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); return TbMsg.transformMsgMetadata(origMsg, metaData); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java index 5bbfe8304b..2b67187687 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java @@ -34,7 +34,6 @@ import org.springframework.test.util.ReflectionTestUtils; 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.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; @@ -44,15 +43,14 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.stream.Stream; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class TbSnsNodeTest { @@ -90,19 +88,18 @@ class TbSnsNodeTest { @ParameterizedTest @MethodSource - void givenTopicNamePattern_whenOnMsg_thenTellSuccess(String topicName, TbMsgMetaData metaData, String data) - throws TbNodeException, ExecutionException, InterruptedException { + void givenTopicNamePattern_whenOnMsg_thenTellSuccess(String topicName, TbMsgMetaData metaData, String data) { config.setAccessKeyId("accessKeyId"); config.setSecretAccessKey("secretAccessKey"); config.setTopicArnPattern(topicName); String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7"; String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31"; - when(ctxMock.getExternalCallExecutor()).thenReturn(executor); - when(snsClientMock.publish(any(PublishRequest.class))).thenReturn(publishResultMock); - when(publishResultMock.getMessageId()).thenReturn(messageId); - when(publishResultMock.getSdkResponseMetadata()).thenReturn(responseMetadataMock); - when(responseMetadataMock.getRequestId()).thenReturn(requestId); + given(ctxMock.getExternalCallExecutor()).willReturn(executor); + given(snsClientMock.publish(any(PublishRequest.class))).willReturn(publishResultMock); + given(publishResultMock.getMessageId()).willReturn(messageId); + given(publishResultMock.getSdkResponseMetadata()).willReturn(responseMetadataMock); + given(responseMetadataMock.getRequestId()).willReturn(requestId); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); node.onMsg(ctxMock, msg); @@ -110,9 +107,9 @@ class TbSnsNodeTest { PublishRequest publishRequest = new PublishRequest() .withTopicArn(TbNodeUtils.processPattern(topicName, msg)) .withMessage(data); - verify(snsClientMock).publish(publishRequest); + then(snsClientMock).should().publish(publishRequest); ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock).tellSuccess(msgArgumentCaptor.capture()); + then(ctxMock).should().tellSuccess(msgArgumentCaptor.capture()); assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) .hasFieldOrPropertyWithValue("messageId", messageId) .hasFieldOrPropertyWithValue("requestId", requestId); @@ -128,19 +125,19 @@ class TbSnsNodeTest { } @Test - void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() throws TbNodeException, ExecutionException, InterruptedException { + void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); - when(ctxMock.getExternalCallExecutor()).thenReturn(listeningExecutor); + given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor); String errorMsg = "Something went wrong"; ListenableFuture failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg)); - when(listeningExecutor.executeAsync(any(Callable.class))).thenReturn(failedFuture); + given(listeningExecutor.executeAsync(any(Callable.class))).willReturn(failedFuture); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); + then(ctxMock).should().tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java index ed08f8badb..b2b80b19d8 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java @@ -35,8 +35,8 @@ import org.springframework.test.util.ReflectionTestUtils; 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.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.aws.sqs.TbSqsNodeConfiguration.QueueType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; @@ -47,15 +47,14 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.stream.Stream; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class TbSqsNodeTest { @@ -63,6 +62,9 @@ class TbSqsNodeTest { private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("764de824-929f-4114-95ea-0ea0401ffa3d")); private final ListeningExecutor executor = new TestDbCallbackExecutor(); + private final String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7"; + private final String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31"; + private TbSqsNode node; private TbSqsNodeConfiguration config; @@ -85,7 +87,7 @@ class TbSqsNodeTest { @Test void verifyDefaultConfig() { - assertThat(config.getQueueType()).isEqualTo(TbSqsNodeConfiguration.QueueType.STANDARD); + assertThat(config.getQueueType()).isEqualTo(QueueType.STANDARD); assertThat(config.getQueueUrlPattern()).isEqualTo("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue-name"); assertThat(config.getDelaySeconds()).isEqualTo(0); assertThat(config.getMessageAttributes()).isEqualTo(Collections.emptyMap()); @@ -96,55 +98,102 @@ class TbSqsNodeTest { @ParameterizedTest @MethodSource - void givenQueueUrlAndMsgAttributesPatternsAndQueueTypes_whenOnMsg_thenTellSuccess(String queueUrl, - TbMsgMetaData metaData, - String data, - Map attributes, - TbSqsNodeConfiguration.QueueType queueType) { - config.setAccessKeyId("accessKeyId"); - config.setSecretAccessKey("secretAccessKey"); + void givenQueueUrlPatternsAndQueueTypeIsFifo_whenOnMsg_thenVerifyRequest(String queueUrl, TbMsgMetaData metaData, String data) { + config.setQueueType(QueueType.FIFO); config.setQueueUrlPattern(queueUrl); - config.setQueueType(queueType); - config.setMessageAttributes(attributes); - String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7"; - String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31"; - String messageBodyMd5 = "msgBodyMd5-55fb8ba2-2b71-4673-a82a-969756764761"; - String messageAttributesMd5 = "msgAttrMd5-e3ba3eef-52ae-436a-bec1-0c2c2252d1f1"; - String sequenceNumber = "seqNum-bb5ddce0-cf4e-4295-b015-524bdb6a332f"; + mockSendingMsgRequest(); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); + node.onMsg(ctxMock, msg); + + SendMessageRequest sendMsgRequest = new SendMessageRequest() + .withQueueUrl(TbNodeUtils.processPattern(queueUrl, msg)) + .withMessageBody(data) + .withMessageDeduplicationId(msg.getId().toString()) + .withMessageGroupId(DEVICE_ID.toString()); + then(sqsClientMock).should().sendMessage(sendMsgRequest); + } + + private static Stream givenQueueUrlPatternsAndQueueTypeIsFifo_whenOnMsg_thenVerifyRequest() { + return Stream.of( + Arguments.of( + "https://sqs.us-east-1.amazonaws.com/123456789012/new-queue-name", + TbMsgMetaData.EMPTY, + TbMsg.EMPTY_JSON_OBJECT), + Arguments.of( + "https://sqs.us-east-1.amazonaws.com/123456789012/$[msgQueueName]", + TbMsgMetaData.EMPTY, + "{\"msgQueueName\":\"msg-queue-name\"}"), + Arguments.of( + "https://sqs.us-east-1.amazonaws.com/123456789012/${mdQueueName}", + new TbMsgMetaData(Map.of("mdQueueName", "md-queue-name")), + TbMsg.EMPTY_JSON_OBJECT) + ); + } - when(ctxMock.getExternalCallExecutor()).thenReturn(executor); - when(sqsClientMock.sendMessage(any(SendMessageRequest.class))).thenReturn(sendMessageResultMock); - when(sendMessageResultMock.getMessageId()).thenReturn(messageId); - when(sendMessageResultMock.getSdkResponseMetadata()).thenReturn(responseMetadataMock); - when(responseMetadataMock.getRequestId()).thenReturn(requestId); - when(sendMessageResultMock.getMD5OfMessageBody()).thenReturn(messageBodyMd5); - when(sendMessageResultMock.getMD5OfMessageAttributes()).thenReturn(messageAttributesMd5); - when(sendMessageResultMock.getSequenceNumber()).thenReturn(sequenceNumber); + @ParameterizedTest + @MethodSource + void givenMsgAttributesPatternsAndQueueTypeIsStandard_whenOnMsg_thenVerifyRequest(TbMsgMetaData metaData, String data, + Map attributes) { + config.setMessageAttributes(attributes); + + mockSendingMsgRequest(); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); node.onMsg(ctxMock, msg); Map messageAttributes = new HashMap<>(); - this.config.getMessageAttributes().forEach((k,v) -> { + this.config.getMessageAttributes().forEach((k, v) -> { String name = TbNodeUtils.processPattern(k, msg); String val = TbNodeUtils.processPattern(v, msg); messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val)); }); - SendMessageRequest sendMsgRequest = new SendMessageRequest() - .withQueueUrl(TbNodeUtils.processPattern(queueUrl, msg)) + SendMessageRequest sendMsgRequest = new SendMessageRequest() + .withQueueUrl(config.getQueueUrlPattern()) .withMessageBody(data) - .withMessageAttributes(messageAttributes); - if (queueType == TbSqsNodeConfiguration.QueueType.STANDARD) { - sendMsgRequest.setDelaySeconds(0); - } else { - sendMsgRequest.withMessageDeduplicationId(msg.getId().toString()); - sendMsgRequest.withMessageGroupId(DEVICE_ID.toString()); - } - verify(sqsClientMock).sendMessage(sendMsgRequest); + .withMessageAttributes(messageAttributes) + .withDelaySeconds(config.getDelaySeconds()); + then(sqsClientMock).should().sendMessage(sendMsgRequest); + } + + private static Stream givenMsgAttributesPatternsAndQueueTypeIsStandard_whenOnMsg_thenVerifyRequest() { + return Stream.of( + Arguments.of(TbMsgMetaData.EMPTY, + TbMsg.EMPTY_JSON_OBJECT, + Map.of("attributeName", "attributeValue")), + Arguments.of(TbMsgMetaData.EMPTY, + "{\"msgAttrNamePattern\":\"msgAttrName\",\"msgAttrValuePattern\":\"msgAttrValue\"}", + Map.of("$[msgAttrNamePattern]", "$[msgAttrValuePattern]")), + Arguments.of(new TbMsgMetaData(Map.of("mdAttrNamePattern", "mdAttrName", "mdAttrValuePattern", "mdAttrValue")), + TbMsg.EMPTY_JSON_OBJECT, + Map.of("${mdAttrNamePattern}", "${mdAttrValuePattern}")) + ); + } + + @Test + void givenMsgResultContainsBodyAndAttributesAndNumber_whenOnMsg_thenTellSuccess() { + String messageBodyMd5 = "msgBodyMd5-55fb8ba2-2b71-4673-a82a-969756764761"; + String messageAttributesMd5 = "msgAttrMd5-e3ba3eef-52ae-436a-bec1-0c2c2252d1f1"; + String sequenceNumber = "seqNum-bb5ddce0-cf4e-4295-b015-524bdb6a332f"; + + mockSendingMsgRequest(); + given(sendMessageResultMock.getMD5OfMessageBody()).willReturn(messageBodyMd5); + given(sendMessageResultMock.getMD5OfMessageAttributes()).willReturn(messageAttributesMd5); + given(sendMessageResultMock.getSequenceNumber()).willReturn(sequenceNumber); + + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + node.onMsg(ctxMock, msg); + + SendMessageRequest sendMsgRequest = new SendMessageRequest() + .withQueueUrl(TbNodeUtils.processPattern(config.getQueueUrlPattern(), msg)) + .withMessageBody(msg.getData()) + .withDelaySeconds(config.getDelaySeconds()); + then(sqsClientMock).should().sendMessage(sendMsgRequest); ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock).tellSuccess(msgArgumentCaptor.capture()); - assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + then(ctxMock).should().tellSuccess(msgArgumentCaptor.capture()); + Map metadata = msgArgumentCaptor.getValue().getMetaData().getData(); + assertThat(metadata) .hasFieldOrPropertyWithValue("messageId", messageId) .hasFieldOrPropertyWithValue("requestId", requestId) .hasFieldOrPropertyWithValue("messageBodyMd5", messageBodyMd5) @@ -153,47 +202,33 @@ class TbSqsNodeTest { verifyNoMoreInteractions(ctxMock, sqsClientMock, sendMessageResultMock, responseMetadataMock); } - private static Stream givenQueueUrlAndMsgAttributesPatternsAndQueueTypes_whenOnMsg_thenTellSuccess() { - return Stream.of( - Arguments.of( - "https://sqs.us-east-1.amazonaws.com/123456789012/new-queue-name", - TbMsgMetaData.EMPTY, - TbMsg.EMPTY_JSON_OBJECT, - Map.of("attributeName", "attributeValue"), - TbSqsNodeConfiguration.QueueType.STANDARD), - Arguments.of( - "https://sqs.us-east-1.amazonaws.com/123456789012/$[msgQueueName]", - TbMsgMetaData.EMPTY, - "{\"msgQueueName\":\"msg-queue-name\",\"msgAttrNamePattern\":\"msgAttrName\",\"msgAttrValuePattern\":\"msgAttrValue\"}", - Map.of("$[msgAttrNamePattern]", "$[msgAttrValuePattern]"), - TbSqsNodeConfiguration.QueueType.FIFO), - Arguments.of("https://sqs.us-east-1.amazonaws.com/123456789012/${mdQueueName}", - new TbMsgMetaData(Map.of("mdQueueName", "md-queue-name", "mdAttrNamePattern", "mdAttrName", "mdAttrValuePattern", "mdAttrValue")), - TbMsg.EMPTY_JSON_OBJECT, - Map.of("${mdAttrNamePattern}", "${mdAttrValuePattern}"), - TbSqsNodeConfiguration.QueueType.STANDARD) - ); - } - @Test - void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() throws TbNodeException, ExecutionException, InterruptedException { + void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); - when(ctxMock.getExternalCallExecutor()).thenReturn(listeningExecutor); + given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor); String errorMsg = "Something went wrong"; ListenableFuture failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg)); - when(listeningExecutor.executeAsync(any(Callable.class))).thenReturn(failedFuture); + given(listeningExecutor.executeAsync(any(Callable.class))).willReturn(failedFuture); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(ctxMock).tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); + then(ctxMock).should().tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); verifyNoMoreInteractions(ctxMock, sqsClientMock); } + private void mockSendingMsgRequest() { + given(ctxMock.getExternalCallExecutor()).willReturn(executor); + given(sqsClientMock.sendMessage(any(SendMessageRequest.class))).willReturn(sendMessageResultMock); + given(sendMessageResultMock.getMessageId()).willReturn(messageId); + given(sendMessageResultMock.getSdkResponseMetadata()).willReturn(responseMetadataMock); + given(responseMetadataMock.getRequestId()).willReturn(requestId); + } + } From 159c97ac67248216bbbd5afef39271ac2bfb9b63 Mon Sep 17 00:00:00 2001 From: Shvaika Dmytro Date: Wed, 12 Jun 2024 18:00:31 +0300 Subject: [PATCH 20/34] Rule node ids seach query: replaced Page with Slice to exclude count query execution (#10989) * replaced Page with Slice to exclude count query execution * fixed test since Slice totalPages and totalElements is always 0 --- .../server/dao/sql/rule/RuleNodeRepository.java | 7 ++++--- .../server/dao/sql/rule/JpaRuleNodeDaoTest.java | 16 +++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index fc4fff49d0..264ea88326 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -51,9 +52,9 @@ public interface RuleNodeRepository extends JpaRepository Pageable pageable); @Query("SELECT r.id FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version") - Page findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType, - @Param("version") int version, - Pageable pageable); + Slice findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType, + @Param("version") int version, + Pageable pageable); List findRuleNodesByRuleChainIdAndExternalIdIn(UUID ruleChainId, List externalIds); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java index 0fa9383ab1..dcc95b2aff 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -127,20 +126,15 @@ public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest { @Test public void testFindRuleNodeIdsByTypeAndVersionLessThan() { + // test - search text ignored PageData ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); - assertEquals(20, ruleNodeIds.getTotalElements()); - assertEquals(2, ruleNodeIds.getTotalPages()); + assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice + assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice assertEquals(10, ruleNodeIds.getData().size()); ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0)); - assertEquals(20, ruleNodeIds.getTotalElements()); - assertEquals(2, ruleNodeIds.getTotalPages()); - assertEquals(10, ruleNodeIds.getData().size()); - - // test - search text ignored - ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, StringUtils.randomAlphabetic(5))); - assertEquals(20, ruleNodeIds.getTotalElements()); - assertEquals(2, ruleNodeIds.getTotalPages()); + assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice + assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice assertEquals(10, ruleNodeIds.getData().size()); } From 17be2154ce193a65f6923f317223f89ef41c8b68 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 13 Jun 2024 16:04:55 +0300 Subject: [PATCH 21/34] fixed cassandra timeseries deletion if partition is INDEFINITE --- .../server/dao/timeseries/CassandraBaseTimeseriesDao.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 383c592a31..6700e3614a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; +import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -239,10 +240,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); - TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - final ListenableFuture> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); + final ListenableFuture> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition); Futures.addCallback(partitionsListFuture, new FutureCallback>() { @Override @@ -416,7 +415,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD readResultsProcessingExecutor); } - private ListenableFuture> getPartitionsFuture(TenantId tenantId, ReadTsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { + private ListenableFuture> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { if (isFixedPartitioning()) { //no need to fetch partitions from DB return Futures.immediateFuture(FIXED_PARTITION); } From 3ea1e5706ecdbd1ab7e5ab4bd907eaba1b999a5a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 14 Jun 2024 17:42:54 +0300 Subject: [PATCH 22/34] updated TenantProfile swagger example --- .../controller/TenantProfileController.java | 6 ++-- .../server/common/data/TenantProfile.java | 6 ++-- .../DefaultTenantProfileConfiguration.java | 29 +++++++++++++++++++ .../profile/TenantProfileConfiguration.java | 3 ++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ad1ed7824a..007f77aa20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -118,8 +118,8 @@ public class TenantProfileController extends BaseController { "Let's review the example of tenant profile data below: " + "\n\n" + MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"name\": \"Default\",\n" + - " \"description\": \"Default tenant profile\",\n" + + " \"name\": \"Your name\",\n" + + " \"description\": \"Your description\",\n" + " \"isolatedTbRuleEngine\": false,\n" + " \"profileData\": {\n" + " \"configuration\": {\n" + @@ -162,7 +162,7 @@ public class TenantProfileController extends BaseController { " \"warnThreshold\": 0\n" + " }\n" + " },\n" + - " \"default\": true\n" + + " \"default\": false\n" + "}" + MARKDOWN_CODE_BLOCK_END + "Remove 'id', from the request body example (below) to create new Tenant Profile entity." + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index e60091a16d..9fe969f58d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.media.Schema; @@ -51,10 +52,11 @@ public class TenantProfile extends BaseData implements HasName @NoXss @Schema(description = "Description of the tenant profile", example = "Any text") private String description; - @Schema(description = "Default Tenant profile to be used.", example = "true") + @Schema(description = "Default Tenant profile to be used.", example = "false") + @JsonProperty("default") private boolean isDefault; @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + - "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false") private boolean isolatedTbRuleEngine; @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 40f4b7f42b..51738c75be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.tenant.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfileType; +@Schema @AllArgsConstructor @NoArgsConstructor @Builder @@ -41,34 +43,61 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxOtaPackagesInBytes; private long maxResourceSize; + @Schema(example = "1000:1,20000:60") private String transportTenantMsgRateLimit; + @Schema(example = "1000:1,20000:60") private String transportTenantTelemetryMsgRateLimit; + @Schema(example = "1000:1,20000:60") private String transportTenantTelemetryDataPointsRateLimit; + @Schema(example = "20:1,600:60") private String transportDeviceMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportDeviceTelemetryMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportDeviceTelemetryDataPointsRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayTelemetryMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayTelemetryDataPointsRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayDeviceMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayDeviceTelemetryMsgRateLimit; + @Schema(example = "20:1,600:60") private String transportGatewayDeviceTelemetryDataPointsRateLimit; + @Schema(example = "20:1,600:60") private String tenantEntityExportRateLimit; + @Schema(example = "20:1,600:60") private String tenantEntityImportRateLimit; + @Schema(example = "20:1,600:60") private String tenantNotificationRequestsRateLimit; + @Schema(example = "20:1,600:60") private String tenantNotificationRequestsPerRuleRateLimit; + @Schema(example = "10000000") private long maxTransportMessages; + @Schema(example = "10000000") private long maxTransportDataPoints; + @Schema(example = "4000000") private long maxREExecutions; + @Schema(example = "5000000") private long maxJSExecutions; + @Schema(example = "5000000") private long maxTbelExecutions; + @Schema(example = "0") private long maxDPStorageDays; + @Schema(example = "50") private int maxRuleNodeExecutionsPerMessage; + @Schema(example = "0") private long maxEmails; + @Schema(example = "true") private Boolean smsEnabled; + @Schema(example = "0") private long maxSms; + @Schema(example = "1000") private long maxCreatedAlarms; private String tenantServerRestLimitsConfiguration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java index d3238b20f8..0662765455 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.TenantProfileType; @@ -31,6 +33,7 @@ import java.io.Serializable; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")}) +@Schema(discriminatorProperty = "type", discriminatorMapping = {@DiscriminatorMapping(value = "DEFAULT", schema = DefaultTenantProfileConfiguration.class)}) public interface TenantProfileConfiguration extends Serializable { @JsonIgnore From 93e62dd1e84c9c63fc496ea2f8b137695a5ff1d2 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 14 Jun 2024 18:13:05 +0300 Subject: [PATCH 23/34] Handle TenantNotFoundException for stale subscriptions (#10973) * Handle TenantNotFoundException for stale subscriptions * Minor refactoring for stale subs cleanup --- .../DefaultTbLocalSubscriptionService.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 8abb78d9c1..100636ad5d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.subscription; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; @@ -24,9 +26,9 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -51,8 +53,6 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscript import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -143,7 +143,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer * Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache * Since number of subscriptions is usually much less than number of devices that are pushing data. */ - subscriptionsByEntityId.values().forEach(sub -> pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED))); + Set staleSubs = new HashSet<>(); + subscriptionsByEntityId.forEach((id, sub) -> { + try { + pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED)); + } catch (TenantNotFoundException e) { + staleSubs.add(id); + log.warn("Cleaning up stale subscription {} for tenant {} due to TenantNotFoundException", id, sub.getTenantId()); + } catch (Exception e) { + log.error("Failed to push subscription {} to manager service", sub, e); + } + }); + if (!staleSubs.isEmpty()) { + staleSubs.forEach(subscriptionsByEntityId::remove); + } } } From 1df93833cb94fc4b3c781f598e2bb2c790c739bf Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 17 Jun 2024 12:49:28 +0300 Subject: [PATCH 24/34] Refactor stale subs cleanup --- .../DefaultTbLocalSubscriptionService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 100636ad5d..9367439d4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -155,7 +155,15 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } }); if (!staleSubs.isEmpty()) { - staleSubs.forEach(subscriptionsByEntityId::remove); + subsLock.lock(); + try { + staleSubs.forEach(entityId -> { + subscriptionsByEntityId.remove(entityId); + entityUpdates.remove(entityId); + }); + } finally { + subsLock.unlock(); + } } } } From c4e5ab65ee73a604cab2d374c98b232d42390517 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 17 Jun 2024 15:50:16 +0300 Subject: [PATCH 25/34] Update pkg.upgradeVersion --- msa/tb/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 70fea2c0a5..8b4a6d0747 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -38,7 +38,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.6.4 + 3.7.0 From b7891dfdd9940bea7026eaed296d029072b5d4a7 Mon Sep 17 00:00:00 2001 From: Kulikov <44275303+nickAS21@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:45:04 +0300 Subject: [PATCH 26/34] fix_bug: lwm2m monitoring (#11025) --- .../monitoring/client/Lwm2mClient.java | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java index 4b95ae1aac..381aac30a0 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -30,10 +30,12 @@ import org.eclipse.leshan.client.californium.LeshanClientBuilder; import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.californium.EndpointFactory; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.LwM2mModel; @@ -42,6 +44,10 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.DeregisterRequest; +import org.eclipse.leshan.core.request.RegisterRequest; +import org.eclipse.leshan.core.request.UpdateRequest; import org.eclipse.leshan.core.response.ReadResponse; import org.thingsboard.monitoring.util.ResourceUtils; @@ -96,7 +102,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); initializer.setInstancesForObject(SECURITY, security); - initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60))); + initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(5))); initializer.setInstancesForObject(DEVICE, this); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); @@ -139,6 +145,89 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { builder.setEncoder(new DefaultLwM2mEncoder(false)); leshanClient = builder.build(); + LwM2mClientObserver observer = new LwM2mClientObserver() { + + @Override + public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {} + + @Override + public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {} + + @Override + public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, + ResponseCode responseCode, String errorMessage, Exception cause) {} + + @Override + public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {} + + @Override + public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) { + log.debug("onRegistrationStarted [{}]", request.getEndpointName()); + } + + @Override + public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) { + log.debug("onRegistrationSuccess [{}] [{}]", request.getEndpointName(), registrationID); + } + + @Override + public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.debug("onRegistrationFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage); + } + + @Override + public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) { + log.debug("onRegistrationTimeout [{}]", request.getEndpointName()); + } + + @Override + public void onUpdateStarted(ServerIdentity server, UpdateRequest request) { + log.debug("onUpdateStarted [{}]", request.getRegistrationId()); + } + + @Override + public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) { + log.debug("onUpdateSuccess [{}]", request.getRegistrationId()); + } + + @Override + public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.debug("onUpdateFailure [{}]", request.getRegistrationId()); + } + + @Override + public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) { + log.debug("onUpdateTimeout [{}]", request.getRegistrationId()); + } + + @Override + public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) { + log.debug("onDeregistrationStarted [{}]", request.getRegistrationId()); + } + + @Override + public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) { + log.debug("onDeregistrationStarted [{}]", request.getRegistrationId()); + } + + @Override + public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.debug("onDeregistrationFailure [{}] [{}] [{}]", request.getRegistrationId(), responseCode, errorMessage); + } + + @Override + public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) { + log.debug("onDeregistrationTimeout [{}]", request.getRegistrationId()); + } + + @Override + public void onUnexpectedError(Throwable unexpectedError) { + log.debug("onUnexpectedError [{}]", unexpectedError.toString()); + } + + }; + leshanClient.addObserver(observer); + setLeshanClient(leshanClient); leshanClient.start(); From e0e5fa12c9f9a10d2b08dd6af40ab0311add8613 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Jun 2024 12:28:20 +0300 Subject: [PATCH 27/34] "Request Timeout." -> "Request timeout" --- .../components/widget/lib/rpc/persistent-table.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index 0ae1bdc4f5..37879144cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -517,7 +517,7 @@ class PersistentDatasource implements DataSource { if (!this.executingRpcRequest || rejection.status === 504) { this.subscription.rpcRejection = rejection; if (rejection.status === 504) { - this.subscription.rpcErrorText = 'Request Timeout.'; + this.subscription.rpcErrorText = 'Request timeout'; } else { this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; const error = parseHttpErrorMessage(rejection, this.translate); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 5106b622dd..695c2454ae 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4088,7 +4088,7 @@ "target-device-is-not-set": "Target device is not set!", "invalid-target-entity": "RPC commands are not supported by {{entityType}} entity.", "failed-to-resolve-target-device": "Failed to resolve target device!", - "request-timeout": "Request Timeout.", + "request-timeout": "Request timeout", "rpc-http-error": "Error: {{status}} - {{statusText}}" } }, From 7024035204317e884a84a6f29059f728dc62bf65 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 19 Jun 2024 13:40:12 +0300 Subject: [PATCH 28/34] added tests for the destroy method --- .../rule/engine/aws/sns/TbSnsNodeTest.java | 52 +++++++++++++++---- .../rule/engine/aws/sqs/TbSqsNodeTest.java | 51 ++++++++++++++---- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java index 2b67187687..09787d3da8 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java @@ -37,6 +37,7 @@ import org.thingsboard.rule.engine.api.TbContext; 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; @@ -47,10 +48,12 @@ 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.then; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) class TbSnsNodeTest { @@ -88,7 +91,8 @@ class TbSnsNodeTest { @ParameterizedTest @MethodSource - void givenTopicNamePattern_whenOnMsg_thenTellSuccess(String topicName, TbMsgMetaData metaData, String data) { + void givenForceAckIsTrueAndTopicNamePattern_whenOnMsg_thenEnqueueForTellNext(String topicName, TbMsgMetaData metaData, String data) { + ReflectionTestUtils.setField(node, "forceAck", true); config.setAccessKeyId("accessKeyId"); config.setSecretAccessKey("secretAccessKey"); config.setTopicArnPattern(topicName); @@ -104,19 +108,25 @@ class TbSnsNodeTest { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); node.onMsg(ctxMock, msg); + then(ctxMock).should().ack(msg); PublishRequest publishRequest = new PublishRequest() .withTopicArn(TbNodeUtils.processPattern(topicName, msg)) .withMessage(data); then(snsClientMock).should().publish(publishRequest); - ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(ctxMock).should().tellSuccess(msgArgumentCaptor.capture()); - assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + ArgumentCaptor actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().enqueueForTellNext(actualMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS)); + TbMsg actualMsg = actualMsgCaptor.getValue(); + assertThat(actualMsg) + .usingRecursiveComparison() + .ignoringFields("metaData", "ctx") + .isEqualTo(msg); + assertThat(actualMsg.getMetaData().getData()) .hasFieldOrPropertyWithValue("messageId", messageId) .hasFieldOrPropertyWithValue("requestId", requestId); verifyNoMoreInteractions(ctxMock, snsClientMock, publishResultMock, responseMetadataMock); } - private static Stream givenTopicNamePattern_whenOnMsg_thenTellSuccess() { + private static Stream givenForceAckIsTrueAndTopicNamePattern_whenOnMsg_thenEnqueueForTellNext() { return Stream.of( Arguments.of("arn:aws:sns:us-east-1:123456789012:NewTopic", TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT), Arguments.of("arn:aws:sns:us-east-1:123456789012:$[msgTopicName]", TbMsgMetaData.EMPTY, "{\"msgTopicName\":\"msg-topic-name\"}"), @@ -125,7 +135,8 @@ class TbSnsNodeTest { } @Test - void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { + void givenForceAckIsFalseAndErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { + ReflectionTestUtils.setField(node, "forceAck", false); ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor); String errorMsg = "Something went wrong"; @@ -135,12 +146,31 @@ class TbSnsNodeTest { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); - ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class)); + ArgumentCaptor actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - then(ctxMock).should().tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); - assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + then(ctxMock).should().tellFailure(actualMsgCaptor.capture(), throwableCaptor.capture()); + TbMsg actualMsg = actualMsgCaptor.getValue(); + assertThat(actualMsg) + .usingRecursiveComparison() + .ignoringFields("metaData", "ctx") + .isEqualTo(msg); + assertThat(actualMsg.getMetaData().getData()) .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); verifyNoMoreInteractions(ctxMock, snsClientMock); } + + @Test + void givenSnsClientIsNotNull_whenDestroy_thenShutdown() { + node.destroy(); + then(snsClientMock).should().shutdown(); + } + + @Test + void givenSnsClientIsNull_whenDestroy_thenVerifyNoInteractions() { + ReflectionTestUtils.setField(node, "snsClient", null); + node.destroy(); + then(snsClientMock).shouldHaveNoInteractions(); + } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java index b2b80b19d8..48f42edb97 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java @@ -39,6 +39,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.aws.sqs.TbSqsNodeConfiguration.QueueType; 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; @@ -51,10 +52,12 @@ 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.then; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) class TbSqsNodeTest { @@ -172,7 +175,8 @@ class TbSqsNodeTest { } @Test - void givenMsgResultContainsBodyAndAttributesAndNumber_whenOnMsg_thenTellSuccess() { + void givenForceAckIsTrueAndMsgResultContainsBodyAndAttributesAndNumber_whenOnMsg_thenEnqueueForTellNext() { + ReflectionTestUtils.setField(node, "forceAck", true); String messageBodyMd5 = "msgBodyMd5-55fb8ba2-2b71-4673-a82a-969756764761"; String messageAttributesMd5 = "msgAttrMd5-e3ba3eef-52ae-436a-bec1-0c2c2252d1f1"; String sequenceNumber = "seqNum-bb5ddce0-cf4e-4295-b015-524bdb6a332f"; @@ -185,15 +189,20 @@ class TbSqsNodeTest { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); + then(ctxMock).should().ack(msg); SendMessageRequest sendMsgRequest = new SendMessageRequest() .withQueueUrl(TbNodeUtils.processPattern(config.getQueueUrlPattern(), msg)) .withMessageBody(msg.getData()) .withDelaySeconds(config.getDelaySeconds()); then(sqsClientMock).should().sendMessage(sendMsgRequest); - ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); - then(ctxMock).should().tellSuccess(msgArgumentCaptor.capture()); - Map metadata = msgArgumentCaptor.getValue().getMetaData().getData(); - assertThat(metadata) + ArgumentCaptor actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().enqueueForTellNext(actualMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS)); + TbMsg actualMsg = actualMsgCaptor.getValue(); + assertThat(actualMsg) + .usingRecursiveComparison() + .ignoringFields("metaData", "ctx") + .isEqualTo(msg); + assertThat(actualMsg.getMetaData().getData()) .hasFieldOrPropertyWithValue("messageId", messageId) .hasFieldOrPropertyWithValue("requestId", requestId) .hasFieldOrPropertyWithValue("messageBodyMd5", messageBodyMd5) @@ -203,7 +212,8 @@ class TbSqsNodeTest { } @Test - void givenErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { + void givenForceAckIsFalseAndErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() { + ReflectionTestUtils.setField(node, "forceAck", false); ListeningExecutor listeningExecutor = mock(ListeningExecutor.class); given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor); String errorMsg = "Something went wrong"; @@ -214,15 +224,34 @@ class TbSqsNodeTest { TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); node.onMsg(ctxMock, msg); - ArgumentCaptor msgArgumentCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class)); + ArgumentCaptor actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - then(ctxMock).should().tellFailure(msgArgumentCaptor.capture(), throwableCaptor.capture()); - assertThat(msgArgumentCaptor.getValue().getMetaData().getData()) + then(ctxMock).should().tellFailure(actualMsgCaptor.capture(), throwableCaptor.capture()); + TbMsg actualMsg = actualMsgCaptor.getValue(); + assertThat(actualMsg) + .usingRecursiveComparison() + .ignoringFields("metaData", "ctx") + .isEqualTo(msg); + assertThat(actualMsg.getMetaData().getData()) .hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg); assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); verifyNoMoreInteractions(ctxMock, sqsClientMock); } + @Test + void givenSqsClientIsNotNull_whenDestroy_thenShutdown() { + node.destroy(); + then(sqsClientMock).should().shutdown(); + } + + @Test + void givenSqsClientIsNull_whenDestroy_thenVerifyNoInteractions() { + ReflectionTestUtils.setField(node, "sqsClient", null); + node.destroy(); + then(sqsClientMock).shouldHaveNoInteractions(); + } + private void mockSendingMsgRequest() { given(ctxMock.getExternalCallExecutor()).willReturn(executor); given(sqsClientMock.sendMessage(any(SendMessageRequest.class))).willReturn(sendMessageResultMock); From fb47a95bf6d44b788a939deb3576e2c666038876 Mon Sep 17 00:00:00 2001 From: Kulikov <44275303+nickAS21@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:03:59 +0300 Subject: [PATCH 29/34] Add tests for LwM2M device profile validation (#10987) * fix_bug: lwm2m Bootstrap ShortServerId add tests * fix_bug: lwm2m Bootstrap ShortServerId add tests comments1 * fix_bug: lwm2m monitoring * fix_bug: lwm2m Bootstrap tests n- formatting --- .../DeviceProfileDataValidatorTest.java | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java index 2da8a7ff45..5a4b20d7a8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java @@ -45,6 +45,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.verify; @@ -73,6 +74,9 @@ class DeviceProfileDataValidatorTest { " \"clientOnlyObserveAfterConnect\": 1\n" + " }"; + private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!"; + private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!"; + private static final String msgErrorNotNull = " Server ShortServerId must not be null!"; private static final String host = "localhost"; private static final String hostBs = "localhost"; @@ -116,19 +120,50 @@ class DeviceProfileDataValidatorTest { validator.validateDataImpl(tenantId, deviceProfile); verify(validator).validateString("Device profile name", deviceProfile.getName()); } + @Test void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() { Integer shortServerId = 123; Integer shortServerIdBs = 0; - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = - getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs)); - DeviceProfile deviceProfile = getDeviceProfile(transportConfiguration); + DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); validator.validateDataImpl(tenantId, deviceProfile); verify(validator).validateString("Device profile name", deviceProfile.getName()); } - private DeviceProfile getDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) { + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() { + verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull); + } + + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_More_65535_Error() { + verifyValidationError(123, 65536, msgErrorBsRange); + } + + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_Less_0_Error() { + verifyValidationError(123, -1, msgErrorBsRange); + } + + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_null_Error_BootstrapShortServerId_Ok() { + verifyValidationError(null, 1, "LwM2M" + msgErrorNotNull); + } + + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() { + verifyValidationError(65535, 111, msgErrorLwm2mRange); + } + + @Test + void testValidateDeviceProfile_Lwm2mShortServerId_Less_1_Error_BootstrapShortServerId_Ok() { + verifyValidationError(0, 111, msgErrorLwm2mRange); + } + + private DeviceProfile getDeviceProfile(Integer shortServerId, Integer shortServerIdBs) { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = + getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs)); DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName("default"); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -151,7 +186,7 @@ class DeviceProfileDataValidatorTest { return transportConfiguration; } - private List getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs){ + private List getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs) { List bootstrap = new ArrayList<>(); bootstrap.add(getBootstrapServerCredentialNoSec(false, shortServerId, shortServerIdBs)); bootstrap.add(getBootstrapServerCredentialNoSec(true, shortServerId, shortServerIdBs)); @@ -168,4 +203,11 @@ class DeviceProfileDataValidatorTest { return bootstrapServerCredential; } + private void verifyValidationError(Integer shortServerId, Integer shortServerIdBs, String msgError) { + DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); + assertThatThrownBy(() -> validator.validateDataImpl(tenantId, deviceProfile)) + .hasMessageContaining(msgError); + + } + } From 468c00788268b6680b42485b9e78ec028e83277a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 20 Jun 2024 15:53:41 +0300 Subject: [PATCH 30/34] Version set to 3.7.1-SNAPSHOT --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 59 files changed, 60 insertions(+), 60 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 902b541566..da2c0bc249 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 77ea283827..e8e9f9b9c5 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 8a95b2a16d..cfff3d8dba 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index eda43510b5..8851b99583 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 59640b586d..ddfcaf38e3 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 8b3d7f7f5f..3d3eae4679 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index 4cd63dbc1f..91176d088f 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index cf3134f09b..19ed1e0b40 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 6f650c127b..64b46a9dd2 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index b12140012d..d3cd3d6928 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 06ae7293ba..57652f2645 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index cd77d9da07..54d68afe62 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index b307d328d2..bef66f2c96 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index d196dd13ea..07dec58d66 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index b760129479..b6d6731067 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 23c507cc43..5eb96a1e1a 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 48bd1cfe9c..5dff87a05b 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index e27f7cbc1b..d5946c8545 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 0ec3492c99..d3cd755545 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 952b94ea2f..023b9473dc 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 0c5c7a02c9..368dcec7d8 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 8ad570fceb..96d714f032 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 20d0f6e5d2..d1fead6a4a 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index c4cee40e0d..971357ea40 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index cce6e00501..ec913318a7 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 061c130a69..6aa47ec4bd 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard dao diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 2dd8e54f83..2ed55e141f 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 4c81e0f31f..ce4de7af17 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index d0864c013a..b7871628f8 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.7.0", + "version": "3.7.1", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 8d3592cbe2..8f67cff304 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 60ff2e3f4a..9c6ff99615 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 71ddc91412..9faf269a20 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 708af73ae0..3dab1c01da 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 8b4a6d0747..b799572468 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 5c11fd7adf..fbe4677b8f 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index b7cdae9c95..efedec74ab 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 19ea2a022c..f3c05af81a 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index f06777265e..a6d4e265a4 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 13ee09c3a7..cd6685b7a5 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 35b3809aae..350f49e7fb 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index e70656c49b..8111c1370d 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 081ebd1d3c..e4f4e0afe9 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 6e7cdd4bc1..6bd7266dd8 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.7.0", + "version": "3.7.1", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 103a498a61..e592b1a8e9 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index b0111b9bcd..7f06a09be2 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard netty-mqtt - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index f75e782bef..13125c67f9 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index e4a959a7ec..7e38a4119d 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 8902ef0cee..1448fa650e 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 743baf1ab4..8fe01acdff 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index de48596d70..5ac1421cdb 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 38309f35b0..dfd750aa4f 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index c2c0252d9c..bdd27ae21c 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 7aaf88b59e..b48c3d801d 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index d0b961939a..be11fbdf2f 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index fe2c2be544..a10d3ff9ff 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 49009b83b0..6b1311b949 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 5f236adb9a..a02cd85c58 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b8a8e4c9a9..f383054ece 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.7.0", + "version": "3.7.1", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index c8b50c4149..931df3a6bc 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.7.0-SNAPSHOT + 3.7.1-SNAPSHOT thingsboard org.thingsboard From 727c9b8fbc47687b37ab4c96bc4431ae9590f1e6 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 20 Jun 2024 18:34:06 +0300 Subject: [PATCH 31/34] Edge install instructions update. Use Java 17. Fix tag usage --- .../install/centos/instructions.md | 14 ++-- .../install/ubuntu/instructions.md | 14 ++-- ...EdgeInstallUpgradeInstructionsService.java | 65 +++++++++++++++++ ...DefaultEdgeInstallInstructionsService.java | 71 +++++-------------- ...DefaultEdgeUpgradeInstructionsService.java | 55 ++++---------- 5 files changed, 110 insertions(+), 109 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java diff --git a/application/src/main/data/json/edge/instructions/install/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md index 22bb3f63a0..bcc1967dfd 100644 --- a/application/src/main/data/json/edge/instructions/install/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -8,15 +8,15 @@ sudo yum install -y nano wget sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm ``` -#### Install Java 11 (OpenJDK) -ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: +#### Install Java 17 (OpenJDK) +ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17: ```bash -sudo yum install java-11-openjdk +sudo yum install java-17-openjdk {:copy-code} ``` -Please don't forget to configure your operating system to use OpenJDK 11 by default. +Please don't forget to configure your operating system to use OpenJDK 17 by default. You can configure which version is the default using the following command: ```bash @@ -34,7 +34,7 @@ java -version Expected command output is: ```text -openjdk version "11.0.xx" +openjdk version "17.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` @@ -144,14 +144,14 @@ CREATE DATABASE tb_edge; Download installation package: ```bash -wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.rpm +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` Go to the download repository and install ThingsBoard Edge service: ```bash -sudo rpm -Uvh tb-edge-${TB_EDGE_VERSION}.rpm +sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index 6fcdcd50f2..fe9601443f 100644 --- a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,15 +1,15 @@ Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. -#### Install Java 11 (OpenJDK) -ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: +#### Install Java 17 (OpenJDK) +ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17: ```bash sudo apt update -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk {:copy-code} ``` -Please don't forget to configure your operating system to use OpenJDK 11 by default. +Please don't forget to configure your operating system to use OpenJDK 17 by default. You can configure which version is the default using the following command: ```bash @@ -27,7 +27,7 @@ java -version Expected command output is: ```text -openjdk version "11.0.xx" +openjdk version "17.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` @@ -76,14 +76,14 @@ CREATE DATABASE tb_edge; Download installation package: ```bash -wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.deb +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` Go to the download repository and install ThingsBoard Edge service: ```bash -sudo dpkg -i tb-edge-${TB_EDGE_VERSION}.deb +sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java new file mode 100644 index 0000000000..c82f7babc5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java @@ -0,0 +1,65 @@ +/** + * 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.server.service.edge.instructions; + +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.server.service.install.InstallScripts; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Slf4j +@RequiredArgsConstructor +public abstract class BaseEdgeInstallUpgradeInstructionsService { + + private static final String EDGE_DIR = "edge"; + private static final String INSTRUCTIONS_DIR = "instructions"; + + private final InstallScripts installScripts; + + @Value("${app.version:unknown}") + @Setter + protected String appVersion; + + protected String readFile(Path file) { + try { + return Files.readString(file); + } catch (IOException e) { + log.warn("Failed to read file: {}", file, e); + throw new RuntimeException(e); + } + } + + protected String getTagVersion(String version) { + return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version; + } + + protected Path resolveFile(String subDir, String... subDirs) { + return getEdgeInstructionsDir().resolve(Paths.get(subDir, subDirs)); + } + + protected Path getEdgeInstructionsDir() { + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, getBaseDirName()); + } + + protected abstract String getBaseDirName(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index fd0d9fc2dd..64f96aa4e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -15,8 +15,7 @@ */ package org.thingsboard.server.service.edge.instructions; -import lombok.RequiredArgsConstructor; -import lombok.Setter; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -27,47 +26,32 @@ import org.thingsboard.server.dao.util.DeviceConnectivityUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - @Service @Slf4j -@RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstructionsService { +public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeInstallInstructionsService { - private static final String EDGE_DIR = "edge"; - private static final String INSTRUCTIONS_DIR = "instructions"; private static final String INSTALL_DIR = "install"; - private final InstallScripts installScripts; - @Value("${edges.rpc.port}") private int rpcPort; @Value("${edges.rpc.ssl.enabled}") private boolean sslEnabled; - @Value("${app.version:unknown}") - @Setter - private String appVersion; + public DefaultEdgeInstallInstructionsService(InstallScripts installScripts) { + super(installScripts); + } @Override public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) { - switch (installationMethod.toLowerCase()) { - case "docker": - return getDockerInstallInstructions(edge, request); - case "ubuntu": - return getUbuntuInstallInstructions(edge, request); - case "centos": - return getCentosInstallInstructions(edge, request); - default: - throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod); - } + return switch (installationMethod.toLowerCase()) { + case "docker" -> getDockerInstallInstructions(edge, request); + case "ubuntu", "centos" -> getLinuxInstallInstructions(edge, request, installationMethod.toLowerCase()); + default -> + throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod); + }; } private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { @@ -88,25 +72,16 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc return new EdgeInstructions(dockerInstallInstructions); } - private EdgeInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { - String ubuntuInstallInstructions = readFile(resolveFile("ubuntu", "instructions.md")); + private EdgeInstructions getLinuxInstallInstructions(Edge edge, HttpServletRequest request, String os) { + String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md")); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); String edgeVersion = appVersion.replace("-SNAPSHOT", ""); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); return new EdgeInstructions(ubuntuInstallInstructions); } - - private EdgeInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) { - String centosInstallInstructions = readFile(resolveFile("centos", "instructions.md")); - centosInstallInstructions = replacePlaceholders(centosInstallInstructions, edge); - centosInstallInstructions = centosInstallInstructions.replace("${BASE_URL}", request.getServerName()); - String edgeVersion = appVersion.replace("-SNAPSHOT", ""); - centosInstallInstructions = centosInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - return new EdgeInstructions(centosInstallInstructions); - } - private String replacePlaceholders(String instructions, Edge edge) { instructions = instructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); instructions = instructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret()); @@ -115,20 +90,8 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc return instructions; } - private String readFile(Path file) { - try { - return Files.readString(file); - } catch (IOException e) { - log.warn("Failed to read file: {}", file, e); - throw new RuntimeException(e); - } - } - - private Path resolveFile(String subDir, String... subDirs) { - return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); - } - - private Path getEdgeInstallInstructionsDir() { - return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, INSTALL_DIR); + @Override + protected String getBaseDirName() { + return INSTALL_DIR; } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 3ff8ae784a..7113123a45 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edge.instructions; -import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -32,47 +31,37 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; @Service @Slf4j -@RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { +public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeUpgradeInstructionsService { private static final Map upgradeVersionHashMap = new HashMap<>(); - private static final String EDGE_DIR = "edge"; - private static final String INSTRUCTIONS_DIR = "instructions"; private static final String UPGRADE_DIR = "upgrade"; - private final InstallScripts installScripts; private final AttributesService attributesService; - @Value("${app.version:unknown}") - @Setter - private String appVersion; + public DefaultEdgeUpgradeInstructionsService(AttributesService attributesService, InstallScripts installScripts) { + super(installScripts); + this.attributesService = attributesService; + } @Override public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { String tbVersion = appVersion.replace("-SNAPSHOT", ""); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); - switch (upgradeMethod.toLowerCase()) { - case "docker": - return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); - case "ubuntu": - case "centos": - return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); - default: - throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); - } + return switch (upgradeMethod.toLowerCase()) { + case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); + case "ubuntu", "centos" -> + getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); + }; } @Override @@ -167,28 +156,12 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc return new EdgeInstructions(result.toString()); } - private String getTagVersion(String version) { - return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version; - } - private String convertEdgeVersionToDocsFormat(String edgeVersion) { return edgeVersion.replace("_", ".").substring(2); } - private String readFile(Path file) { - try { - return Files.readString(file); - } catch (IOException e) { - log.warn("Failed to read file: {}", file, e); - throw new RuntimeException(e); - } - } - - private Path resolveFile(String subDir, String... subDirs) { - return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); - } - - private Path getEdgeInstallInstructionsDir() { - return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, UPGRADE_DIR); + @Override + protected String getBaseDirName() { + return UPGRADE_DIR; } } From ac6622492018aa56b46a590f5778886f3d6e16c1 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 21 Jun 2024 19:14:58 +0300 Subject: [PATCH 32/34] UI: Implement 'Label card' and 'Label & value card' widgets. --- .../json/system/widget_bundles/cards.json | 5 +- .../widget_types/label___value_card.json | 23 ++ .../json/system/widget_types/label_card.json | 25 ++ .../basic/basic-widget-config.module.ts | 12 +- .../label-card-basic-config.component.html | 88 +++++++ .../label-card-basic-config.component.ts | 130 +++++++++++ .../cards/label-card-widget.component.html | 30 +++ .../cards/label-card-widget.component.scss | 70 ++++++ .../lib/cards/label-card-widget.component.ts | 170 ++++++++++++++ .../lib/cards/label-card-widget.models.ts | 60 +++++ .../label-value-card-widget.component.html | 34 +++ .../label-value-card-widget.component.scss | 80 +++++++ .../label-value-card-widget.component.ts | 220 ++++++++++++++++++ .../cards/label-value-card-widget.models.ts | 80 +++++++ .../label-card-widget-settings.component.html | 73 ++++++ .../label-card-widget-settings.component.ts | 84 +++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../widget/widget-components.module.ts | 10 +- .../assets/locale/locale.constant-en_US.json | 6 + 19 files changed, 1202 insertions(+), 10 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/label___value_card.json create mode 100644 application/src/main/data/json/system/widget_types/label_card.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index e0f4f4c615..84f8cd9785 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -2,10 +2,9 @@ "widgetsBundle": { "alias": "cards", "title": "Cards", - "image": "data:image/png;base64,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", + "image": "tb-image:Y2FyZHNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IkNhcmRzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,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", "description": "Includes cards that display dynamic content based on data from one or more entities. It also includes static HTML cards.", "order": 2000, - "externalId": null, "name": "Cards" }, "widgetTypeFqns": [ @@ -13,6 +12,8 @@ "cards.horizontal_value_card", "cards.aggregated_value_card", "simple_value_and_chart_card", + "label_card", + "label_value_card", "progress_bar", "cards.label_widget", "cards.dashboard_state_widget", diff --git a/application/src/main/data/json/system/widget_types/label___value_card.json b/application/src/main/data/json/system/widget_types/label___value_card.json new file mode 100644 index 0000000000..57e26d3050 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/label___value_card.json @@ -0,0 +1,23 @@ +{ + "fqn": "label_value_card", + "name": "Label & value card", + "deprecated": false, + "image": null, + "description": "Displays a single entity attribute or the latest telemetry with optional label and icon.", + "descriptor": { + "type": "latest", + "sizeX": 4.5, + "sizeY": 1, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.labelValueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.labelValueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "hasBasicMode": false, + "basicModeDirective": "", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Label & value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + }, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/label_card.json b/application/src/main/data/json/system/widget_types/label_card.json new file mode 100644 index 0000000000..51eb9d3360 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/label_card.json @@ -0,0 +1,25 @@ +{ + "fqn": "label_card", + "name": "Label card", + "deprecated": false, + "image": "tb-image:bGFiZWxDYXJkLnN2Zw==:IkxhYmVsIGNhcmQiIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEyIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxMiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDk3OF80NTAzOSkiPgo8cmVjdCB4PSI2IiB5PSI1MiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI1NiIgcng9IjQiIGZpbGw9IndoaXRlIi8+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIxX2JfNDk3OF80NTAzOSkiPgo8cmVjdCB4PSIxOCIgeT0iNjQiIHdpZHRoPSIxNzYiIGhlaWdodD0iMzIiIHJ4PSI0IiBmaWxsPSJ3aGl0ZSIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjwvZz4KPHBhdGggZD0iTTU2LjUgODFWNzNDNTYuNSA3MS4zNCA1NS4xNiA3MCA1My41IDcwQzUxLjg0IDcwIDUwLjUgNzEuMzQgNTAuNSA3M1Y4MUM0OS4yOSA4MS45MSA0OC41IDgzLjM3IDQ4LjUgODVDNDguNSA4Ny43NiA1MC43NCA5MCA1My41IDkwQzU2LjI2IDkwIDU4LjUgODcuNzYgNTguNSA4NUM1OC41IDgzLjM3IDU3LjcxIDgxLjkxIDU2LjUgODFaTTUyLjUgNzNDNTIuNSA3Mi40NSA1Mi45NSA3MiA1My41IDcyQzU0LjA1IDcyIDU0LjUgNzIuNDUgNTQuNSA3M0g1My41Vjc0SDU0LjVWNzZINTMuNVY3N0g1NC41Vjc5SDUyLjVWNzNaIiBmaWxsPSIjNTQ2OUZGIi8+CjxwYXRoIGQ9Ik03OC42MDY0IDc1LjA0NjlWODVINzYuOTA0M1Y3NS4wNDY5SDc4LjYwNjRaTTgxLjczMDUgNzUuMDQ2OVY3Ni40MTQxSDczLjgwNzZWNzUuMDQ2OUg4MS43MzA1Wk04NC43NTU5IDc0LjVWODVIODMuMTE1MlY3NC41SDg0Ljc1NTlaTTg0LjQ2ODggODEuMDI4M0w4My45MzU1IDgxLjAyMTVDODMuOTQwMSA4MC41MTExIDg0LjAxMDcgODAuMDM5NCA4NC4xNDc1IDc5LjYwNjRDODQuMjg4NyA3OS4xNzM1IDg0LjQ4NDcgNzguNzk3NSA4NC43MzU0IDc4LjQ3ODVDODQuOTkwNiA3OC4xNTQ5IDg1LjI5NTkgNzcuOTA2NiA4NS42NTE0IDc3LjczMzRDODYuMDA2OCA3Ny41NTU3IDg2LjQwMSA3Ny40NjY4IDg2LjgzNCA3Ny40NjY4Qzg3LjE5ODYgNzcuNDY2OCA4Ny41MjY3IDc3LjUxNjkgODcuODE4NCA3Ny42MTcyQzg4LjExNDYgNzcuNzE3NCA4OC4zNjk4IDc3Ljg3OTIgODguNTg0IDc4LjEwMjVDODguNzk4MiA3OC4zMjEzIDg4Ljk2IDc4LjYwODQgODkuMDY5MyA3OC45NjM5Qzg5LjE4MzMgNzkuMzE0OCA4OS4yNDAyIDc5Ljc0MzIgODkuMjQwMiA4MC4yNDlWODVIODcuNTg1OVY4MC4yMzU0Qzg3LjU4NTkgNzkuODc5OSA4Ny41MzM1IDc5LjU5NzMgODcuNDI4NyA3OS4zODc3Qzg3LjMyODUgNzkuMTc4MSA4Ny4xODAzIDc5LjAyNzcgODYuOTg0NCA3OC45MzY1Qzg2Ljc4ODQgNzguODQwOCA4Ni41NDkyIDc4Ljc5MyA4Ni4yNjY2IDc4Ljc5M0M4NS45NzA0IDc4Ljc5MyA4NS43MDgzIDc4Ljg1MjIgODUuNDgwNSA3OC45NzA3Qzg1LjI1NzIgNzkuMDg5MiA4NS4wNzAzIDc5LjI1MSA4NC45MTk5IDc5LjQ1NjFDODQuNzY5NSA3OS42NjExIDg0LjY1NTYgNzkuODk4MSA4NC41NzgxIDgwLjE2N0M4NC41MDUyIDgwLjQzNTkgODQuNDY4OCA4MC43MjMgODQuNDY4OCA4MS4wMjgzWk05NC40MTg5IDg1LjEzNjdDOTMuODcyMSA4NS4xMzY3IDkzLjM3NzYgODUuMDQ3OSA5Mi45MzU1IDg0Ljg3MDFDOTIuNDk4IDg0LjY4NzggOTIuMTI0MyA4NC40MzQ5IDkxLjgxNDUgODQuMTExM0M5MS41MDkxIDgzLjc4NzggOTEuMjc0NCA4My40MDcyIDkxLjExMDQgODIuOTY5N0M5MC45NDYzIDgyLjUzMjIgOTAuODY0MyA4Mi4wNjA1IDkwLjg2NDMgODEuNTU0N1Y4MS4yODEyQzkwLjg2NDMgODAuNzAyNSA5MC45NDg2IDgwLjE3ODQgOTEuMTE3MiA3OS43MDlDOTEuMjg1OCA3OS4yMzk2IDkxLjUyMDUgNzguODM4NSA5MS44MjEzIDc4LjUwNTlDOTIuMTIyMSA3OC4xNjg2IDkyLjQ3NzUgNzcuOTExMSA5Mi44ODc3IDc3LjczMzRDOTMuMjk3OSA3Ny41NTU3IDkzLjc0MjIgNzcuNDY2OCA5NC4yMjA3IDc3LjQ2NjhDOTQuNzQ5MyA3Ny40NjY4IDk1LjIxMTkgNzcuNTU1NyA5NS42MDg0IDc3LjczMzRDOTYuMDA0OSA3Ny45MTExIDk2LjMzMyA3OC4xNjE4IDk2LjU5MjggNzguNDg1NEM5Ni44NTcxIDc4LjgwNDQgOTcuMDUzMSA3OS4xODQ5IDk3LjE4MDcgNzkuNjI3Qzk3LjMxMjggODAuMDY5IDk3LjM3ODkgODAuNTU2NiA5Ny4zNzg5IDgxLjA4OThWODEuNzkzOUg5MS42NjQxVjgwLjYxMTNIOTUuNzUyVjgwLjQ4MTRDOTUuNzQyOCA4MC4xODUyIDk1LjY4MzYgNzkuOTA3MiA5NS41NzQyIDc5LjY0NzVDOTUuNDY5NCA3OS4zODc3IDk1LjMwNzYgNzkuMTc4MSA5NS4wODg5IDc5LjAxODZDOTQuODcwMSA3OC44NTkgOTQuNTc4NSA3OC43NzkzIDk0LjIxMzkgNzguNzc5M0M5My45NDA0IDc4Ljc3OTMgOTMuNjk2NiA3OC44Mzg1IDkzLjQ4MjQgNzguOTU3QzkzLjI3MjggNzkuMDcxIDkzLjA5NzMgNzkuMjM3MyA5Mi45NTYxIDc5LjQ1NjFDOTIuODE0OCA3OS42NzQ4IDkyLjcwNTQgNzkuOTM5MSA5Mi42Mjc5IDgwLjI0OUM5Mi41NTUgODAuNTU0NCA5Mi41MTg2IDgwLjg5ODQgOTIuNTE4NiA4MS4yODEyVjgxLjU1NDdDOTIuNTE4NiA4MS44NzgzIDkyLjU2MTggODIuMTc5IDkyLjY0ODQgODIuNDU3QzkyLjczOTYgODIuNzMwNSA5Mi44NzE3IDgyLjk2OTcgOTMuMDQ0OSA4My4xNzQ4QzkzLjIxODEgODMuMzc5OSA5My40Mjc3IDgzLjU0MTcgOTMuNjczOCA4My42NjAyQzkzLjkxOTkgODMuNzc0MSA5NC4yMDAyIDgzLjgzMTEgOTQuNTE0NiA4My44MzExQzk0LjkxMTEgODMuODMxMSA5NS4yNjQzIDgzLjc1MTMgOTUuNTc0MiA4My41OTE4Qzk1Ljg4NDEgODMuNDMyMyA5Ni4xNTMgODMuMjA2NyA5Ni4zODA5IDgyLjkxNUw5Ny4yNDkgODMuNzU1OUM5Ny4wODk1IDgzLjk4ODMgOTYuODgyMiA4NC4yMTE2IDk2LjYyNyA4NC40MjU4Qzk2LjM3MTcgODQuNjM1NCA5Ni4wNTk2IDg0LjgwNjMgOTUuNjkwNCA4NC45Mzg1Qzk1LjMyNTggODUuMDcwNiA5NC45MDIgODUuMTM2NyA5NC40MTg5IDg1LjEzNjdaTTEwMC41NjggNzkuMDExN1Y4NUg5OC45MjA5Vjc3LjYwMzVIMTAwLjQ5M0wxMDAuNTY4IDc5LjAxMTdaTTEwMi44MzEgNzcuNTU1N0wxMDIuODE3IDc5LjA4NjlDMTAyLjcxNyA3OS4wNjg3IDEwMi42MDggNzkuMDU1IDEwMi40ODkgNzkuMDQ1OUMxMDIuMzc1IDc5LjAzNjggMTAyLjI2MSA3OS4wMzIyIDEwMi4xNDcgNzkuMDMyMkMxMDEuODY1IDc5LjAzMjIgMTAxLjYxNyA3OS4wNzMyIDEwMS40MDIgNzkuMTU1M0MxMDEuMTg4IDc5LjIzMjcgMTAxLjAwOCA3OS4zNDY3IDEwMC44NjIgNzkuNDk3MUMxMDAuNzIxIDc5LjY0MjkgMTAwLjYxMiA3OS44MjA2IDEwMC41MzQgODAuMDMwM0MxMDAuNDU3IDgwLjIzOTkgMTAwLjQxMSA4MC40NzQ2IDEwMC4zOTcgODAuNzM0NEwxMDAuMDIxIDgwLjc2MTdDMTAwLjAyMSA4MC4yOTY5IDEwMC4wNjcgNzkuODY2MiAxMDAuMTU4IDc5LjQ2OTdDMTAwLjI0OSA3OS4wNzMyIDEwMC4zODYgNzguNzI0NiAxMDAuNTY4IDc4LjQyMzhDMTAwLjc1NSA3OC4xMjMgMTAwLjk4OCA3Ny44ODgzIDEwMS4yNjYgNzcuNzE5N0MxMDEuNTQ4IDc3LjU1MTEgMTAxLjg3NCA3Ny40NjY4IDEwMi4yNDMgNzcuNDY2OEMxMDIuMzQzIDc3LjQ2NjggMTAyLjQ1MSA3Ny40NzU5IDEwMi41NjQgNzcuNDk0MUMxMDIuNjgzIDc3LjUxMjQgMTAyLjc3MiA3Ny41MzI5IDEwMi44MzEgNzcuNTU1N1pNMTA1Ljc2MSA3OS4xMDc0Vjg1SDEwNC4xMTNWNzcuNjAzNUgxMDUuNjY1TDEwNS43NjEgNzkuMTA3NFpNMTA1LjQ5NCA4MS4wMjgzTDEwNC45MzQgODEuMDIxNUMxMDQuOTM0IDgwLjUxMTEgMTA0Ljk5NyA4MC4wMzk0IDEwNS4xMjUgNzkuNjA2NEMxMDUuMjUzIDc5LjE3MzUgMTA1LjQzOSA3OC43OTc1IDEwNS42ODYgNzguNDc4NUMxMDUuOTMyIDc4LjE1NDkgMTA2LjIzNyA3Ny45MDY2IDEwNi42MDIgNzcuNzMzNEMxMDYuOTcxIDc3LjU1NTcgMTA3LjM5NyA3Ny40NjY4IDEwNy44OCA3Ny40NjY4QzEwOC4yMTcgNzcuNDY2OCAxMDguNTI1IDc3LjUxNjkgMTA4LjgwMyA3Ny42MTcyQzEwOS4wODUgNzcuNzEyOSAxMDkuMzI5IDc3Ljg2NTYgMTA5LjUzNCA3OC4wNzUyQzEwOS43NDQgNzguMjg0OCAxMDkuOTAzIDc4LjU1MzcgMTEwLjAxMyA3OC44ODE4QzExMC4xMjcgNzkuMjEgMTEwLjE4NCA3OS42MDY0IDExMC4xODQgODAuMDcxM1Y4NUgxMDguNTM2VjgwLjIxNDhDMTA4LjUzNiA3OS44NTQ4IDEwOC40ODEgNzkuNTcyMyAxMDguMzcyIDc5LjM2NzJDMTA4LjI2NyA3OS4xNjIxIDEwOC4xMTUgNzkuMDE2MyAxMDcuOTE0IDc4LjkyOTdDMTA3LjcxOCA3OC44Mzg1IDEwNy40ODMgNzguNzkzIDEwNy4yMSA3OC43OTNDMTA2LjkgNzguNzkzIDEwNi42MzYgNzguODUyMiAxMDYuNDE3IDc4Ljk3MDdDMTA2LjIwMyA3OS4wODkyIDEwNi4wMjcgNzkuMjUxIDEwNS44OTEgNzkuNDU2MUMxMDUuNzU0IDc5LjY2MTEgMTA1LjY1NCA3OS44OTgxIDEwNS41OSA4MC4xNjdDMTA1LjUyNiA4MC40MzU5IDEwNS40OTQgODAuNzIzIDEwNS40OTQgODEuMDI4M1pNMTEwLjA4MSA4MC41OTA4TDEwOS4zMDkgODAuNzYxN0MxMDkuMzA5IDgwLjMxNTEgMTA5LjM3IDc5Ljg5MzYgMTA5LjQ5MyA3OS40OTcxQzEwOS42MjEgNzkuMDk2IDEwOS44MDUgNzguNzQ1MSAxMTAuMDQ3IDc4LjQ0NDNDMTEwLjI5MyA3OC4xMzkgMTEwLjU5NiA3Ny44OTk3IDExMC45NTYgNzcuNzI2NkMxMTEuMzE2IDc3LjU1MzQgMTExLjcyOSA3Ny40NjY4IDExMi4xOTMgNzcuNDY2OEMxMTIuNTcyIDc3LjQ2NjggMTEyLjkwOSA3Ny41MTkyIDExMy4yMDUgNzcuNjI0QzExMy41MDYgNzcuNzI0MyAxMTMuNzYxIDc3Ljg4MzggMTEzLjk3MSA3OC4xMDI1QzExNC4xOCA3OC4zMjEzIDExNC4zNCA3OC42MDYxIDExNC40NDkgNzguOTU3QzExNC41NTkgNzkuMzAzNCAxMTQuNjEzIDc5LjcyMjcgMTE0LjYxMyA4MC4yMTQ4Vjg1SDExMi45NTlWODAuMjA4QzExMi45NTkgNzkuODM0MyAxMTIuOTA0IDc5LjU0NDkgMTEyLjc5NSA3OS4zMzk4QzExMi42OSA3OS4xMzQ4IDExMi41NCA3OC45OTM1IDExMi4zNDQgNzguOTE2QzExMi4xNDggNzguODM0IDExMS45MTMgNzguNzkzIDExMS42NCA3OC43OTNDMTExLjM4NCA3OC43OTMgMTExLjE1OSA3OC44NDA4IDExMC45NjMgNzguOTM2NUMxMTAuNzcxIDc5LjAyNzcgMTEwLjYxIDc5LjE1NzYgMTEwLjQ3OCA3OS4zMjYyQzExMC4zNDUgNzkuNDkwMiAxMTAuMjQ1IDc5LjY3OTQgMTEwLjE3NyA3OS44OTM2QzExMC4xMTMgODAuMTA3NyAxMTAuMDgxIDgwLjM0MDIgMTEwLjA4MSA4MC41OTA4Wk0xMTYuMjQ0IDgxLjM4MzhWODEuMjI2NkMxMTYuMjQ0IDgwLjY5MzQgMTE2LjMyMiA4MC4xOTg5IDExNi40NzcgNzkuNzQzMkMxMTYuNjMyIDc5LjI4MjkgMTE2Ljg1NSA3OC44ODQxIDExNy4xNDYgNzguNTQ2OUMxMTcuNDQzIDc4LjIwNTEgMTE3LjgwMyA3Ny45NDA4IDExOC4yMjcgNzcuNzUzOUMxMTguNjU1IDc3LjU2MjUgMTE5LjEzOCA3Ny40NjY4IDExOS42NzYgNzcuNDY2OEMxMjAuMjE4IDc3LjQ2NjggMTIwLjcwMSA3Ny41NjI1IDEyMS4xMjUgNzcuNzUzOUMxMjEuNTUzIDc3Ljk0MDggMTIxLjkxNiA3OC4yMDUxIDEyMi4yMTIgNzguNTQ2OUMxMjIuNTA4IDc4Ljg4NDEgMTIyLjczNCA3OS4yODI5IDEyMi44ODkgNzkuNzQzMkMxMjMuMDQ0IDgwLjE5ODkgMTIzLjEyMSA4MC42OTM0IDEyMy4xMjEgODEuMjI2NlY4MS4zODM4QzEyMy4xMjEgODEuOTE3IDEyMy4wNDQgODIuNDExNSAxMjIuODg5IDgyLjg2NzJDMTIyLjczNCA4My4zMjI5IDEyMi41MDggODMuNzIxNyAxMjIuMjEyIDg0LjA2MzVDMTIxLjkxNiA4NC40MDA3IDEyMS41NTYgODQuNjY1IDEyMS4xMzIgODQuODU2NEMxMjAuNzA4IDg1LjA0MzMgMTIwLjIyNyA4NS4xMzY3IDExOS42ODkgODUuMTM2N0MxMTkuMTQ3IDg1LjEzNjcgMTE4LjY2MiA4NS4wNDMzIDExOC4yMzMgODQuODU2NEMxMTcuODEgODQuNjY1IDExNy40NSA4NC40MDA3IDExNy4xNTMgODQuMDYzNUMxMTYuODU3IDgzLjcyMTcgMTE2LjYzMiA4My4zMjI5IDExNi40NzcgODIuODY3MkMxMTYuMzIyIDgyLjQxMTUgMTE2LjI0NCA4MS45MTcgMTE2LjI0NCA4MS4zODM4Wk0xMTcuODkyIDgxLjIyNjZWODEuMzgzOEMxMTcuODkyIDgxLjcxNjUgMTE3LjkyNiA4Mi4wMzA5IDExNy45OTQgODIuMzI3MUMxMTguMDYyIDgyLjYyMzQgMTE4LjE3IDgyLjg4MzEgMTE4LjMxNSA4My4xMDY0QzExOC40NjEgODMuMzI5OCAxMTguNjQ4IDgzLjUwNTIgMTE4Ljg3NiA4My42MzI4QzExOS4xMDQgODMuNzYwNCAxMTkuMzc1IDgzLjgyNDIgMTE5LjY4OSA4My44MjQyQzExOS45OTUgODMuODI0MiAxMjAuMjU5IDgzLjc2MDQgMTIwLjQ4MiA4My42MzI4QzEyMC43MSA4My41MDUyIDEyMC44OTcgODMuMzI5OCAxMjEuMDQzIDgzLjEwNjRDMTIxLjE4OSA4Mi44ODMxIDEyMS4yOTYgODIuNjIzNCAxMjEuMzY0IDgyLjMyNzFDMTIxLjQzNyA4Mi4wMzA5IDEyMS40NzQgODEuNzE2NSAxMjEuNDc0IDgxLjM4MzhWODEuMjI2NkMxMjEuNDc0IDgwLjg5ODQgMTIxLjQzNyA4MC41ODg1IDEyMS4zNjQgODAuMjk2OUMxMjEuMjk2IDgwLjAwMDcgMTIxLjE4NyA3OS43Mzg2IDEyMS4wMzYgNzkuNTEwN0MxMjAuODkgNzkuMjgyOSAxMjAuNzAzIDc5LjEwNTEgMTIwLjQ3NiA3OC45Nzc1QzEyMC4yNTIgNzguODQ1NCAxMTkuOTg2IDc4Ljc3OTMgMTE5LjY3NiA3OC43NzkzQzExOS4zNjYgNzguNzc5MyAxMTkuMDk3IDc4Ljg0NTQgMTE4Ljg2OSA3OC45Nzc1QzExOC42NDYgNzkuMTA1MSAxMTguNDYxIDc5LjI4MjkgMTE4LjMxNSA3OS41MTA3QzExOC4xNyA3OS43Mzg2IDExOC4wNjIgODAuMDAwNyAxMTcuOTk0IDgwLjI5NjlDMTE3LjkyNiA4MC41ODg1IDExNy44OTIgODAuODk4NCAxMTcuODkyIDgxLjIyNjZaTTEyOC45MDEgODIuOTk3MUMxMjguOTAxIDgyLjgzMyAxMjguODYgODIuNjg0OSAxMjguNzc4IDgyLjU1MjdDMTI4LjY5NiA4Mi40MTYgMTI4LjUzOSA4Mi4yOTMgMTI4LjMwNyA4Mi4xODM2QzEyOC4wNzkgODIuMDc0MiAxMjcuNzQyIDgxLjk3NCAxMjcuMjk1IDgxLjg4MjhDMTI2LjkwMyA4MS43OTYyIDEyNi41NDMgODEuNjkzNyAxMjYuMjE1IDgxLjU3NTJDMTI1Ljg5MSA4MS40NTIxIDEyNS42MTMgODEuMzA0IDEyNS4zODEgODEuMTMwOUMxMjUuMTQ4IDgwLjk1NzcgMTI0Ljk2OCA4MC43NTI2IDEyNC44NDEgODAuNTE1NkMxMjQuNzEzIDgwLjI3ODYgMTI0LjY0OSA4MC4wMDUyIDEyNC42NDkgNzkuNjk1M0MxMjQuNjQ5IDc5LjM5NDUgMTI0LjcxNSA3OS4xMDk3IDEyNC44NDggNzguODQwOEMxMjQuOTggNzguNTcxOSAxMjUuMTY5IDc4LjMzNSAxMjUuNDE1IDc4LjEyOTlDMTI1LjY2MSA3Ny45MjQ4IDEyNS45NiA3Ny43NjMgMTI2LjMxMSA3Ny42NDQ1QzEyNi42NjYgNzcuNTI2IDEyNy4wNjIgNzcuNDY2OCAxMjcuNSA3Ny40NjY4QzEyOC4xMiA3Ny40NjY4IDEyOC42NTEgNzcuNTcxNiAxMjkuMDkzIDc3Ljc4MTJDMTI5LjUzOSA3Ny45ODYzIDEyOS44ODEgNzguMjY2NiAxMzAuMTE4IDc4LjYyMjFDMTMwLjM1NSA3OC45NzMgMTMwLjQ3NCA3OS4zNjk1IDEzMC40NzQgNzkuODExNUgxMjguODI2QzEyOC44MjYgNzkuNjE1NiAxMjguNzc2IDc5LjQzMzMgMTI4LjY3NiA3OS4yNjQ2QzEyOC41OCA3OS4wOTE1IDEyOC40MzQgNzguOTUyNSAxMjguMjM4IDc4Ljg0NzdDMTI4LjA0MiA3OC43MzgzIDEyNy43OTYgNzguNjgzNiAxMjcuNSA3OC42ODM2QzEyNy4yMTcgNzguNjgzNiAxMjYuOTgzIDc4LjcyOTIgMTI2Ljc5NiA3OC44MjAzQzEyNi42MTQgNzguOTA2OSAxMjYuNDc3IDc5LjAyMDggMTI2LjM4NiA3OS4xNjIxQzEyNi4yOTkgNzkuMzAzNCAxMjYuMjU2IDc5LjQ1ODMgMTI2LjI1NiA3OS42MjdDMTI2LjI1NiA3OS43NSAxMjYuMjc5IDc5Ljg2MTcgMTI2LjMyNCA3OS45NjE5QzEyNi4zNzQgODAuMDU3NiAxMjYuNDU2IDgwLjE0NjUgMTI2LjU3IDgwLjIyODVDMTI2LjY4NCA4MC4zMDYgMTI2LjgzOSA4MC4zNzg5IDEyNy4wMzUgODAuNDQ3M0MxMjcuMjM2IDgwLjUxNTYgMTI3LjQ4NiA4MC41ODE3IDEyNy43ODcgODAuNjQ1NUMxMjguMzUyIDgwLjc2NCAxMjguODM4IDgwLjkxNjcgMTI5LjI0MyA4MS4xMDM1QzEyOS42NTMgODEuMjg1OCAxMjkuOTY4IDgxLjUyMjggMTMwLjE4NyA4MS44MTQ1QzEzMC40MDUgODIuMTAxNiAxMzAuNTE1IDgyLjQ2NjEgMTMwLjUxNSA4Mi45MDgyQzEzMC41MTUgODMuMjM2MyAxMzAuNDQ0IDgzLjUzNzEgMTMwLjMwMyA4My44MTA1QzEzMC4xNjYgODQuMDc5NCAxMjkuOTY1IDg0LjMxNDEgMTI5LjcwMSA4NC41MTQ2QzEyOS40MzcgODQuNzEwNiAxMjkuMTIgODQuODYzMyAxMjguNzUxIDg0Ljk3MjdDMTI4LjM4NiA4NS4wODIgMTI3Ljk3NiA4NS4xMzY3IDEyNy41MjEgODUuMTM2N0MxMjYuODUxIDg1LjEzNjcgMTI2LjI4MyA4NS4wMTgyIDEyNS44MTggODQuNzgxMkMxMjUuMzU0IDg0LjUzOTcgMTI1IDg0LjIzMjEgMTI0Ljc1OSA4My44NTg0QzEyNC41MjIgODMuNDgwMSAxMjQuNDAzIDgzLjA4ODIgMTI0LjQwMyA4Mi42ODI2SDEyNS45OTZDMTI2LjAxNCA4Mi45ODggMTI2LjA5OSA4My4yMzE4IDEyNi4yNDkgODMuNDE0MUMxMjYuNDA0IDgzLjU5MTggMTI2LjU5NSA4My43MjE3IDEyNi44MjMgODMuODAzN0MxMjcuMDU2IDgzLjg4MTIgMTI3LjI5NSA4My45MTk5IDEyNy41NDEgODMuOTE5OUMxMjcuODM3IDgzLjkxOTkgMTI4LjA4NiA4My44ODEyIDEyOC4yODYgODMuODAzN0MxMjguNDg3IDgzLjcyMTcgMTI4LjYzOSA4My42MTIzIDEyOC43NDQgODMuNDc1NkMxMjguODQ5IDgzLjMzNDMgMTI4LjkwMSA4My4xNzQ4IDEyOC45MDEgODIuOTk3MVpNMTM1LjU5OCA3Ny42MDM1Vjc4LjgwNjZIMTMxLjQyOFY3Ny42MDM1SDEzNS41OThaTTEzMi42MzEgNzUuNzkySDEzNC4yNzhWODIuOTU2MUMxMzQuMjc4IDgzLjE4MzkgMTM0LjMxIDgzLjM1OTQgMTM0LjM3NCA4My40ODI0QzEzNC40NDIgODMuNjAwOSAxMzQuNTM2IDgzLjY4MDcgMTM0LjY1NCA4My43MjE3QzEzNC43NzMgODMuNzYyNyAxMzQuOTEyIDgzLjc4MzIgMTM1LjA3MSA4My43ODMyQzEzNS4xODUgODMuNzgzMiAxMzUuMjk1IDgzLjc3NjQgMTM1LjM5OSA4My43NjI3QzEzNS41MDQgODMuNzQ5IDEzNS41ODkgODMuNzM1NCAxMzUuNjUyIDgzLjcyMTdMMTM1LjY1OSA4NC45Nzk1QzEzNS41MjIgODUuMDIwNSAxMzUuMzYzIDg1LjA1NyAxMzUuMTgxIDg1LjA4ODlDMTM1LjAwMyA4NS4xMjA4IDEzNC43OTggODUuMTM2NyAxMzQuNTY1IDg1LjEzNjdDMTM0LjE4NyA4NS4xMzY3IDEzMy44NTIgODUuMDcwNiAxMzMuNTYxIDg0LjkzODVDMTMzLjI2OSA4NC44MDE4IDEzMy4wNDEgODQuNTgwNyAxMzIuODc3IDg0LjI3NTRDMTMyLjcxMyA4My45NzAxIDEzMi42MzEgODMuNTY0NSAxMzIuNjMxIDgzLjA1ODZWNzUuNzkyWk0xNDEuMjg5IDgzLjUxNjZWNzkuOTg5M0MxNDEuMjg5IDc5LjcyNDkgMTQxLjI0MSA3OS40OTcxIDE0MS4xNDYgNzkuMzA1N0MxNDEuMDUgNzkuMTE0MyAxNDAuOTA0IDc4Ljk2NjEgMTQwLjcwOCA3OC44NjEzQzE0MC41MTcgNzguNzU2NSAxNDAuMjc1IDc4LjcwNDEgMTM5Ljk4MyA3OC43MDQxQzEzOS43MTUgNzguNzA0MSAxMzkuNDgyIDc4Ljc0OTcgMTM5LjI4NiA3OC44NDA4QzEzOS4wOSA3OC45MzIgMTM4LjkzOCA3OS4wNTUgMTM4LjgyOCA3OS4yMUMxMzguNzE5IDc5LjM2NDkgMTM4LjY2NCA3OS41NDA0IDEzOC42NjQgNzkuNzM2M0gxMzcuMDIzQzEzNy4wMjMgNzkuNDQ0NyAxMzcuMDk0IDc5LjE2MjEgMTM3LjIzNSA3OC44ODg3QzEzNy4zNzcgNzguNjE1MiAxMzcuNTgyIDc4LjM3MTQgMTM3Ljg1MSA3OC4xNTcyQzEzOC4xMTkgNzcuOTQzIDEzOC40NDEgNzcuNzc0NCAxMzguODE0IDc3LjY1MTRDMTM5LjE4OCA3Ny41MjgzIDEzOS42MDcgNzcuNDY2OCAxNDAuMDcyIDc3LjQ2NjhDMTQwLjYyOCA3Ny40NjY4IDE0MS4xMiA3Ny41NjAyIDE0MS41NDkgNzcuNzQ3MUMxNDEuOTgyIDc3LjkzMzkgMTQyLjMyMSA3OC4yMTY1IDE0Mi41NjcgNzguNTk0N0MxNDIuODE4IDc4Ljk2ODQgMTQyLjk0MyA3OS40Mzc4IDE0Mi45NDMgODAuMDAyOVY4My4yOTFDMTQyLjk0MyA4My42MjgzIDE0Mi45NjYgODMuOTMxMyAxNDMuMDEyIDg0LjIwMDJDMTQzLjA2MiA4NC40NjQ1IDE0My4xMzIgODQuNjk0NyAxNDMuMjI0IDg0Ljg5MDZWODVIMTQxLjUzNUMxNDEuNDU4IDg0LjgyMjMgMTQxLjM5NiA4NC41OTY3IDE0MS4zNTEgODQuMzIzMkMxNDEuMzEgODQuMDQ1MiAxNDEuMjg5IDgzLjc3NjQgMTQxLjI4OSA4My41MTY2Wk0xNDEuNTI4IDgwLjUwMkwxNDEuNTQyIDgxLjUyMDVIMTQwLjM1OUMxNDAuMDU0IDgxLjUyMDUgMTM5Ljc4NSA4MS41NTAxIDEzOS41NTMgODEuNjA5NEMxMzkuMzIgODEuNjY0MSAxMzkuMTI3IDgxLjc0NjEgMTM4Ljk3MiA4MS44NTU1QzEzOC44MTcgODEuOTY0OCAxMzguNzAxIDgyLjA5NyAxMzguNjIzIDgyLjI1MkMxMzguNTQ2IDgyLjQwNjkgMTM4LjUwNyA4Mi41ODI0IDEzOC41MDcgODIuNzc4M0MxMzguNTA3IDgyLjk3NDMgMTM4LjU1MiA4My4xNTQzIDEzOC42NDQgODMuMzE4NEMxMzguNzM1IDgzLjQ3NzkgMTM4Ljg2NyA4My42MDMyIDEzOS4wNCA4My42OTQzQzEzOS4yMTggODMuNzg1NSAxMzkuNDMyIDgzLjgzMTEgMTM5LjY4MyA4My44MzExQzE0MC4wMiA4My44MzExIDE0MC4zMTQgODMuNzYyNyAxNDAuNTY0IDgzLjYyNkMxNDAuODIgODMuNDg0NyAxNDEuMDIgODMuMzEzOCAxNDEuMTY2IDgzLjExMzNDMTQxLjMxMiA4Mi45MDgyIDE0MS4zODkgODIuNzE0NSAxNDEuMzk4IDgyLjUzMjJMMTQxLjkzMiA4My4yNjM3QzE0MS44NzcgODMuNDUwNSAxNDEuNzg0IDgzLjY1MSAxNDEuNjUxIDgzLjg2NTJDMTQxLjUxOSA4NC4wNzk0IDE0MS4zNDYgODQuMjg0NSAxNDEuMTMyIDg0LjQ4MDVDMTQwLjkyMiA4NC42NzE5IDE0MC42NjkgODQuODI5MSAxNDAuMzczIDg0Ljk1MjFDMTQwLjA4MSA4NS4wNzUyIDEzOS43NDQgODUuMTM2NyAxMzkuMzYxIDg1LjEzNjdDMTM4Ljg3OCA4NS4xMzY3IDEzOC40NDggODUuMDQxIDEzOC4wNjkgODQuODQ5NkMxMzcuNjkxIDg0LjY1MzYgMTM3LjM5NSA4NC4zOTE2IDEzNy4xODEgODQuMDYzNUMxMzYuOTY2IDgzLjczMDggMTM2Ljg1OSA4My4zNTQ4IDEzNi44NTkgODIuOTM1NUMxMzYuODU5IDgyLjU0MzYgMTM2LjkzMiA4Mi4xOTczIDEzNy4wNzggODEuODk2NUMxMzcuMjI5IDgxLjU5MTEgMTM3LjQ0NyA4MS4zMzU5IDEzNy43MzQgODEuMTMwOUMxMzguMDI2IDgwLjkyNTggMTM4LjM4MiA4MC43NzA4IDEzOC44MDEgODAuNjY2QzEzOS4yMiA4MC41NTY2IDEzOS42OTkgODAuNTAyIDE0MC4yMzYgODAuNTAySDE0MS41MjhaTTE0OC4zMDcgNzcuNjAzNVY3OC44MDY2SDE0NC4xMzdWNzcuNjAzNUgxNDguMzA3Wk0xNDUuMzQgNzUuNzkySDE0Ni45ODdWODIuOTU2MUMxNDYuOTg3IDgzLjE4MzkgMTQ3LjAxOSA4My4zNTk0IDE0Ny4wODMgODMuNDgyNEMxNDcuMTUxIDgzLjYwMDkgMTQ3LjI0NSA4My42ODA3IDE0Ny4zNjMgODMuNzIxN0MxNDcuNDgyIDgzLjc2MjcgMTQ3LjYyMSA4My43ODMyIDE0Ny43OCA4My43ODMyQzE0Ny44OTQgODMuNzgzMiAxNDguMDA0IDgzLjc3NjQgMTQ4LjEwOCA4My43NjI3QzE0OC4yMTMgODMuNzQ5IDE0OC4yOTggODMuNzM1NCAxNDguMzYxIDgzLjcyMTdMMTQ4LjM2OCA4NC45Nzk1QzE0OC4yMzEgODUuMDIwNSAxNDguMDcyIDg1LjA1NyAxNDcuODkgODUuMDg4OUMxNDcuNzEyIDg1LjEyMDggMTQ3LjUwNyA4NS4xMzY3IDE0Ny4yNzQgODUuMTM2N0MxNDYuODk2IDg1LjEzNjcgMTQ2LjU2MSA4NS4wNzA2IDE0Ni4yNyA4NC45Mzg1QzE0NS45NzggODQuODAxOCAxNDUuNzUgODQuNTgwNyAxNDUuNTg2IDg0LjI3NTRDMTQ1LjQyMiA4My45NzAxIDE0NS4zNCA4My41NjQ1IDE0NS4zNCA4My4wNTg2Vjc1Ljc5MlpNMTU3LjYwNCA3Ni4zNzNMMTU0LjYzMSA4NUgxNTIuODMzTDE1Ni41NzkgNzUuMDQ2OUgxNTcuNzI4TDE1Ny42MDQgNzYuMzczWk0xNjAuMDkzIDg1TDE1Ny4xMTIgNzYuMzczTDE1Ni45ODIgNzUuMDQ2OUgxNTguMTM4TDE2MS44OTcgODVIMTYwLjA5M1pNMTU5Ljk0OSA4MS4zMDg2VjgyLjY2ODlIMTU0LjUzNVY4MS4zMDg2SDE1OS45NDlaTTE2Ny41IDc1LjAxMjdWODVIMTY1Ljg1M1Y3Ni45Njc4TDE2My40MTIgNzcuNzk0OVY3Ni40MzQ2TDE2Ny4zMDIgNzUuMDEyN0gxNjcuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kXzQ5NzhfNDUwMzkiIHg9IjAiIHk9IjQ4IiB3aWR0aD0iMjEyIiBoZWlnaHQ9IjY4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMyIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xMiAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ5NzhfNDUwMzkiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDk3OF80NTAzOSIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPGZpbHRlciBpZD0iZmlsdGVyMV9iXzQ5NzhfNDUwMzkiIHg9IjEyIiB5PSI1OCIgd2lkdGg9IjE4OCIgaGVpZ2h0PSI0NCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVHYXVzc2lhbkJsdXIgaW49IkJhY2tncm91bmRJbWFnZUZpeCIgc3RkRGV2aWF0aW9uPSIzIi8+CjxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80OTc4XzQ1MDM5Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80OTc4XzQ1MDM5IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=", + "description": "Displays a single label with optional icon.", + "descriptor": { + "type": "static", + "sizeX": 4.5, + "sizeY": 1, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.labelCardWidget.onInit();\n};\n\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-label-card-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-label-card-basic-config", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Label card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + }, + "tags": [ + "label" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 6a7e8738cc..1a705c3c6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -133,6 +133,9 @@ import { DigitalSimpleGaugeBasicConfigComponent } from '@home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component'; import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component'; +import { + LabelCardBasicConfigComponent +} from '@home/components/widget/config/basic/cards/label-card-basic-config.component'; @NgModule({ declarations: [ @@ -177,7 +180,8 @@ import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/con PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent, - MobileAppQrCodeBasicConfigComponent + MobileAppQrCodeBasicConfigComponent, + LabelCardBasicConfigComponent ], imports: [ CommonModule, @@ -224,7 +228,8 @@ import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/con PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent, - MobileAppQrCodeBasicConfigComponent + MobileAppQrCodeBasicConfigComponent, + LabelCardBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -265,5 +270,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + +
+
widget-config.appearance
+
+ + {{ 'widgets.label-card.auto-scale' | translate }} + +
+
+
widgets.label-card.label
+
+ + + + + + + +
+
+
+ + {{ 'widgets.label-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts new file mode 100644 index 0000000000..1865bed12c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts @@ -0,0 +1,130 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { + labelCardWidgetDefaultSettings, + LabelCardWidgetSettings +} from '@home/components/widget/lib/cards/label-card-widget.models'; + +@Component({ + selector: 'tb-label-card-basic-config', + templateUrl: './label-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class LabelCardBasicConfigComponent extends BasicWidgetConfigComponent { + + labelCardWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.labelCardWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: LabelCardWidgetSettings = {...labelCardWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.labelCardWidgetConfigForm = this.fb.group({ + autoScale: [settings.autoScale, []], + + label: [settings.label, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + padding: [settings.padding, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.autoScale = config.autoScale; + + this.widgetConfig.config.settings.label = config.label; + this.widgetConfig.config.settings.labelFont = config.labelFont; + this.widgetConfig.config.settings.labelColor = config.labelColor; + + this.widgetConfig.config.settings.showIcon = config.showIcon; + this.widgetConfig.config.settings.iconSize = config.iconSize; + this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit; + this.widgetConfig.config.settings.icon = config.icon; + this.widgetConfig.config.settings.iconColor = config.iconColor; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.settings.padding = config.padding; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showIcon: boolean = this.labelCardWidgetConfigForm.get('showIcon').value; + if (showIcon) { + this.labelCardWidgetConfigForm.get('iconSize').enable(); + this.labelCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.labelCardWidgetConfigForm.get('icon').enable(); + this.labelCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.labelCardWidgetConfigForm.get('iconSize').disable(); + this.labelCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.labelCardWidgetConfigForm.get('icon').disable(); + this.labelCardWidgetConfigForm.get('iconColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html new file mode 100644 index 0000000000..4dfc45a4f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html @@ -0,0 +1,30 @@ + +
+
+
+ +
+
+
+ {{ icon }} +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss new file mode 100644 index 0000000000..317b3617b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss @@ -0,0 +1,70 @@ +/** + * 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. + */ + +.tb-label-card-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 20px 24px; + &.tb-label-card-pointer { + cursor: pointer; + } + > div:not(.tb-label-card-overlay), > tb-icon { + z-index: 1; + } + .tb-label-card-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + > div.tb-label-card-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } + div.tb-widget-title { + padding: 0; + } + .tb-label-card-content { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + .tb-label-card-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8px; + } + .tb-label-card-label { + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts new file mode 100644 index 0000000000..5bd6411b86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts @@ -0,0 +1,170 @@ +/// +/// 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. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + labelCardWidgetDefaultSettings, + LabelCardWidgetSettings +} from '@home/components/widget/lib/cards/label-card-widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { + backgroundStyle, + ComponentStyle, + iconStyle, + overlayStyle, + resolveCssSize, + textStyle +} from '@shared/models/widget-settings.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Component({ + selector: 'tb-label-card-widget', + templateUrl: './label-card-widget.component.html', + styleUrls: ['./label-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class LabelCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('labelCardPanel', {static: false}) + labelCardPanel: ElementRef; + + @ViewChild('labelCardContent', {static: false}) + labelCardContent: ElementRef; + + @ViewChild('labelCardRow', {static: false}) + labelCardRow: ElementRef; + + settings: LabelCardWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + label: string; + labelStyle: ComponentStyle = {}; + + showIcon = true; + icon = ''; + iconStyle: ComponentStyle = {}; + + hasCardClickAction = false; + + private panelResize$: ResizeObserver; + + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.labelCardWidget = this; + this.settings = {...labelCardWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + + this.showIcon = this.settings.showIcon; + this.icon = this.settings.icon; + this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit); + this.iconStyle.color = this.settings.iconColor; + + this.label = this.settings.label; + this.labelStyle = textStyle(this.settings.labelFont); + this.labelStyle.color = this.settings.labelColor; + + this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; + } + + public ngAfterViewInit() { + if (this.settings.autoScale) { + this.renderer.setStyle(this.labelCardContent.nativeElement, 'overflow', 'visible'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'position', 'absolute'); + this.panelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.panelResize$.observe(this.labelCardPanel.nativeElement); + this.onResize(); + } + } + + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public cardClick($event: Event) { + this.ctx.actionsApi.cardClick($event); + } + + private onResize() { + const paddingLeft = getComputedStyle(this.labelCardPanel.nativeElement).paddingLeft; + const paddingRight = getComputedStyle(this.labelCardPanel.nativeElement).paddingRight; + const paddingTop = getComputedStyle(this.labelCardPanel.nativeElement).paddingTop; + const paddingBottom = getComputedStyle(this.labelCardPanel.nativeElement).paddingBottom; + const pLeft = resolveCssSize(paddingLeft)[0]; + const pRight = resolveCssSize(paddingRight)[0]; + const pTop = resolveCssSize(paddingTop)[0]; + const pBottom = resolveCssSize(paddingBottom)[0]; + const panelWidth = this.labelCardPanel.nativeElement.getBoundingClientRect().width - (pLeft + pRight); + const panelHeight = this.labelCardPanel.nativeElement.getBoundingClientRect().height - (pTop + pBottom); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', 'auto'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `none`); + const contentWidth = this.labelCardRow.nativeElement.getBoundingClientRect().width; + const contentHeight = this.labelCardRow.nativeElement.getBoundingClientRect().height; + const panelAspect = panelWidth / panelHeight; + const contentAspect = contentWidth / contentHeight; + let scale: number; + if (contentAspect > panelAspect) { + scale = panelWidth / contentWidth; + } else { + scale = panelHeight / contentHeight; + } + const width = panelWidth / scale; + this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', width + 'px'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `scale(${scale})`); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts new file mode 100644 index 0000000000..de90d287ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts @@ -0,0 +1,60 @@ +/// +/// 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. +/// + +import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; + +export interface LabelCardWidgetSettings { + autoScale: boolean; + label: string; + labelFont: Font; + labelColor: string; + showIcon: boolean; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; + iconColor: string; + background: BackgroundSettings; + padding: string; +} + +export const labelCardWidgetDefaultSettings: LabelCardWidgetSettings = { + autoScale: true, + label: 'Thermostat', + labelFont: { + family: 'Roboto', + size: 20, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '24px' + }, + labelColor: 'rgba(0, 0, 0, 0.87)', + showIcon: true, + icon: 'thermostat', + iconSize: 24, + iconSizeUnit: 'px', + iconColor: '#5469FF', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '12px' +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html new file mode 100644 index 0000000000..5f1628233a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html @@ -0,0 +1,34 @@ + +
+
+
+ +
+
+
+
+ {{ icon }} +
+
+
{{ valueText }}
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss new file mode 100644 index 0000000000..67e2ce9a6d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss @@ -0,0 +1,80 @@ +/** + * 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. + */ + +.tb-label-value-card-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 20px 24px; + &.tb-label-value-card-pointer { + cursor: pointer; + } + > div:not(.tb-label-value-card-overlay), > tb-icon { + z-index: 1; + } + .tb-label-value-card-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + > div.tb-label-value-card-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } + div.tb-widget-title { + padding: 0; + } + .tb-label-value-card-content { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + .tb-label-value-card-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 16px; + } + .tb-label-value-card-icon-label { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8px; + } + .tb-label-value-card-label { + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + } + .tb-label-value-card-value { + white-space: nowrap; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts new file mode 100644 index 0000000000..c4aecd7943 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts @@ -0,0 +1,220 @@ +/// +/// 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. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { + backgroundStyle, + ColorProcessor, + ComponentStyle, + getDataKey, + getSingleTsValue, + iconStyle, + overlayStyle, + resolveCssSize, + textStyle +} from '@shared/models/widget-settings.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { + labelValueCardWidgetDefaultSettings, + LabelValueCardWidgetSettings +} from '@home/components/widget/lib/cards/label-value-card-widget.models'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-label-value-card-widget', + templateUrl: './label-value-card-widget.component.html', + styleUrls: ['./label-value-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class LabelValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('labelCardPanel', {static: false}) + labelCardPanel: ElementRef; + + @ViewChild('labelCardContent', {static: false}) + labelCardContent: ElementRef; + + @ViewChild('labelCardRow', {static: false}) + labelCardRow: ElementRef; + + settings: LabelValueCardWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + showLabel = true; + label$: Observable; + labelStyle: ComponentStyle = {}; + labelColor: ColorProcessor; + + valueText = 'N/A'; + valueStyle: ComponentStyle = {}; + valueColor: ColorProcessor; + + showIcon = true; + icon = ''; + iconStyle: ComponentStyle = {}; + iconColor: ColorProcessor; + + hasCardClickAction = false; + + private panelResize$: ResizeObserver; + + private decimals = 0; + private units = ''; + + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.labelValueCardWidget = this; + this.settings = {...labelValueCardWidgetDefaultSettings, ...this.ctx.settings}; + + this.decimals = this.ctx.decimals; + this.units = this.ctx.units; + const dataKey = getDataKey(this.ctx.datasources); + if (isDefinedAndNotNull(dataKey?.decimals)) { + this.decimals = dataKey.decimals; + } + if (dataKey?.units) { + this.units = dataKey.units; + } + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + + this.showIcon = this.settings.showIcon; + this.icon = this.settings.icon; + this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit); + this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor); + + this.showLabel = this.settings.showLabel; + const label = this.settings.label; + this.label$ = this.ctx.registerLabelPattern(label, this.label$); + this.labelStyle = textStyle(this.settings.labelFont); + this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor); + this.valueStyle = textStyle(this.settings.valueFont); + this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); + + this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; + } + + public ngAfterViewInit() { + if (this.settings.autoScale) { + this.renderer.setStyle(this.labelCardContent.nativeElement, 'overflow', 'visible'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'position', 'absolute'); + this.panelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.panelResize$.observe(this.labelCardPanel.nativeElement); + this.onResize(); + } + } + + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + const tsValue = getSingleTsValue(this.ctx.data); + let ts; + let value; + if (tsValue && isDefinedAndNotNull(tsValue[1]) && tsValue[0] !== 0) { + ts = tsValue[0]; + value = tsValue[1]; + this.valueText = formatValue(value, this.decimals, this.units, false); + } else { + this.valueText = 'N/A'; + } + this.iconColor.update(value); + this.labelColor.update(value); + this.valueColor.update(value); + this.cd.detectChanges(); + if (this.settings.autoScale) { + setTimeout(() => { + this.onResize(); + }, 0); + } + } + + public cardClick($event: Event) { + this.ctx.actionsApi.cardClick($event); + } + + private onResize() { + const paddingLeft = getComputedStyle(this.labelCardPanel.nativeElement).paddingLeft; + const paddingRight = getComputedStyle(this.labelCardPanel.nativeElement).paddingRight; + const paddingTop = getComputedStyle(this.labelCardPanel.nativeElement).paddingTop; + const paddingBottom = getComputedStyle(this.labelCardPanel.nativeElement).paddingBottom; + const pLeft = resolveCssSize(paddingLeft)[0]; + const pRight = resolveCssSize(paddingRight)[0]; + const pTop = resolveCssSize(paddingTop)[0]; + const pBottom = resolveCssSize(paddingBottom)[0]; + const panelWidth = this.labelCardPanel.nativeElement.getBoundingClientRect().width - (pLeft + pRight); + const panelHeight = this.labelCardPanel.nativeElement.getBoundingClientRect().height - (pTop + pBottom); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', 'auto'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `none`); + const contentWidth = this.labelCardRow.nativeElement.getBoundingClientRect().width; + const contentHeight = this.labelCardRow.nativeElement.getBoundingClientRect().height; + const panelAspect = panelWidth / panelHeight; + const contentAspect = contentWidth / contentHeight; + let scale: number; + if (contentAspect > panelAspect) { + scale = panelWidth / contentWidth; + } else { + scale = panelHeight / contentHeight; + } + const width = panelWidth / scale; + this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', width + 'px'); + this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `scale(${scale})`); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.models.ts new file mode 100644 index 0000000000..b24651d688 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.models.ts @@ -0,0 +1,80 @@ +/// +/// 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. +/// + +import { + BackgroundSettings, + BackgroundType, + ColorSettings, + constantColor, + cssUnit, + Font +} from '@shared/models/widget-settings.models'; + +export interface LabelValueCardWidgetSettings { + autoScale: boolean; + showLabel: boolean; + label: string; + labelFont: Font; + labelColor: ColorSettings; + showIcon: boolean; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; + iconColor: ColorSettings; + valueFont: Font; + valueColor: ColorSettings; + background: BackgroundSettings; + padding: string; +} + +export const labelValueCardWidgetDefaultSettings: LabelValueCardWidgetSettings = { + autoScale: true, + showLabel: true, + label: 'Temperature', + labelFont: { + family: 'Roboto', + size: 20, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '24px' + }, + labelColor: constantColor('rgba(0, 0, 0, 0.87)'), + showIcon: true, + icon: 'thermostat', + iconSize: 24, + iconSizeUnit: 'px', + iconColor: constantColor('#5469FF'), + valueFont: { + family: 'Roboto', + size: 20, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '24px' + }, + valueColor: constantColor('rgba(0, 0, 0, 0.87)'), + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '12px' +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.html new file mode 100644 index 0000000000..5f7e4e6359 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.html @@ -0,0 +1,73 @@ + + +
+
widgets.label-card.label-card-style
+
+ + {{ 'widgets.label-card.auto-scale' | translate }} + +
+
+
widgets.label-card.label
+
+ + + + + + + +
+
+
+ + {{ 'widgets.label-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts new file mode 100644 index 0000000000..b3ebca2ae8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts @@ -0,0 +1,84 @@ +/// +/// 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. +/// + +import { Component, Injector } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { labelCardWidgetDefaultSettings } from '@home/components/widget/lib/cards/label-card-widget.models'; + +@Component({ + selector: 'tb-label-card-widget-settings', + templateUrl: './label-card-widget-settings.component.html', + styleUrls: [] +}) +export class LabelCardWidgetSettingsComponent extends WidgetSettingsComponent { + + labelCardWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.labelCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...labelCardWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.labelCardWidgetSettingsForm = this.fb.group({ + autoScale: [settings.autoScale, []], + + label: [settings.label, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + background: [settings.background, []], + padding: [settings.padding, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showIcon']; + } + + protected updateValidators(emitEvent: boolean) { + const showIcon: boolean = this.labelCardWidgetSettingsForm.get('showIcon').value; + if (showIcon) { + this.labelCardWidgetSettingsForm.get('iconSize').enable(); + this.labelCardWidgetSettingsForm.get('iconSizeUnit').enable(); + this.labelCardWidgetSettingsForm.get('icon').enable(); + this.labelCardWidgetSettingsForm.get('iconColor').enable(); + } else { + this.labelCardWidgetSettingsForm.get('iconSize').disable(); + this.labelCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.labelCardWidgetSettingsForm.get('icon').disable(); + this.labelCardWidgetSettingsForm.get('iconColor').disable(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 461c92bfea..47db116fbd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -356,6 +356,9 @@ import { import { MobileAppQrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component'; +import { + LabelCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/label-card-widget-settings.component'; @NgModule({ declarations: [ @@ -482,7 +485,8 @@ import { PieChartWidgetSettingsComponent, BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, - RadarChartWidgetSettingsComponent + RadarChartWidgetSettingsComponent, + LabelCardWidgetSettingsComponent ], imports: [ CommonModule, @@ -614,7 +618,8 @@ import { PieChartWidgetSettingsComponent, BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, - RadarChartWidgetSettingsComponent + RadarChartWidgetSettingsComponent, + LabelCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -713,5 +718,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Mon, 24 Jun 2024 12:58:33 +0300 Subject: [PATCH 33/34] UI: Add settings for 'Label & value card' widget. --- .../widget_types/label___value_card.json | 12 +- .../basic/basic-widget-config.module.ts | 12 +- ...bel-value-card-basic-config.component.html | 114 +++++++++++ ...label-value-card-basic-config.component.ts | 191 ++++++++++++++++++ .../label-value-card-widget.component.ts | 2 - .../label-card-widget-settings.component.ts | 3 +- ...-value-card-widget-settings.component.html | 81 ++++++++ ...el-value-card-widget-settings.component.ts | 108 ++++++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 4 + 10 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_types/label___value_card.json b/application/src/main/data/json/system/widget_types/label___value_card.json index 57e26d3050..e9ec9bae73 100644 --- a/application/src/main/data/json/system/widget_types/label___value_card.json +++ b/application/src/main/data/json/system/widget_types/label___value_card.json @@ -2,7 +2,7 @@ "fqn": "label_value_card", "name": "Label & value card", "deprecated": false, - "image": null, + "image": "tb-image:bGFiZWxWYWx1ZUNhcmQuc3Zn:IkxhYmVsICYgdmFsdWUgY2FyZCIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEyIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxMiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDk4OF80NTA1NikiPgo8cmVjdCB4PSI2IiB5PSI1MiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI1NiIgcng9IjQiIGZpbGw9IndoaXRlIi8+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIxX2JfNDk4OF80NTA1NikiPgo8cmVjdCB4PSIxOCIgeT0iNjQiIHdpZHRoPSIxNzYiIGhlaWdodD0iMzIiIHJ4PSI0IiBmaWxsPSJ3aGl0ZSIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjwvZz4KPHBhdGggZD0iTTM5LjUgODFWNzNDMzkuNSA3MS4zNCAzOC4xNiA3MCAzNi41IDcwQzM0Ljg0IDcwIDMzLjUgNzEuMzQgMzMuNSA3M1Y4MUMzMi4yOSA4MS45MSAzMS41IDgzLjM3IDMxLjUgODVDMzEuNSA4Ny43NiAzMy43NCA5MCAzNi41IDkwQzM5LjI2IDkwIDQxLjUgODcuNzYgNDEuNSA4NUM0MS41IDgzLjM3IDQwLjcxIDgxLjkxIDM5LjUgODFaTTM1LjUgNzNDMzUuNSA3Mi40NSAzNS45NSA3MiAzNi41IDcyQzM3LjA1IDcyIDM3LjUgNzIuNDUgMzcuNSA3M0gzNi41Vjc0SDM3LjVWNzZIMzYuNVY3N0gzNy41Vjc5SDM1LjVWNzNaIiBmaWxsPSIjNTQ2OUZGIi8+CjxwYXRoIGQ9Ik02MS42MDY0IDc1LjA0NjlWODVINTkuOTA0M1Y3NS4wNDY5SDYxLjYwNjRaTTY0LjczMDUgNzUuMDQ2OVY3Ni40MTQxSDU2LjgwNzZWNzUuMDQ2OUg2NC43MzA1Wk02OC43MzM0IDg1LjEzNjdDNjguMTg2NSA4NS4xMzY3IDY3LjY5MjEgODUuMDQ3OSA2Ny4yNSA4NC44NzAxQzY2LjgxMjUgODQuNjg3OCA2Ni40Mzg4IDg0LjQzNDkgNjYuMTI4OSA4NC4xMTEzQzY1LjgyMzYgODMuNzg3OCA2NS41ODg5IDgzLjQwNzIgNjUuNDI0OCA4Mi45Njk3QzY1LjI2MDcgODIuNTMyMiA2NS4xNzg3IDgyLjA2MDUgNjUuMTc4NyA4MS41NTQ3VjgxLjI4MTJDNjUuMTc4NyA4MC43MDI1IDY1LjI2MyA4MC4xNzg0IDY1LjQzMTYgNzkuNzA5QzY1LjYwMDMgNzkuMjM5NiA2NS44MzUgNzguODM4NSA2Ni4xMzU3IDc4LjUwNTlDNjYuNDM2NSA3OC4xNjg2IDY2Ljc5MiA3Ny45MTExIDY3LjIwMjEgNzcuNzMzNEM2Ny42MTIzIDc3LjU1NTcgNjguMDU2NiA3Ny40NjY4IDY4LjUzNTIgNzcuNDY2OEM2OS4wNjM4IDc3LjQ2NjggNjkuNTI2NCA3Ny41NTU3IDY5LjkyMjkgNzcuNzMzNEM3MC4zMTkzIDc3LjkxMTEgNzAuNjQ3NSA3OC4xNjE4IDcwLjkwNzIgNzguNDg1NEM3MS4xNzE1IDc4LjgwNDQgNzEuMzY3NSA3OS4xODQ5IDcxLjQ5NTEgNzkuNjI3QzcxLjYyNzMgODAuMDY5IDcxLjY5MzQgODAuNTU2NiA3MS42OTM0IDgxLjA4OThWODEuNzkzOUg2NS45Nzg1VjgwLjYxMTNINzAuMDY2NFY4MC40ODE0QzcwLjA1NzMgODAuMTg1MiA2OS45OTggNzkuOTA3MiA2OS44ODg3IDc5LjY0NzVDNjkuNzgzOSA3OS4zODc3IDY5LjYyMjEgNzkuMTc4MSA2OS40MDMzIDc5LjAxODZDNjkuMTg0NiA3OC44NTkgNjguODkyOSA3OC43NzkzIDY4LjUyODMgNzguNzc5M0M2OC4yNTQ5IDc4Ljc3OTMgNjguMDExMSA3OC44Mzg1IDY3Ljc5NjkgNzguOTU3QzY3LjU4NzIgNzkuMDcxIDY3LjQxMTggNzkuMjM3MyA2Ny4yNzA1IDc5LjQ1NjFDNjcuMTI5MiA3OS42NzQ4IDY3LjAxOTkgNzkuOTM5MSA2Ni45NDI0IDgwLjI0OUM2Ni44Njk1IDgwLjU1NDQgNjYuODMzIDgwLjg5ODQgNjYuODMzIDgxLjI4MTJWODEuNTU0N0M2Ni44MzMgODEuODc4MyA2Ni44NzYzIDgyLjE3OSA2Ni45NjI5IDgyLjQ1N0M2Ny4wNTQgODIuNzMwNSA2Ny4xODYyIDgyLjk2OTcgNjcuMzU5NCA4My4xNzQ4QzY3LjUzMjYgODMuMzc5OSA2Ny43NDIyIDgzLjU0MTcgNjcuOTg4MyA4My42NjAyQzY4LjIzNDQgODMuNzc0MSA2OC41MTQ2IDgzLjgzMTEgNjguODI5MSA4My44MzExQzY5LjIyNTYgODMuODMxMSA2OS41Nzg4IDgzLjc1MTMgNjkuODg4NyA4My41OTE4QzcwLjE5ODYgODMuNDMyMyA3MC40Njc0IDgzLjIwNjcgNzAuNjk1MyA4Mi45MTVMNzEuNTYzNSA4My43NTU5QzcxLjQwNCA4My45ODgzIDcxLjE5NjYgODQuMjExNiA3MC45NDE0IDg0LjQyNThDNzAuNjg2MiA4NC42MzU0IDcwLjM3NCA4NC44MDYzIDcwLjAwNDkgODQuOTM4NUM2OS42NDAzIDg1LjA3MDYgNjkuMjE2NSA4NS4xMzY3IDY4LjczMzQgODUuMTM2N1pNNzQuODc2IDc5LjEwNzRWODVINzMuMjI4NVY3Ny42MDM1SDc0Ljc4MDNMNzQuODc2IDc5LjEwNzRaTTc0LjYwOTQgODEuMDI4M0w3NC4wNDg4IDgxLjAyMTVDNzQuMDQ4OCA4MC41MTExIDc0LjExMjYgODAuMDM5NCA3NC4yNDAyIDc5LjYwNjRDNzQuMzY3OCA3OS4xNzM1IDc0LjU1NDcgNzguNzk3NSA3NC44MDA4IDc4LjQ3ODVDNzUuMDQ2OSA3OC4xNTQ5IDc1LjM1MjIgNzcuOTA2NiA3NS43MTY4IDc3LjczMzRDNzYuMDg1OSA3Ny41NTU3IDc2LjUxMiA3Ny40NjY4IDc2Ljk5NTEgNzcuNDY2OEM3Ny4zMzI0IDc3LjQ2NjggNzcuNjQgNzcuNTE2OSA3Ny45MTggNzcuNjE3MkM3OC4yMDA1IDc3LjcxMjkgNzguNDQ0MyA3Ny44NjU2IDc4LjY0OTQgNzguMDc1MkM3OC44NTkgNzguMjg0OCA3OS4wMTg2IDc4LjU1MzcgNzkuMTI3OSA3OC44ODE4Qzc5LjI0MTkgNzkuMjEgNzkuMjk4OCA3OS42MDY0IDc5LjI5ODggODAuMDcxM1Y4NUg3Ny42NTE0VjgwLjIxNDhDNzcuNjUxNCA3OS44NTQ4IDc3LjU5NjcgNzkuNTcyMyA3Ny40ODczIDc5LjM2NzJDNzcuMzgyNSA3OS4xNjIxIDc3LjIyOTggNzkuMDE2MyA3Ny4wMjkzIDc4LjkyOTdDNzYuODMzMyA3OC44Mzg1IDc2LjU5ODYgNzguNzkzIDc2LjMyNTIgNzguNzkzQzc2LjAxNTMgNzguNzkzIDc1Ljc1MSA3OC44NTIyIDc1LjUzMjIgNzguOTcwN0M3NS4zMTggNzkuMDg5MiA3NS4xNDI2IDc5LjI1MSA3NS4wMDU5IDc5LjQ1NjFDNzQuODY5MSA3OS42NjExIDc0Ljc2ODkgNzkuODk4MSA3NC43MDUxIDgwLjE2N0M3NC42NDEzIDgwLjQzNTkgNzQuNjA5NCA4MC43MjMgNzQuNjA5NCA4MS4wMjgzWk03OS4xOTYzIDgwLjU5MDhMNzguNDIzOCA4MC43NjE3Qzc4LjQyMzggODAuMzE1MSA3OC40ODU0IDc5Ljg5MzYgNzguNjA4NCA3OS40OTcxQzc4LjczNiA3OS4wOTYgNzguOTIwNiA3OC43NDUxIDc5LjE2MjEgNzguNDQ0M0M3OS40MDgyIDc4LjEzOSA3OS43MTEzIDc3Ljg5OTcgODAuMDcxMyA3Ny43MjY2QzgwLjQzMTMgNzcuNTUzNCA4MC44NDM4IDc3LjQ2NjggODEuMzA4NiA3Ny40NjY4QzgxLjY4NjggNzcuNDY2OCA4Mi4wMjQxIDc3LjUxOTIgODIuMzIwMyA3Ny42MjRDODIuNjIxMSA3Ny43MjQzIDgyLjg3NjMgNzcuODgzOCA4My4wODU5IDc4LjEwMjVDODMuMjk1NiA3OC4zMjEzIDgzLjQ1NTEgNzguNjA2MSA4My41NjQ1IDc4Ljk1N0M4My42NzM4IDc5LjMwMzQgODMuNzI4NSA3OS43MjI3IDgzLjcyODUgODAuMjE0OFY4NUg4Mi4wNzQyVjgwLjIwOEM4Mi4wNzQyIDc5LjgzNDMgODIuMDE5NSA3OS41NDQ5IDgxLjkxMDIgNzkuMzM5OEM4MS44MDUzIDc5LjEzNDggODEuNjU0OSA3OC45OTM1IDgxLjQ1OSA3OC45MTZDODEuMjYzIDc4LjgzNCA4MS4wMjgzIDc4Ljc5MyA4MC43NTQ5IDc4Ljc5M0M4MC40OTk3IDc4Ljc5MyA4MC4yNzQxIDc4Ljg0MDggODAuMDc4MSA3OC45MzY1Qzc5Ljg4NjcgNzkuMDI3NyA3OS43MjQ5IDc5LjE1NzYgNzkuNTkyOCA3OS4zMjYyQzc5LjQ2MDYgNzkuNDkwMiA3OS4zNjA0IDc5LjY3OTQgNzkuMjkyIDc5Ljg5MzZDNzkuMjI4MiA4MC4xMDc3IDc5LjE5NjMgODAuMzQwMiA3OS4xOTYzIDgwLjU5MDhaTTg3LjMyODEgNzkuMDI1NFY4Ny44NDM4SDg1LjY4MDdWNzcuNjAzNUg4Ny4xOTgyTDg3LjMyODEgNzkuMDI1NFpNOTIuMTQ3NSA4MS4yMzM0VjgxLjM3N0M5Mi4xNDc1IDgxLjkxNDcgOTIuMDgzNyA4Mi40MTM3IDkxLjk1NjEgODIuODc0QzkxLjgzMyA4My4zMjk4IDkxLjY0ODQgODMuNzI4NSA5MS40MDIzIDg0LjA3MDNDOTEuMTYwOCA4NC40MDc2IDkwLjg2MjMgODQuNjY5NiA5MC41MDY4IDg0Ljg1NjRDOTAuMTUxNCA4NS4wNDMzIDg5Ljc0MTIgODUuMTM2NyA4OS4yNzY0IDg1LjEzNjdDODguODE2MSA4NS4xMzY3IDg4LjQxMjggODUuMDUyNCA4OC4wNjY0IDg0Ljg4MzhDODcuNzI0NiA4NC43MTA2IDg3LjQzNTIgODQuNDY2OCA4Ny4xOTgyIDg0LjE1MjNDODYuOTYxMyA4My44Mzc5IDg2Ljc2OTkgODMuNDY4OCA4Ni42MjQgODMuMDQ0OUM4Ni40ODI3IDgyLjYxNjUgODYuMzgyNSA4Mi4xNDcxIDg2LjMyMzIgODEuNjM2N1Y4MS4wODNDODYuMzgyNSA4MC41NDA3IDg2LjQ4MjcgODAuMDQ4NSA4Ni42MjQgNzkuNjA2NEM4Ni43Njk5IDc5LjE2NDQgODYuOTYxMyA3OC43ODM5IDg3LjE5ODIgNzguNDY0OEM4Ny40MzUyIDc4LjE0NTggODcuNzI0NiA3Ny44OTk3IDg4LjA2NjQgNzcuNzI2NkM4OC40MDgyIDc3LjU1MzQgODguODA3IDc3LjQ2NjggODkuMjYyNyA3Ny40NjY4Qzg5LjcyNzUgNzcuNDY2OCA5MC4xNCA3Ny41NTc5IDkwLjUgNzcuNzQwMkM5MC44NiA3Ny45MTggOTEuMTYzMSA3OC4xNzMyIDkxLjQwOTIgNzguNTA1OUM5MS42NTUzIDc4LjgzNCA5MS44Mzk4IDc5LjIzMDUgOTEuOTYyOSA3OS42OTUzQzkyLjA4NTkgODAuMTU1NiA5Mi4xNDc1IDgwLjY2ODMgOTIuMTQ3NSA4MS4yMzM0Wk05MC41IDgxLjM3N1Y4MS4yMzM0QzkwLjUgODAuODkxNiA5MC40NjgxIDgwLjU3NDkgOTAuNDA0MyA4MC4yODMyQzkwLjM0MDUgNzkuOTg3IDkwLjI0MDIgNzkuNzI3MiA5MC4xMDM1IDc5LjUwMzlDODkuOTY2OCA3OS4yODA2IDg5Ljc5MTMgNzkuMTA3NCA4OS41NzcxIDc4Ljk4NDRDODkuMzY3NSA3OC44NTY4IDg5LjExNDYgNzguNzkzIDg4LjgxODQgNzguNzkzQzg4LjUyNjcgNzguNzkzIDg4LjI3NiA3OC44NDMxIDg4LjA2NjQgNzguOTQzNEM4Ny44NTY4IDc5LjAzOTEgODcuNjgxMyA3OS4xNzM1IDg3LjU0IDc5LjM0NjdDODcuMzk4OCA3OS41MTk5IDg3LjI4OTQgNzkuNzIyNyA4Ny4yMTE5IDc5Ljk1NTFDODcuMTM0NCA4MC4xODI5IDg3LjA3OTggODAuNDMxMyA4Ny4wNDc5IDgwLjcwMDJWODIuMDI2NEM4Ny4xMDI1IDgyLjM1NDUgODcuMTk2IDgyLjY1NTMgODcuMzI4MSA4Mi45Mjg3Qzg3LjQ2MDMgODMuMjAyMSA4Ny42NDcxIDgzLjQyMDkgODcuODg4NyA4My41ODVDODguMTM0OCA4My43NDQ1IDg4LjQ0OTIgODMuODI0MiA4OC44MzIgODMuODI0MkM4OS4xMjgzIDgzLjgyNDIgODkuMzgxMiA4My43NjA0IDg5LjU5MDggODMuNjMyOEM4OS44MDA1IDgzLjUwNTIgODkuOTcxNCA4My4zMjk4IDkwLjEwMzUgODMuMTA2NEM5MC4yNDAyIDgyLjg3ODYgOTAuMzQwNSA4Mi42MTY1IDkwLjQwNDMgODIuMzIwM0M5MC40NjgxIDgyLjAyNDEgOTAuNSA4MS43MDk2IDkwLjUgODEuMzc3Wk05Ny4wNTk2IDg1LjEzNjdDOTYuNTEyNyA4NS4xMzY3IDk2LjAxODIgODUuMDQ3OSA5NS41NzYyIDg0Ljg3MDFDOTUuMTM4NyA4NC42ODc4IDk0Ljc2NSA4NC40MzQ5IDk0LjQ1NTEgODQuMTExM0M5NC4xNDk3IDgzLjc4NzggOTMuOTE1IDgzLjQwNzIgOTMuNzUxIDgyLjk2OTdDOTMuNTg2OSA4Mi41MzIyIDkzLjUwNDkgODIuMDYwNSA5My41MDQ5IDgxLjU1NDdWODEuMjgxMkM5My41MDQ5IDgwLjcwMjUgOTMuNTg5MiA4MC4xNzg0IDkzLjc1NzggNzkuNzA5QzkzLjkyNjQgNzkuMjM5NiA5NC4xNjExIDc4LjgzODUgOTQuNDYxOSA3OC41MDU5Qzk0Ljc2MjcgNzguMTY4NiA5NS4xMTgyIDc3LjkxMTEgOTUuNTI4MyA3Ny43MzM0Qzk1LjkzODUgNzcuNTU1NyA5Ni4zODI4IDc3LjQ2NjggOTYuODYxMyA3Ny40NjY4Qzk3LjM5IDc3LjQ2NjggOTcuODUyNSA3Ny41NTU3IDk4LjI0OSA3Ny43MzM0Qzk4LjY0NTUgNzcuOTExMSA5OC45NzM2IDc4LjE2MTggOTkuMjMzNCA3OC40ODU0Qzk5LjQ5NzcgNzguODA0NCA5OS42OTM3IDc5LjE4NDkgOTkuODIxMyA3OS42MjdDOTkuOTUzNSA4MC4wNjkgMTAwLjAyIDgwLjU1NjYgMTAwLjAyIDgxLjA4OThWODEuNzkzOUg5NC4zMDQ3VjgwLjYxMTNIOTguMzkyNlY4MC40ODE0Qzk4LjM4MzUgODAuMTg1MiA5OC4zMjQyIDc5LjkwNzIgOTguMjE0OCA3OS42NDc1Qzk4LjExIDc5LjM4NzcgOTcuOTQ4MiA3OS4xNzgxIDk3LjcyOTUgNzkuMDE4NkM5Ny41MTA3IDc4Ljg1OSA5Ny4yMTkxIDc4Ljc3OTMgOTYuODU0NSA3OC43NzkzQzk2LjU4MTEgNzguNzc5MyA5Ni4zMzcyIDc4LjgzODUgOTYuMTIzIDc4Ljk1N0M5NS45MTM0IDc5LjA3MSA5NS43MzggNzkuMjM3MyA5NS41OTY3IDc5LjQ1NjFDOTUuNDU1NCA3OS42NzQ4IDk1LjM0NiA3OS45MzkxIDk1LjI2ODYgODAuMjQ5Qzk1LjE5NTYgODAuNTU0NCA5NS4xNTkyIDgwLjg5ODQgOTUuMTU5MiA4MS4yODEyVjgxLjU1NDdDOTUuMTU5MiA4MS44NzgzIDk1LjIwMjUgODIuMTc5IDk1LjI4OTEgODIuNDU3Qzk1LjM4MDIgODIuNzMwNSA5NS41MTI0IDgyLjk2OTcgOTUuNjg1NSA4My4xNzQ4Qzk1Ljg1ODcgODMuMzc5OSA5Ni4wNjg0IDgzLjU0MTcgOTYuMzE0NSA4My42NjAyQzk2LjU2MDUgODMuNzc0MSA5Ni44NDA4IDgzLjgzMTEgOTcuMTU1MyA4My44MzExQzk3LjU1MTggODMuODMxMSA5Ny45MDQ5IDgzLjc1MTMgOTguMjE0OCA4My41OTE4Qzk4LjUyNDcgODMuNDMyMyA5OC43OTM2IDgzLjIwNjcgOTkuMDIxNSA4Mi45MTVMOTkuODg5NiA4My43NTU5Qzk5LjczMDEgODMuOTg4MyA5OS41MjI4IDg0LjIxMTYgOTkuMjY3NiA4NC40MjU4Qzk5LjAxMjQgODQuNjM1NCA5OC43MDAyIDg0LjgwNjMgOTguMzMxMSA4NC45Mzg1Qzk3Ljk2NjUgODUuMDcwNiA5Ny41NDI2IDg1LjEzNjcgOTcuMDU5NiA4NS4xMzY3Wk0xMDMuMjA5IDc5LjAxMTdWODVIMTAxLjU2MlY3Ny42MDM1SDEwMy4xMzRMMTAzLjIwOSA3OS4wMTE3Wk0xMDUuNDcyIDc3LjU1NTdMMTA1LjQ1OCA3OS4wODY5QzEwNS4zNTggNzkuMDY4NyAxMDUuMjQ4IDc5LjA1NSAxMDUuMTMgNzkuMDQ1OUMxMDUuMDE2IDc5LjAzNjggMTA0LjkwMiA3OS4wMzIyIDEwNC43ODggNzkuMDMyMkMxMDQuNTA2IDc5LjAzMjIgMTA0LjI1NyA3OS4wNzMyIDEwNC4wNDMgNzkuMTU1M0MxMDMuODI5IDc5LjIzMjcgMTAzLjY0OSA3OS4zNDY3IDEwMy41MDMgNzkuNDk3MUMxMDMuMzYyIDc5LjY0MjkgMTAzLjI1MiA3OS44MjA2IDEwMy4xNzUgODAuMDMwM0MxMDMuMDk3IDgwLjIzOTkgMTAzLjA1MiA4MC40NzQ2IDEwMy4wMzggODAuNzM0NEwxMDIuNjYyIDgwLjc2MTdDMTAyLjY2MiA4MC4yOTY5IDEwMi43MDggNzkuODY2MiAxMDIuNzk5IDc5LjQ2OTdDMTAyLjg5IDc5LjA3MzIgMTAzLjAyNyA3OC43MjQ2IDEwMy4yMDkgNzguNDIzOEMxMDMuMzk2IDc4LjEyMyAxMDMuNjI4IDc3Ljg4ODMgMTAzLjkwNiA3Ny43MTk3QzEwNC4xODkgNzcuNTUxMSAxMDQuNTE1IDc3LjQ2NjggMTA0Ljg4NCA3Ny40NjY4QzEwNC45ODQgNzcuNDY2OCAxMDUuMDkxIDc3LjQ3NTkgMTA1LjIwNSA3Ny40OTQxQzEwNS4zMjQgNzcuNTEyNCAxMDUuNDEyIDc3LjUzMjkgMTA1LjQ3MiA3Ny41NTU3Wk0xMTAuNjc4IDgzLjUxNjZWNzkuOTg5M0MxMTAuNjc4IDc5LjcyNDkgMTEwLjYzIDc5LjQ5NzEgMTEwLjUzNCA3OS4zMDU3QzExMC40MzggNzkuMTE0MyAxMTAuMjkzIDc4Ljk2NjEgMTEwLjA5NyA3OC44NjEzQzEwOS45MDUgNzguNzU2NSAxMDkuNjY0IDc4LjcwNDEgMTA5LjM3MiA3OC43MDQxQzEwOS4xMDMgNzguNzA0MSAxMDguODcxIDc4Ljc0OTcgMTA4LjY3NSA3OC44NDA4QzEwOC40NzkgNzguOTMyIDEwOC4zMjYgNzkuMDU1IDEwOC4yMTcgNzkuMjFDMTA4LjEwNyA3OS4zNjQ5IDEwOC4wNTMgNzkuNTQwNCAxMDguMDUzIDc5LjczNjNIMTA2LjQxMkMxMDYuNDEyIDc5LjQ0NDcgMTA2LjQ4MyA3OS4xNjIxIDEwNi42MjQgNzguODg4N0MxMDYuNzY1IDc4LjYxNTIgMTA2Ljk3IDc4LjM3MTQgMTA3LjIzOSA3OC4xNTcyQzEwNy41MDggNzcuOTQzIDEwNy44MjkgNzcuNzc0NCAxMDguMjAzIDc3LjY1MTRDMTA4LjU3NyA3Ny41MjgzIDEwOC45OTYgNzcuNDY2OCAxMDkuNDYxIDc3LjQ2NjhDMTEwLjAxNyA3Ny40NjY4IDExMC41MDkgNzcuNTYwMiAxMTAuOTM4IDc3Ljc0NzFDMTExLjM3IDc3LjkzMzkgMTExLjcxIDc4LjIxNjUgMTExLjk1NiA3OC41OTQ3QzExMi4yMDcgNzguOTY4NCAxMTIuMzMyIDc5LjQzNzggMTEyLjMzMiA4MC4wMDI5VjgzLjI5MUMxMTIuMzMyIDgzLjYyODMgMTEyLjM1NSA4My45MzEzIDExMi40IDg0LjIwMDJDMTEyLjQ1MSA4NC40NjQ1IDExMi41MjEgODQuNjk0NyAxMTIuNjEyIDg0Ljg5MDZWODVIMTEwLjkyNEMxMTAuODQ2IDg0LjgyMjMgMTEwLjc4NSA4NC41OTY3IDExMC43MzkgODQuMzIzMkMxMTAuNjk4IDg0LjA0NTIgMTEwLjY3OCA4My43NzY0IDExMC42NzggODMuNTE2NlpNMTEwLjkxNyA4MC41MDJMMTEwLjkzMSA4MS41MjA1SDEwOS43NDhDMTA5LjQ0MyA4MS41MjA1IDEwOS4xNzQgODEuNTUwMSAxMDguOTQxIDgxLjYwOTRDMTA4LjcwOSA4MS42NjQxIDEwOC41MTUgODEuNzQ2MSAxMDguMzYgODEuODU1NUMxMDguMjA1IDgxLjk2NDggMTA4LjA4OSA4Mi4wOTcgMTA4LjAxMiA4Mi4yNTJDMTA3LjkzNCA4Mi40MDY5IDEwNy44OTYgODIuNTgyNCAxMDcuODk2IDgyLjc3ODNDMTA3Ljg5NiA4Mi45NzQzIDEwNy45NDEgODMuMTU0MyAxMDguMDMyIDgzLjMxODRDMTA4LjEyMyA4My40Nzc5IDEwOC4yNTYgODMuNjAzMiAxMDguNDI5IDgzLjY5NDNDMTA4LjYwNiA4My43ODU1IDEwOC44MjEgODMuODMxMSAxMDkuMDcxIDgzLjgzMTFDMTA5LjQwOSA4My44MzExIDEwOS43MDIgODMuNzYyNyAxMDkuOTUzIDgzLjYyNkMxMTAuMjA4IDgzLjQ4NDcgMTEwLjQwOSA4My4zMTM4IDExMC41NTUgODMuMTEzM0MxMTAuNzAxIDgyLjkwODIgMTEwLjc3OCA4Mi43MTQ1IDExMC43ODcgODIuNTMyMkwxMTEuMzIgODMuMjYzN0MxMTEuMjY2IDgzLjQ1MDUgMTExLjE3MiA4My42NTEgMTExLjA0IDgzLjg2NTJDMTEwLjkwOCA4NC4wNzk0IDExMC43MzUgODQuMjg0NSAxMTAuNTIxIDg0LjQ4MDVDMTEwLjMxMSA4NC42NzE5IDExMC4wNTggODQuODI5MSAxMDkuNzYyIDg0Ljk1MjFDMTA5LjQ3IDg1LjA3NTIgMTA5LjEzMyA4NS4xMzY3IDEwOC43NSA4NS4xMzY3QzEwOC4yNjcgODUuMTM2NyAxMDcuODM2IDg1LjA0MSAxMDcuNDU4IDg0Ljg0OTZDMTA3LjA4IDg0LjY1MzYgMTA2Ljc4NCA4NC4zOTE2IDEwNi41NjkgODQuMDYzNUMxMDYuMzU1IDgzLjczMDggMTA2LjI0OCA4My4zNTQ4IDEwNi4yNDggODIuOTM1NUMxMDYuMjQ4IDgyLjU0MzYgMTA2LjMyMSA4Mi4xOTczIDEwNi40NjcgODEuODk2NUMxMDYuNjE3IDgxLjU5MTEgMTA2LjgzNiA4MS4zMzU5IDEwNy4xMjMgODEuMTMwOUMxMDcuNDE1IDgwLjkyNTggMTA3Ljc3IDgwLjc3MDggMTA4LjE4OSA4MC42NjZDMTA4LjYwOSA4MC41NTY2IDEwOS4wODcgODAuNTAyIDEwOS42MjUgODAuNTAySDExMC45MTdaTTExNy42OTUgNzcuNjAzNVY3OC44MDY2SDExMy41MjVWNzcuNjAzNUgxMTcuNjk1Wk0xMTQuNzI5IDc1Ljc5MkgxMTYuMzc2VjgyLjk1NjFDMTE2LjM3NiA4My4xODM5IDExNi40MDggODMuMzU5NCAxMTYuNDcyIDgzLjQ4MjRDMTE2LjU0IDgzLjYwMDkgMTE2LjYzMyA4My42ODA3IDExNi43NTIgODMuNzIxN0MxMTYuODcgODMuNzYyNyAxMTcuMDA5IDgzLjc4MzIgMTE3LjE2OSA4My43ODMyQzExNy4yODMgODMuNzgzMiAxMTcuMzkyIDgzLjc3NjQgMTE3LjQ5NyA4My43NjI3QzExNy42MDIgODMuNzQ5IDExNy42ODYgODMuNzM1NCAxMTcuNzUgODMuNzIxN0wxMTcuNzU3IDg0Ljk3OTVDMTE3LjYyIDg1LjAyMDUgMTE3LjQ2MSA4NS4wNTcgMTE3LjI3OCA4NS4wODg5QzExNy4xMDEgODUuMTIwOCAxMTYuODk2IDg1LjEzNjcgMTE2LjY2MyA4NS4xMzY3QzExNi4yODUgODUuMTM2NyAxMTUuOTUgODUuMDcwNiAxMTUuNjU4IDg0LjkzODVDMTE1LjM2NyA4NC44MDE4IDExNS4xMzkgODQuNTgwNyAxMTQuOTc1IDg0LjI3NTRDMTE0LjgxMSA4My45NzAxIDExNC43MjkgODMuNTY0NSAxMTQuNzI5IDgzLjA1ODZWNzUuNzkyWk0xMjMuNjY3IDgzLjI1NjhWNzcuNjAzNUgxMjUuMzIxVjg1SDEyMy43NjNMMTIzLjY2NyA4My4yNTY4Wk0xMjMuODk5IDgxLjcxODhMMTI0LjQ1MyA4MS43MDUxQzEyNC40NTMgODIuMjAxOCAxMjQuMzk4IDgyLjY1OTggMTI0LjI4OSA4My4wNzkxQzEyNC4xOCA4My40OTM4IDEyNC4wMTEgODMuODU2MSAxMjMuNzgzIDg0LjE2NkMxMjMuNTU1IDg0LjQ3MTQgMTIzLjI2NCA4NC43MTA2IDEyMi45MDggODQuODgzOEMxMjIuNTUzIDg1LjA1MjQgMTIyLjEyNyA4NS4xMzY3IDEyMS42MyA4NS4xMzY3QzEyMS4yNyA4NS4xMzY3IDEyMC45MzkgODUuMDg0MyAxMjAuNjM5IDg0Ljk3OTVDMTIwLjMzOCA4NC44NzQ3IDEyMC4wNzggODQuNzEyOSAxMTkuODU5IDg0LjQ5NDFDMTE5LjY0NSA4NC4yNzU0IDExOS40NzkgODMuOTkwNiAxMTkuMzYgODMuNjM5NkMxMTkuMjQyIDgzLjI4ODcgMTE5LjE4MyA4Mi44Njk1IDExOS4xODMgODIuMzgxOFY3Ny42MDM1SDEyMC44M1Y4Mi4zOTU1QzEyMC44MyA4Mi42NjQ0IDEyMC44NjIgODIuODkgMTIwLjkyNiA4My4wNzIzQzEyMC45OSA4My4yNSAxMjEuMDc2IDgzLjM5MzYgMTIxLjE4NiA4My41MDI5QzEyMS4yOTUgODMuNjEyMyAxMjEuNDIzIDgzLjY4OTggMTIxLjU2OCA4My43MzU0QzEyMS43MTQgODMuNzgwOSAxMjEuODY5IDgzLjgwMzcgMTIyLjAzMyA4My44MDM3QzEyMi41MDMgODMuODAzNyAxMjIuODcyIDgzLjcxMjYgMTIzLjE0MSA4My41MzAzQzEyMy40MTQgODMuMzQzNCAxMjMuNjA4IDgzLjA5MjggMTIzLjcyMiA4Mi43NzgzQzEyMy44NCA4Mi40NjM5IDEyMy44OTkgODIuMTEwNyAxMjMuODk5IDgxLjcxODhaTTEyOC45MTQgNzkuMDExN1Y4NUgxMjcuMjY3Vjc3LjYwMzVIMTI4LjgzOUwxMjguOTE0IDc5LjAxMTdaTTEzMS4xNzcgNzcuNTU1N0wxMzEuMTYzIDc5LjA4NjlDMTMxLjA2MyA3OS4wNjg3IDEzMC45NTMgNzkuMDU1IDEzMC44MzUgNzkuMDQ1OUMxMzAuNzIxIDc5LjAzNjggMTMwLjYwNyA3OS4wMzIyIDEzMC40OTMgNzkuMDMyMkMxMzAuMjExIDc5LjAzMjIgMTI5Ljk2MiA3OS4wNzMyIDEyOS43NDggNzkuMTU1M0MxMjkuNTM0IDc5LjIzMjcgMTI5LjM1NCA3OS4zNDY3IDEyOS4yMDggNzkuNDk3MUMxMjkuMDY3IDc5LjY0MjkgMTI4Ljk1NyA3OS44MjA2IDEyOC44OCA4MC4wMzAzQzEyOC44MDIgODAuMjM5OSAxMjguNzU3IDgwLjQ3NDYgMTI4Ljc0MyA4MC43MzQ0TDEyOC4zNjcgODAuNzYxN0MxMjguMzY3IDgwLjI5NjkgMTI4LjQxMyA3OS44NjYyIDEyOC41MDQgNzkuNDY5N0MxMjguNTk1IDc5LjA3MzIgMTI4LjczMiA3OC43MjQ2IDEyOC45MTQgNzguNDIzOEMxMjkuMTAxIDc4LjEyMyAxMjkuMzMzIDc3Ljg4ODMgMTI5LjYxMSA3Ny43MTk3QzEyOS44OTQgNzcuNTUxMSAxMzAuMjIgNzcuNDY2OCAxMzAuNTg5IDc3LjQ2NjhDMTMwLjY4OSA3Ny40NjY4IDEzMC43OTYgNzcuNDc1OSAxMzAuOTEgNzcuNDk0MUMxMzEuMDI5IDc3LjUxMjQgMTMxLjExOCA3Ny41MzI5IDEzMS4xNzcgNzcuNTU1N1pNMTM1LjU4MyA4NS4xMzY3QzEzNS4wMzYgODUuMTM2NyAxMzQuNTQyIDg1LjA0NzkgMTM0LjEgODQuODcwMUMxMzMuNjYyIDg0LjY4NzggMTMzLjI4OCA4NC40MzQ5IDEzMi45NzkgODQuMTExM0MxMzIuNjczIDgzLjc4NzggMTMyLjQzOCA4My40MDcyIDEzMi4yNzQgODIuOTY5N0MxMzIuMTEgODIuNTMyMiAxMzIuMDI4IDgyLjA2MDUgMTMyLjAyOCA4MS41NTQ3VjgxLjI4MTJDMTMyLjAyOCA4MC43MDI1IDEzMi4xMTMgODAuMTc4NCAxMzIuMjgxIDc5LjcwOUMxMzIuNDUgNzkuMjM5NiAxMzIuNjg1IDc4LjgzODUgMTMyLjk4NSA3OC41MDU5QzEzMy4yODYgNzguMTY4NiAxMzMuNjQyIDc3LjkxMTEgMTM0LjA1MiA3Ny43MzM0QzEzNC40NjIgNzcuNTU1NyAxMzQuOTA2IDc3LjQ2NjggMTM1LjM4NSA3Ny40NjY4QzEzNS45MTMgNzcuNDY2OCAxMzYuMzc2IDc3LjU1NTcgMTM2Ljc3MiA3Ny43MzM0QzEzNy4xNjkgNzcuOTExMSAxMzcuNDk3IDc4LjE2MTggMTM3Ljc1NyA3OC40ODU0QzEzOC4wMjEgNzguODA0NCAxMzguMjE3IDc5LjE4NDkgMTM4LjM0NSA3OS42MjdDMTM4LjQ3NyA4MC4wNjkgMTM4LjU0MyA4MC41NTY2IDEzOC41NDMgODEuMDg5OFY4MS43OTM5SDEzMi44MjhWODAuNjExM0gxMzYuOTE2VjgwLjQ4MTRDMTM2LjkwNyA4MC4xODUyIDEzNi44NDggNzkuOTA3MiAxMzYuNzM4IDc5LjY0NzVDMTM2LjYzMyA3OS4zODc3IDEzNi40NzIgNzkuMTc4MSAxMzYuMjUzIDc5LjAxODZDMTM2LjAzNCA3OC44NTkgMTM1Ljc0MyA3OC43NzkzIDEzNS4zNzggNzguNzc5M0MxMzUuMTA0IDc4Ljc3OTMgMTM0Ljg2MSA3OC44Mzg1IDEzNC42NDYgNzguOTU3QzEzNC40MzcgNzkuMDcxIDEzNC4yNjEgNzkuMjM3MyAxMzQuMTIgNzkuNDU2MUMxMzMuOTc5IDc5LjY3NDggMTMzLjg2OSA3OS45MzkxIDEzMy43OTIgODAuMjQ5QzEzMy43MTkgODAuNTU0NCAxMzMuNjgzIDgwLjg5ODQgMTMzLjY4MyA4MS4yODEyVjgxLjU1NDdDMTMzLjY4MyA4MS44NzgzIDEzMy43MjYgODIuMTc5IDEzMy44MTIgODIuNDU3QzEzMy45MDQgODIuNzMwNSAxMzQuMDM2IDgyLjk2OTcgMTM0LjIwOSA4My4xNzQ4QzEzNC4zODIgODMuMzc5OSAxMzQuNTkyIDgzLjU0MTcgMTM0LjgzOCA4My42NjAyQzEzNS4wODQgODMuNzc0MSAxMzUuMzY0IDgzLjgzMTEgMTM1LjY3OSA4My44MzExQzEzNi4wNzUgODMuODMxMSAxMzYuNDI4IDgzLjc1MTMgMTM2LjczOCA4My41OTE4QzEzNy4wNDggODMuNDMyMyAxMzcuMzE3IDgzLjIwNjcgMTM3LjU0NSA4Mi45MTVMMTM4LjQxMyA4My43NTU5QzEzOC4yNTQgODMuOTg4MyAxMzguMDQ2IDg0LjIxMTYgMTM3Ljc5MSA4NC40MjU4QzEzNy41MzYgODQuNjM1NCAxMzcuMjI0IDg0LjgwNjMgMTM2Ljg1NCA4NC45Mzg1QzEzNi40OSA4NS4wNzA2IDEzNi4wNjYgODUuMTM2NyAxMzUuNTgzIDg1LjEzNjdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNjIuOTI0IDgzLjY4NzVWODVIMTU2LjI1MlY4My44NzIxTDE1OS40OTIgODAuMzM3OUMxNTkuODQ4IDc5LjkzNjggMTYwLjEyOCA3OS41OTA1IDE2MC4zMzMgNzkuMjk4OEMxNjAuNTM4IDc5LjAwNzIgMTYwLjY4MiA3OC43NDUxIDE2MC43NjQgNzguNTEyN0MxNjAuODUgNzguMjc1NyAxNjAuODk0IDc4LjA0NTYgMTYwLjg5NCA3Ny44MjIzQzE2MC44OTQgNzcuNTA3OCAxNjAuODM0IDc3LjIzMjEgMTYwLjcxNiA3Ni45OTUxQzE2MC42MDIgNzYuNzUzNiAxNjAuNDMzIDc2LjU2NDUgMTYwLjIxIDc2LjQyNzdDMTU5Ljk4NyA3Ni4yODY1IDE1OS43MTUgNzYuMjE1OCAxNTkuMzk2IDc2LjIxNThDMTU5LjAyNyA3Ni4yMTU4IDE1OC43MTcgNzYuMjk1NiAxNTguNDY3IDc2LjQ1NTFDMTU4LjIxNiA3Ni42MTQ2IDE1OC4wMjcgNzYuODM1NiAxNTcuODk5IDc3LjExODJDMTU3Ljc3MiA3Ny4zOTYyIDE1Ny43MDggNzcuNzE1MiAxNTcuNzA4IDc4LjA3NTJIMTU2LjA2MUMxNTYuMDYxIDc3LjQ5NjQgMTU2LjE5MyA3Ni45Njc4IDE1Ni40NTcgNzYuNDg5M0MxNTYuNzIxIDc2LjAwNjIgMTU3LjEwNCA3NS42MjM0IDE1Ny42MDUgNzUuMzQwOEMxNTguMTA3IDc1LjA1MzcgMTU4LjcxMSA3NC45MTAyIDE1OS40MTcgNzQuOTEwMkMxNjAuMDgyIDc0LjkxMDIgMTYwLjY0NyA3NS4wMjE4IDE2MS4xMTIgNzUuMjQ1MUMxNjEuNTc3IDc1LjQ2ODQgMTYxLjkzIDc1Ljc4NTIgMTYyLjE3MiA3Ni4xOTUzQzE2Mi40MTggNzYuNjA1NSAxNjIuNTQxIDc3LjA5MDggMTYyLjU0MSA3Ny42NTE0QzE2Mi41NDEgNzcuOTYxMyAxNjIuNDkxIDc4LjI2ODkgMTYyLjM5MSA3OC41NzQyQzE2Mi4yOSA3OC44Nzk2IDE2Mi4xNDcgNzkuMTg0OSAxNjEuOTYgNzkuNDkwMkMxNjEuNzc4IDc5Ljc5MSAxNjEuNTYxIDgwLjA5NDEgMTYxLjMxMSA4MC4zOTk0QzE2MS4wNiA4MC43MDAyIDE2MC43ODQgODEuMDA1NSAxNjAuNDgzIDgxLjMxNTRMMTU4LjMzIDgzLjY4NzVIMTYyLjkyNFpNMTcxLjEzMSA4My42ODc1Vjg1SDE2NC40NTlWODMuODcyMUwxNjcuNjk5IDgwLjMzNzlDMTY4LjA1NSA3OS45MzY4IDE2OC4zMzUgNzkuNTkwNSAxNjguNTQgNzkuMjk4OEMxNjguNzQ1IDc5LjAwNzIgMTY4Ljg4OSA3OC43NDUxIDE2OC45NzEgNzguNTEyN0MxNjkuMDU3IDc4LjI3NTcgMTY5LjEwMSA3OC4wNDU2IDE2OS4xMDEgNzcuODIyM0MxNjkuMTAxIDc3LjUwNzggMTY5LjA0MSA3Ny4yMzIxIDE2OC45MjMgNzYuOTk1MUMxNjguODA5IDc2Ljc1MzYgMTY4LjY0IDc2LjU2NDUgMTY4LjQxNyA3Ni40Mjc3QzE2OC4xOTQgNzYuMjg2NSAxNjcuOTIzIDc2LjIxNTggMTY3LjYwNCA3Ni4yMTU4QzE2Ny4yMzQgNzYuMjE1OCAxNjYuOTI0IDc2LjI5NTYgMTY2LjY3NCA3Ni40NTUxQzE2Ni40MjMgNzYuNjE0NiAxNjYuMjM0IDc2LjgzNTYgMTY2LjEwNiA3Ny4xMTgyQzE2NS45NzkgNzcuMzk2MiAxNjUuOTE1IDc3LjcxNTIgMTY1LjkxNSA3OC4wNzUySDE2NC4yNjhDMTY0LjI2OCA3Ny40OTY0IDE2NC40IDc2Ljk2NzggMTY0LjY2NCA3Ni40ODkzQzE2NC45MjggNzYuMDA2MiAxNjUuMzExIDc1LjYyMzQgMTY1LjgxMiA3NS4zNDA4QzE2Ni4zMTQgNzUuMDUzNyAxNjYuOTE4IDc0LjkxMDIgMTY3LjYyNCA3NC45MTAyQzE2OC4yODkgNzQuOTEwMiAxNjguODU0IDc1LjAyMTggMTY5LjMxOSA3NS4yNDUxQzE2OS43ODQgNzUuNDY4NCAxNzAuMTM3IDc1Ljc4NTIgMTcwLjM3OSA3Ni4xOTUzQzE3MC42MjUgNzYuNjA1NSAxNzAuNzQ4IDc3LjA5MDggMTcwLjc0OCA3Ny42NTE0QzE3MC43NDggNzcuOTYxMyAxNzAuNjk4IDc4LjI2ODkgMTcwLjU5OCA3OC41NzQyQzE3MC40OTcgNzguODc5NiAxNzAuMzU0IDc5LjE4NDkgMTcwLjE2NyA3OS40OTAyQzE2OS45ODUgNzkuNzkxIDE2OS43NjggODAuMDk0MSAxNjkuNTE4IDgwLjM5OTRDMTY5LjI2NyA4MC43MDAyIDE2OC45OTEgODEuMDA1NSAxNjguNjkgODEuMzE1NEwxNjYuNTM3IDgzLjY4NzVIMTcxLjEzMVpNMTcyLjc5NiA3Ni43MzU0QzE3Mi43OTYgNzYuNDAyNyAxNzIuODc4IDc2LjA5NzMgMTczLjA0MiA3NS44MTkzQzE3My4yMDYgNzUuNTQxMyAxNzMuNDI1IDc1LjMyMDMgMTczLjY5OCA3NS4xNTYyQzE3My45NzYgNzQuOTg3NiAxNzQuMjc3IDc0LjkwMzMgMTc0LjYwMSA3NC45MDMzQzE3NC45MjkgNzQuOTAzMyAxNzUuMjI3IDc0Ljk4NzYgMTc1LjQ5NiA3NS4xNTYyQzE3NS43NjUgNzUuMzIwMyAxNzUuOTc5IDc1LjU0MTMgMTc2LjEzOSA3NS44MTkzQzE3Ni4zMDMgNzYuMDk3MyAxNzYuMzg1IDc2LjQwMjcgMTc2LjM4NSA3Ni43MzU0QzE3Ni4zODUgNzcuMDY4IDE3Ni4zMDMgNzcuMzczNCAxNzYuMTM5IDc3LjY1MTRDMTc1Ljk3OSA3Ny45MjQ4IDE3NS43NjUgNzguMTQxMyAxNzUuNDk2IDc4LjMwMDhDMTc1LjIyNyA3OC40NjAzIDE3NC45MjkgNzguNTQgMTc0LjYwMSA3OC41NEMxNzQuMjc3IDc4LjU0IDE3My45NzYgNzguNDYwMyAxNzMuNjk4IDc4LjMwMDhDMTczLjQyNSA3OC4xNDEzIDE3My4yMDYgNzcuOTI0OCAxNzMuMDQyIDc3LjY1MTRDMTcyLjg3OCA3Ny4zNzM0IDE3Mi43OTYgNzcuMDY4IDE3Mi43OTYgNzYuNzM1NFpNMTczLjcxOSA3Ni43MzU0QzE3My43MTkgNzYuOTgxNCAxNzMuODA1IDc3LjE4ODggMTczLjk3OSA3Ny4zNTc0QzE3NC4xNTIgNzcuNTIxNSAxNzQuMzU5IDc3LjYwMzUgMTc0LjYwMSA3Ny42MDM1QzE3NC44NDIgNzcuNjAzNSAxNzUuMDQ1IDc3LjUyMTUgMTc1LjIwOSA3Ny4zNTc0QzE3NS4zNzMgNzcuMTkzNCAxNzUuNDU1IDc2Ljk4NiAxNzUuNDU1IDc2LjczNTRDMTc1LjQ1NSA3Ni40ODAxIDE3NS4zNzMgNzYuMjY4MiAxNzUuMjA5IDc2LjA5OTZDMTc1LjA0NSA3NS45MzEgMTc0Ljg0MiA3NS44NDY3IDE3NC42MDEgNzUuODQ2N0MxNzQuMzU5IDc1Ljg0NjcgMTc0LjE1MiA3NS45MzEgMTczLjk3OSA3Ni4wOTk2QzE3My44MDUgNzYuMjY4MiAxNzMuNzE5IDc2LjQ4MDEgMTczLjcxOSA3Ni43MzU0Wk0xODQuNDE0IDgxLjc1OThIMTg2LjEyM0MxODYuMDY4IDgyLjQxMTUgMTg1Ljg4NiA4Mi45OTI1IDE4NS41NzYgODMuNTAyOUMxODUuMjY2IDg0LjAwODggMTg0LjgzMSA4NC40MDc2IDE4NC4yNzEgODQuNjk5MkMxODMuNzEgODQuOTkwOSAxODMuMDI5IDg1LjEzNjcgMTgyLjIyNyA4NS4xMzY3QzE4MS42MTEgODUuMTM2NyAxODEuMDU4IDg1LjAyNzMgMTgwLjU2NSA4NC44MDg2QzE4MC4wNzMgODQuNTg1MyAxNzkuNjUyIDg0LjI3MDggMTc5LjMwMSA4My44NjUyQzE3OC45NSA4My40NTUxIDE3OC42ODEgODIuOTYwNiAxNzguNDk0IDgyLjM4MThDMTc4LjMxMiA4MS44MDMxIDE3OC4yMjEgODEuMTU1OSAxNzguMjIxIDgwLjQ0MDRWNzkuNjEzM0MxNzguMjIxIDc4Ljg5NzggMTc4LjMxNCA3OC4yNTA3IDE3OC41MDEgNzcuNjcxOUMxNzguNjkyIDc3LjA5MzEgMTc4Ljk2NiA3Ni41OTg2IDE3OS4zMjEgNzYuMTg4NUMxNzkuNjc3IDc1Ljc3MzggMTgwLjEwMyA3NS40NTcgMTgwLjYgNzUuMjM4M0MxODEuMTAxIDc1LjAxOTUgMTgxLjY2NCA3NC45MTAyIDE4Mi4yODggNzQuOTEwMkMxODMuMDgxIDc0LjkxMDIgMTgzLjc1MSA3NS4wNTYgMTg0LjI5OCA3NS4zNDc3QzE4NC44NDUgNzUuNjM5MyAxODUuMjY5IDc2LjA0MjYgMTg1LjU2OSA3Ni41NTc2QzE4NS44NzUgNzcuMDcyNiAxODYuMDYyIDc3LjY2MjggMTg2LjEzIDc4LjMyODFIMTg0LjQyMUMxODQuMzc1IDc3Ljg5OTcgMTg0LjI3NSA3Ny41MzI5IDE4NC4xMiA3Ny4yMjc1QzE4My45NyA3Ni45MjIyIDE4My43NDYgNzYuNjg5OCAxODMuNDUgNzYuNTMwM0MxODMuMTU0IDc2LjM2NjIgMTgyLjc2NyA3Ni4yODQyIDE4Mi4yODggNzYuMjg0MkMxODEuODk2IDc2LjI4NDIgMTgxLjU1NCA3Ni4zNTcxIDE4MS4yNjMgNzYuNTAyOUMxODAuOTcxIDc2LjY0ODggMTgwLjcyNyA3Ni44NjMgMTgwLjUzMSA3Ny4xNDU1QzE4MC4zMzUgNzcuNDI4MSAxODAuMTg3IDc3Ljc3NjcgMTgwLjA4NyA3OC4xOTE0QzE3OS45OTEgNzguNjAxNiAxNzkuOTQzIDc5LjA3MSAxNzkuOTQzIDc5LjU5OTZWODAuNDQwNEMxNzkuOTQzIDgwLjk0MTcgMTc5Ljk4NyA4MS4zOTc1IDE4MC4wNzMgODEuODA3NkMxODAuMTY0IDgyLjIxMzIgMTgwLjMwMSA4Mi41NjE4IDE4MC40ODMgODIuODUzNUMxODAuNjcgODMuMTQ1MiAxODAuOTA3IDgzLjM3MDggMTgxLjE5NCA4My41MzAzQzE4MS40ODEgODMuNjg5OCAxODEuODI2IDgzLjc2OTUgMTgyLjIyNyA4My43Njk1QzE4Mi43MTQgODMuNzY5NSAxODMuMTA4IDgzLjY5MjEgMTgzLjQwOSA4My41MzcxQzE4My43MTUgODMuMzgyMiAxODMuOTQ1IDgzLjE1NjYgMTg0LjEgODIuODYwNEMxODQuMjU5IDgyLjU1OTYgMTg0LjM2NCA4Mi4xOTI3IDE4NC40MTQgODEuNzU5OFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kXzQ5ODhfNDUwNTYiIHg9IjAiIHk9IjQ4IiB3aWR0aD0iMjEyIiBoZWlnaHQ9IjY4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMyIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xMiAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ5ODhfNDUwNTYiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDk4OF80NTA1NiIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPGZpbHRlciBpZD0iZmlsdGVyMV9iXzQ5ODhfNDUwNTYiIHg9IjEyIiB5PSI1OCIgd2lkdGg9IjE4OCIgaGVpZ2h0PSI0NCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVHYXVzc2lhbkJsdXIgaW49IkJhY2tncm91bmRJbWFnZUZpeCIgc3RkRGV2aWF0aW9uPSIzIi8+CjxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80OTg4XzQ1MDU2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80OTg4XzQ1MDU2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=", "description": "Displays a single entity attribute or the latest telemetry with optional label and icon.", "descriptor": { "type": "latest", @@ -14,10 +14,12 @@ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.labelValueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.labelValueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "", - "hasBasicMode": false, - "basicModeDirective": "", + "settingsDirective": "tb-label-value-card-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-label-value-card-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Label & value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, - "tags": null + "tags": [ + "label" + ] } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 1a705c3c6f..ac8cb25d60 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -136,6 +136,9 @@ import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/con import { LabelCardBasicConfigComponent } from '@home/components/widget/config/basic/cards/label-card-basic-config.component'; +import { + LabelValueCardBasicConfigComponent +} from '@home/components/widget/config/basic/cards/label-value-card-basic-config.component'; @NgModule({ declarations: [ @@ -181,7 +184,8 @@ import { RadarChartBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent, MobileAppQrCodeBasicConfigComponent, - LabelCardBasicConfigComponent + LabelCardBasicConfigComponent, + LabelValueCardBasicConfigComponent ], imports: [ CommonModule, @@ -229,7 +233,8 @@ import { RadarChartBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent, MobileAppQrCodeBasicConfigComponent, - LabelCardBasicConfigComponent + LabelCardBasicConfigComponent, + LabelValueCardBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -271,5 +276,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + + +
+
widget-config.appearance
+
+ + {{ 'widgets.label-card.auto-scale' | translate }} + +
+
+ + {{ 'widgets.label-card.label' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.label-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
widgets.label-value-card.value
+
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts new file mode 100644 index 0000000000..5add2284ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts @@ -0,0 +1,191 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DataKey, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + WidgetConfig, +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { + labelValueCardWidgetDefaultSettings, + LabelValueCardWidgetSettings +} from '@home/components/widget/lib/cards/label-value-card-widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; + +@Component({ + selector: 'tb-label-value-card-basic-config', + templateUrl: './label-value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class LabelValueCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.labelValueCardWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.labelValueCardWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + labelValueCardWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.labelValueCardWidgetConfigForm; + } + + protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { + return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: LabelValueCardWidgetSettings = {...labelValueCardWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.labelValueCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + autoScale: [settings.autoScale, []], + + showLabel: [settings.showLabel, []], + label: [settings.label, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + padding: [settings.padding, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.autoScale = config.autoScale; + + this.widgetConfig.config.settings.showLabel = config.showLabel; + this.widgetConfig.config.settings.label = config.label; + this.widgetConfig.config.settings.labelFont = config.labelFont; + this.widgetConfig.config.settings.labelColor = config.labelColor; + + this.widgetConfig.config.settings.showIcon = config.showIcon; + this.widgetConfig.config.settings.iconSize = config.iconSize; + this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit; + this.widgetConfig.config.settings.icon = config.icon; + this.widgetConfig.config.settings.iconColor = config.iconColor; + + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.valueFont = config.valueFont; + this.widgetConfig.config.settings.valueColor = config.valueColor; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.settings.padding = config.padding; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'showIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showLabel: boolean = this.labelValueCardWidgetConfigForm.get('showLabel').value; + const showIcon: boolean = this.labelValueCardWidgetConfigForm.get('showIcon').value; + + if (showLabel) { + this.labelValueCardWidgetConfigForm.get('label').enable(); + this.labelValueCardWidgetConfigForm.get('labelFont').enable(); + this.labelValueCardWidgetConfigForm.get('labelColor').enable(); + } else { + this.labelValueCardWidgetConfigForm.get('label').disable(); + this.labelValueCardWidgetConfigForm.get('labelFont').disable(); + this.labelValueCardWidgetConfigForm.get('labelColor').disable(); + } + + if (showIcon) { + this.labelValueCardWidgetConfigForm.get('iconSize').enable(); + this.labelValueCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.labelValueCardWidgetConfigForm.get('icon').enable(); + this.labelValueCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.labelValueCardWidgetConfigForm.get('iconSize').disable(); + this.labelValueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.labelValueCardWidgetConfigForm.get('icon').disable(); + this.labelValueCardWidgetConfigForm.get('iconColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _valuePreviewFn(): string { + const units: string = this.labelValueCardWidgetConfigForm.get('units').value; + const decimals: number = this.labelValueCardWidgetConfigForm.get('decimals').value; + return formatValue(22, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts index c4aecd7943..9a47a5db9d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts @@ -165,10 +165,8 @@ export class LabelValueCardWidgetComponent implements OnInit, AfterViewInit, OnD public onDataUpdated() { const tsValue = getSingleTsValue(this.ctx.data); - let ts; let value; if (tsValue && isDefinedAndNotNull(tsValue[1]) && tsValue[0] !== 0) { - ts = tsValue[0]; value = tsValue[1]; this.valueText = formatValue(value, this.decimals, this.units, false); } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts index b3ebca2ae8..166170c8ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Injector } from '@angular/core'; +import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; @@ -31,7 +31,6 @@ export class LabelCardWidgetSettingsComponent extends WidgetSettingsComponent { labelCardWidgetSettingsForm: UntypedFormGroup; constructor(protected store: Store, - private $injector: Injector, private fb: UntypedFormBuilder) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html new file mode 100644 index 0000000000..b10d497f6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html @@ -0,0 +1,81 @@ + + +
+
widgets.label-value-card.label-value-card-style
+
+ + {{ 'widgets.label-card.auto-scale' | translate }} + +
+
+ + {{ 'widgets.label-card.label' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.label-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
widgets.label-value-card.value
+
+ + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts new file mode 100644 index 0000000000..b8c80ba0de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts @@ -0,0 +1,108 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { labelValueCardWidgetDefaultSettings } from '@home/components/widget/lib/cards/label-value-card-widget.models'; +import { formatValue } from '@core/utils'; + +@Component({ + selector: 'tb-label-value-card-widget-settings', + templateUrl: './label-value-card-widget-settings.component.html', + styleUrls: [] +}) +export class LabelValueCardWidgetSettingsComponent extends WidgetSettingsComponent { + + labelValueCardWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.labelValueCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...labelValueCardWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.labelValueCardWidgetSettingsForm = this.fb.group({ + autoScale: [settings.autoScale, []], + + showLabel: [settings.showLabel, []], + label: [settings.label, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + background: [settings.background, []], + padding: [settings.padding, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'showIcon']; + } + + protected updateValidators(emitEvent: boolean) { + const showLabel: boolean = this.labelValueCardWidgetSettingsForm.get('showLabel').value; + const showIcon: boolean = this.labelValueCardWidgetSettingsForm.get('showIcon').value; + + if (showLabel) { + this.labelValueCardWidgetSettingsForm.get('label').enable(); + this.labelValueCardWidgetSettingsForm.get('labelFont').enable(); + this.labelValueCardWidgetSettingsForm.get('labelColor').enable(); + } else { + this.labelValueCardWidgetSettingsForm.get('label').disable(); + this.labelValueCardWidgetSettingsForm.get('labelFont').disable(); + this.labelValueCardWidgetSettingsForm.get('labelColor').disable(); + } + + if (showIcon) { + this.labelValueCardWidgetSettingsForm.get('iconSize').enable(); + this.labelValueCardWidgetSettingsForm.get('iconSizeUnit').enable(); + this.labelValueCardWidgetSettingsForm.get('icon').enable(); + this.labelValueCardWidgetSettingsForm.get('iconColor').enable(); + } else { + this.labelValueCardWidgetSettingsForm.get('iconSize').disable(); + this.labelValueCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.labelValueCardWidgetSettingsForm.get('icon').disable(); + this.labelValueCardWidgetSettingsForm.get('iconColor').disable(); + } + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 47db116fbd..e4df67e2f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -359,6 +359,9 @@ import { import { LabelCardWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-card-widget-settings.component'; +import { + LabelValueCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/label-value-card-widget-settings.component'; @NgModule({ declarations: [ @@ -486,7 +489,8 @@ import { BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, RadarChartWidgetSettingsComponent, - LabelCardWidgetSettingsComponent + LabelCardWidgetSettingsComponent, + LabelValueCardWidgetSettingsComponent ], imports: [ CommonModule, @@ -619,7 +623,8 @@ import { BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, RadarChartWidgetSettingsComponent, - LabelCardWidgetSettingsComponent + LabelCardWidgetSettingsComponent, + LabelValueCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -719,5 +724,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Mon, 24 Jun 2024 16:42:15 +0300 Subject: [PATCH 34/34] Message count node: added tests (#10541) * added tests for msg count node * fixed tests * added test for default config and added check to verify all acknowledged msgs * changed usage when() to given() * added new line --- .../engine/action/TbMsgCountNodeTest.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java new file mode 100644 index 0000000000..8b2b786955 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java @@ -0,0 +1,155 @@ +/** + * 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.action; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +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.util.ArrayList; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.times; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class TbMsgCountNodeTest { + + private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("ee682a85-7f5a-4182-91bc-46e555138fe2")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1b21c7cc-0c9e-4ab1-b867-99451599e146")); + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("04dfbd38-10e5-47b7-925f-11e795db89e1")); + + private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("msg-count-node-test"); + private final TbMsg tickMsg = TbMsg.newMsg(TbMsgType.MSG_COUNT_SELF_MSG, RULE_NODE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + + private ScheduledExecutorService executorService; + private TbMsgCountNode node; + private TbMsgCountNodeConfiguration config; + + @Mock + private TbContext ctxMock; + + @BeforeEach + public void setUp() { + node = new TbMsgCountNode(); + config = new TbMsgCountNodeConfiguration().defaultConfiguration(); + executorService = Executors.newSingleThreadScheduledExecutor(factory); + } + + @AfterEach + public void tearDown() { + if (executorService != null) { + executorService.shutdownNow(); + } + node.destroy(); + } + + @Test + public void verifyDefaultConfig() { + assertThat(config.getInterval()).isEqualTo(1); + assertThat(config.getTelemetryPrefix()).isEqualTo("messageCount"); + } + + @Test + public void givenIncomingMsgs_whenOnMsg_thenSendsMsgWithMsgCount() throws TbNodeException, InterruptedException { + // GIVEN + int msgCount = 100; + var awaitTellSelfLatch = new CountDownLatch(1); + var currentMsgNumber = new AtomicInteger(0); + var msgWithCounterSent = new AtomicBoolean(false); + + willAnswer((Answer) invocationOnMock -> { + executorService.schedule(() -> { + TbMsg tickMsg = invocationOnMock.getArgument(0); + msgWithCounterSent.set(true); + node.onMsg(ctxMock, tickMsg); + awaitTellSelfLatch.countDown(); + }, config.getInterval(), TimeUnit.SECONDS); + return null; + }).given(ctxMock).tellSelf(any(TbMsg.class), anyLong()); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getServiceId()).willReturn("tb-rule-engine"); + given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID); + given(ctxMock.newMsg(null, TbMsgType.MSG_COUNT_SELF_MSG, RULE_NODE_ID, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING)).willReturn(tickMsg); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var expectedProcessedMsgs = new ArrayList(); + for (int i = 0; i < msgCount; i++) { + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + if (msgWithCounterSent.get()) { + break; + } + node.onMsg(ctxMock, msg); + expectedProcessedMsgs.add(msg); + currentMsgNumber.getAndIncrement(); + } + + awaitTellSelfLatch.await(); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should(times(currentMsgNumber.get())).ack(msgCaptor.capture()); + var actualProcessedMsgs = msgCaptor.getAllValues(); + assertThat(actualProcessedMsgs).hasSize(expectedProcessedMsgs.size()); + assertThat(actualProcessedMsgs).isNotEmpty(); + assertThat(actualProcessedMsgs).containsExactlyInAnyOrderElementsOf(expectedProcessedMsgs); + + ArgumentCaptor msgWithCounterCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().enqueueForTellNext(msgWithCounterCaptor.capture(), eq(TbNodeConnectionType.SUCCESS)); + TbMsg resultedMsg = msgWithCounterCaptor.getValue(); + String expectedData = "{\"messageCount_tb-rule-engine\":" + currentMsgNumber + "}"; + TbMsg expectedMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, expectedData); + assertThat(resultedMsg).usingRecursiveComparison() + .ignoringFields("id", "ts", "ctx", "metaData") + .isEqualTo(expectedMsg); + Map actualMetadata = resultedMsg.getMetaData().getData(); + assertThat(actualMetadata).hasFieldOrProperty("delta"); + } + +}