diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 177814df4a..747ec345d9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -65,6 +65,10 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE, ALARM_ORIGINATOR_SOURCE, ENTITY_SOURCE); if (!knownSources.contains(conf.getOriginatorSource())) { - log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource()); - throw new IllegalArgumentException("Source '" + conf.getOriginatorSource() + "' is unsupported."); + log.error("Unsupported source type '[{}]'! Only {} types are allowed.", conf.getOriginatorSource(), supportedOriginatorSourcesStr); + throw new IllegalArgumentException("Unsupported source type '" + conf.getOriginatorSource() + + "'! Only " + supportedOriginatorSourcesStr + " types are allowed."); } if (conf.getOriginatorSource().equals(RELATED_SOURCE)) { if (conf.getRelationsQuery() == null) { - log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]", - conf.getRelationsQuery()); - throw new IllegalArgumentException("Relations query should be specified for the related entity."); + log.error("Relations query should be specified if 'Related entity' source is selected."); + throw new IllegalArgumentException("Relations query should be specified if 'Related entity' source is selected."); } } if (conf.getOriginatorSource().equals(ENTITY_SOURCE)) { if (conf.getEntityType() == null) { - log.error("Entity type not specified for [{}]", ENTITY_SOURCE); - throw new IllegalArgumentException("Entity type should be specified for the entity."); + log.error("Entity type should be specified if '[{}]' source is selected.", ENTITY_SOURCE); + throw new IllegalArgumentException("Entity type should be specified if 'Entity by name pattern' source is selected."); } if (StringUtils.isEmpty(conf.getEntityNamePattern())) { - log.error("EntityNamePattern not specified for type [{}]", conf.getEntityType()); - throw new IllegalArgumentException("Entity name pattern should be specified for the entity."); + log.error("Name pattern should be specified if '[{}]' source is selected.", ENTITY_SOURCE); + throw new IllegalArgumentException("Name pattern should be specified if 'Entity by name pattern' source is selected."); } EntitiesByNameAndTypeLoader.checkEntityType(EntityType.valueOf(conf.getEntityType())); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index be328a0aec..8e72f92fc5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -32,8 +34,10 @@ import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AlarmId; @@ -58,8 +62,11 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -72,11 +79,22 @@ import static org.mockito.BDDMockito.then; public class TbChangeOriginatorNodeTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("79830b6d-4f93-49bd-9b5b-d31ce51da77b")); + private final CustomerId CUSTOMER_ID = new CustomerId(UUID.fromString("c6b2c94b-5517-4f20-bf8e-ae9407eb8a7a")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("990605a4-db46-4ed4-942f-e18200453571")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("55de3f10-1b55-4950-b711-ed132896b260")); private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor(); + private final String CUSTOMER_SOURCE = "CUSTOMER"; + private final String TENANT_SOURCE = "TENANT"; + private final String RELATED_SOURCE = "RELATED"; + private final String ALARM_ORIGINATOR_SOURCE = "ALARM_ORIGINATOR"; + private final String ENTITY_SOURCE = "ENTITY"; + + private final String supportedOriginatorSourcesStr = String.join(", ", List.of( + CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE, ALARM_ORIGINATOR_SOURCE, ENTITY_SOURCE) + ); + private TbChangeOriginatorNode node; private TbChangeOriginatorNodeConfiguration config; @@ -99,52 +117,37 @@ public class TbChangeOriginatorNodeTest { @Test public void newChainCanBeStarted() throws TbNodeException { - init(); - AssetId assetId = new AssetId(Uuids.timeBased()); - CustomerId customerId = new CustomerId(Uuids.timeBased()); Asset asset = new Asset(); - asset.setCustomerId(customerId); + asset.setCustomerId(CUSTOMER_ID); RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON,TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, ASSET_ID, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getAssetService()).willReturn(assetServiceMock); - given(assetServiceMock.findAssetByIdAsync(any(), eq(assetId))).willReturn(Futures.immediateFuture(asset)); + given(assetServiceMock.findAssetByIdAsync(any(), eq(ASSET_ID))).willReturn(Futures.immediateFuture(asset)); + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); node.onMsg(ctxMock, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); then(ctxMock).should().transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); - assertThat(originatorCaptor.getValue()).isEqualTo(customerId); - } - - public void init() throws TbNodeException { - TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration(); - config.setOriginatorSource("CUSTOMER"); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - - node = new TbChangeOriginatorNode(); - node.init(null, nodeConfiguration); + assertThat(originatorCaptor.getValue()).isEqualTo(CUSTOMER_ID); } @Test public void verifyDefaultConfig() { var config = new TbChangeOriginatorNodeConfiguration().defaultConfiguration(); - assertThat(config.getOriginatorSource()).isEqualTo("CUSTOMER"); - + assertThat(config.getOriginatorSource()).isEqualTo(CUSTOMER_SOURCE); RelationsQuery relationsQuery = new RelationsQuery(); relationsQuery.setDirection(EntitySearchDirection.FROM); relationsQuery.setMaxLevel(1); RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter)); assertThat(config.getRelationsQuery()).isEqualTo(relationsQuery); - assertThat(config.getEntityType()).isNull(); assertThat(config.getEntityNamePattern()).isNull(); } @@ -155,45 +158,45 @@ public class TbChangeOriginatorNodeTest { assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Source 'UNSUPPORTED_SOURCE' is unsupported."); + .hasMessage("Unsupported source type 'UNSUPPORTED_SOURCE'! Only " + supportedOriginatorSourcesStr + " types are allowed."); } @Test public void givenRelatedSourceAndRelatedQueryIsNull_whenInit_thenThrowsException() { - config.setOriginatorSource("RELATED"); + config.setOriginatorSource(RELATED_SOURCE); config.setRelationsQuery(null); assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relations query should be specified for the related entity."); + .hasMessage("Relations query should be specified if 'Related entity' source is selected."); } @Test public void givenEntitySourceAndEntityTypeIsNull_whenInit_thenThrowsException() { - config.setOriginatorSource("ENTITY"); + config.setOriginatorSource(ENTITY_SOURCE); config.setEntityType(null); assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Entity type should be specified for the entity."); + .hasMessage("Entity type should be specified if 'Entity by name pattern' source is selected."); } @ParameterizedTest @NullAndEmptySource public void givenEntitySourceAndEntityNamePatternIsEmpty_whenInit_thenThrowsException(String entityName) { - config.setOriginatorSource("ENTITY"); - config.setEntityType("DEVICE"); + config.setOriginatorSource(ENTITY_SOURCE); + config.setEntityType(EntityType.DEVICE.name()); config.setEntityNamePattern(entityName); assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Entity name pattern should be specified for the entity."); + .hasMessage("Name pattern should be specified if 'Entity by name pattern' source is selected."); } @Test public void givenEntitySourceAndUnexpectedEntityType_whenInit_thenThrowsException() { - config.setOriginatorSource("ENTITY"); - config.setEntityType("TENANT"); + config.setOriginatorSource(ENTITY_SOURCE); + config.setEntityType(EntityType.TENANT.name()); config.setEntityNamePattern("tenant-A"); assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) @@ -203,14 +206,11 @@ public class TbChangeOriginatorNodeTest { @Test public void givenOriginatorSourceIsCustomer_whenOnMsg_thenTellSuccess() throws TbNodeException { - config.setOriginatorSource("CUSTOMER"); - - CustomerId customerId = new CustomerId(UUID.fromString("d2746ed1-2c45-41d7-b34c-94f947f2aa03")); Device device = new Device(DEVICE_ID); - device.setCustomerId(customerId); + device.setCustomerId(CUSTOMER_ID); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); - TbMsg expectedMsg = TbMsg.transformMsgOriginator(msg, customerId); + TbMsg expectedMsg = TbMsg.transformMsgOriginator(msg, CUSTOMER_ID); given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getDeviceService()).willReturn(deviceServiceMock); @@ -229,7 +229,7 @@ public class TbChangeOriginatorNodeTest { @Test public void givenOriginatorSourceIsTenant_whenOnMsg_thenTellSuccess() throws TbNodeException { - config.setOriginatorSource("TENANT"); + config.setOriginatorSource(TENANT_SOURCE); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, ASSET_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); TbMsg expectedMsg = TbMsg.transformMsgOriginator(msg, TENANT_ID); @@ -248,7 +248,7 @@ public class TbChangeOriginatorNodeTest { @Test public void givenOriginatorSourceIsRelatedAndNewOriginatorIsNull_whenOnMsg_thenTellFailure() throws TbNodeException { - config.setOriginatorSource("RELATED"); + config.setOriginatorSource(RELATED_SOURCE); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, ASSET_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); @@ -278,7 +278,7 @@ public class TbChangeOriginatorNodeTest { @Test public void givenOriginatorSourceIsAlarmOriginator_whenOnMsg_thenTellSuccess() throws TbNodeException { - config.setOriginatorSource("ALARM_ORIGINATOR"); + config.setOriginatorSource(ALARM_ORIGINATOR_SOURCE); AlarmId alarmId = new AlarmId(UUID.fromString("6b43f694-cb5f-4199-9023-e9e40eeb82dd")); Alarm alarm = new Alarm(alarmId); @@ -302,15 +302,14 @@ public class TbChangeOriginatorNodeTest { assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); } - @Test - public void givenOriginatorSourceIsEntity_whenOnMsg_thenTellSuccess() throws TbNodeException { - config.setOriginatorSource("ENTITY"); - config.setEntityType("ASSET"); - config.setEntityNamePattern("${md-name-pattern}"); + @ParameterizedTest + @MethodSource + public void givenOriginatorSourceIsEntity_whenOnMsg_thenTellSuccess(String entityNamePattern, TbMsgMetaData metaData, String data) throws TbNodeException { + config.setOriginatorSource(ENTITY_SOURCE); + config.setEntityType(EntityType.ASSET.name()); + config.setEntityNamePattern(entityNamePattern); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("md-name-pattern", "test-asset"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, TbMsg.EMPTY_JSON_OBJECT); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data); TbMsg expectedMsg = TbMsg.transformMsgOriginator(msg, ASSET_ID); given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); @@ -322,16 +321,25 @@ public class TbChangeOriginatorNodeTest { node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); node.onMsg(ctxMock, msg); - then(assetServiceMock).should().findAssetByTenantIdAndName(TENANT_ID, "test-asset"); + String expectedEntityName = TbNodeUtils.processPattern(entityNamePattern, msg); + then(assetServiceMock).should().findAssetByTenantIdAndName(TENANT_ID, expectedEntityName); ArgumentCaptor actualMsg = ArgumentCaptor.forClass(TbMsg.class); then(ctxMock).should().tellSuccess(actualMsg.capture()); assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); } + private static Stream givenOriginatorSourceIsEntity_whenOnMsg_thenTellSuccess() { + return Stream.of( + Arguments.of("test-asset", TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT), + Arguments.of("${md-name-pattern}", new TbMsgMetaData(Map.of("md-name-pattern", "md-test-asset")), TbMsg.EMPTY_JSON_OBJECT), + Arguments.of("${msg-name-pattern}", TbMsgMetaData.EMPTY, "{\"msg-name-pattern\":\"msg-test-asset\"}") + ); + } + @Test public void givenOriginatorSourceIsEntityAndEntityCouldNotFound_whenOnMsg_thenTellFailure() throws TbNodeException { - config.setOriginatorSource("ENTITY"); - config.setEntityType("ASSET"); + config.setOriginatorSource(ENTITY_SOURCE); + config.setEntityType(EntityType.ASSET.name()); config.setEntityNamePattern("${md-name-pattern}"); TbMsgMetaData metaData = new TbMsgMetaData();