@ -18,9 +18,20 @@ package org.thingsboard.rule.engine.telemetry;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import lombok.extern.slf4j.Slf4j ;
import org.junit.jupiter.api.BeforeEach ;
import org.junit.jupiter.api.Test ;
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.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.DataConstants ;
import org.thingsboard.server.common.data.id.DeviceId ;
import org.thingsboard.server.common.data.id.TenantId ;
import org.thingsboard.server.common.data.kv.AttributeKvEntry ;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry ;
import org.thingsboard.server.common.data.kv.BooleanDataEntry ;
@ -28,27 +39,47 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry ;
import org.thingsboard.server.common.data.kv.LongDataEntry ;
import org.thingsboard.server.common.data.kv.StringDataEntry ;
import org.thingsboard.server.common.data.msg.TbMsgType ;
import org.thingsboard.server.common.data.util.TbPair ;
import org.thingsboard.server.common.msg.TbMsg ;
import org.thingsboard.server.common.msg.TbMsgMetaData ;
import java.util.ArrayList ;
import java.util.Collections ;
import java.util.List ;
import java.util.UUID ;
import java.util.stream.Stream ;
import static org.assertj.core.api.Assertions.assertThat ;
import static org.mockito.ArgumentMatchers.any ;
import static org.mockito.ArgumentMatchers.anyBoolean ;
import static org.mockito.ArgumentMatchers.anyInt ;
import static org.mockito.ArgumentMatchers.anyString ;
import static org.mockito.ArgumentMatchers.eq ;
import static org.mockito.BDDMockito.willCallRealMethod ;
import static org.mockito.Mockito.mock ;
import static org.mockito.Mockito.spy ;
import static org.mockito.Mockito.times ;
import static org.mockito.Mockito.verify ;
import static org.mockito.Mockito.when ;
import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY ;
@Slf4j
class TbMsgAttributesNodeTest {
final String updateAttributesOnlyOnValueChangeKey = "updateAttributesOnlyOnValueChange" ;
private TenantId tenantId ;
private DeviceId deviceId ;
private TbMsgAttributesNode node ;
@BeforeEach
void setUp ( ) {
tenantId = new TenantId ( UUID . fromString ( "6c18691e-4470-4766-9739-aface71d761f" ) ) ;
deviceId = new DeviceId ( UUID . fromString ( "b66159d7-c77e-45e8-bb41-a8f557f434c1" ) ) ;
node = spy ( TbMsgAttributesNode . class ) ;
}
@Test
void testFilterChangedAttr_whenCurrentAttributesEmpty_thenReturnNewAttributes ( ) {
TbMsgAttributesNode node = spy ( TbMsgAttributesNode . class ) ;
List < AttributeKvEntry > newAttributes = new ArrayList < > ( ) ;
List < AttributeKvEntry > filtered = node . filterChangedAttr ( Collections . emptyList ( ) , newAttributes ) ;
@ -57,7 +88,6 @@ class TbMsgAttributesNodeTest {
@Test
void testFilterChangedAttr_whenCurrentAttributesContainsInAnyOrderNewAttributes_thenReturnEmptyList ( ) {
TbMsgAttributesNode node = spy ( TbMsgAttributesNode . class ) ;
List < AttributeKvEntry > currentAttributes = List . of (
new BaseAttributeKvEntry ( 1694000000L , new StringDataEntry ( "address" , "Peremohy ave 1" ) ) ,
new BaseAttributeKvEntry ( 1694000000L , new BooleanDataEntry ( "valid" , true ) ) ,
@ -78,7 +108,6 @@ class TbMsgAttributesNodeTest {
@Test
void testFilterChangedAttr_whenCurrentAttributesContainsInAnyOrderNewAttributes_thenReturnExpectedList ( ) {
TbMsgAttributesNode node = spy ( TbMsgAttributesNode . class ) ;
List < AttributeKvEntry > currentAttributes = List . of (
new BaseAttributeKvEntry ( 1694000000L , new StringDataEntry ( "address" , "Peremohy ave 1" ) ) ,
new BaseAttributeKvEntry ( 1694000000L , new BooleanDataEntry ( "valid" , true ) ) ,
@ -103,41 +132,112 @@ class TbMsgAttributesNodeTest {
assertThat ( filtered ) . containsExactlyInAnyOrderElementsOf ( expected ) ;
}
@Test
void testUpgrade_fromVersion0 ( ) throws TbNodeException {
// Notify device backward-compatibility test arguments
private static Stream < Arguments > givenNotifyDeviceMdValue_whenSaveAndNotify_thenVerifyExpectedArgumentForNotifyDeviceInSaveAndNotifyMethod ( ) {
return Stream . of (
Arguments . of ( null , true ) ,
Arguments . of ( "null" , false ) ,
Arguments . of ( "true" , true ) ,
Arguments . of ( "false" , false )
) ;
}
TbMsgAttributesNode node = mock ( TbMsgAttributesNode . class ) ;
willCallRealMethod ( ) . given ( node ) . upgrade ( anyInt ( ) , any ( ) ) ;
// Notify device backward-compatibility test
@ParameterizedTest
@MethodSource
void givenNotifyDeviceMdValue_whenSaveAndNotify_thenVerifyExpectedArgumentForNotifyDeviceInSaveAndNotifyMethod ( String mdValue , boolean expectedArgumentValue ) throws TbNodeException {
var ctxMock = mock ( TbContext . class ) ;
var telemetryServiceMock = mock ( RuleEngineTelemetryService . class ) ;
ObjectNode defaultConfig = ( ObjectNode ) JacksonUtil . valueToTree ( new TbMsgAttributesNodeConfiguration ( ) . defaultConfiguration ( ) ) ;
defaultConfig . put ( "notifyDevice" , false ) ;
var tbNodeConfiguration = new TbNodeConfiguration ( defaultConfig ) ;
assertThat ( defaultConfig . has ( "notifyDevice" ) ) . as ( "pre condition has notifyDevice" ) . isTrue ( ) ;
when ( ctxMock . getTenantId ( ) ) . thenReturn ( tenantId ) ;
when ( ctxMock . getTelemetryService ( ) ) . thenReturn ( telemetryServiceMock ) ;
willCallRealMethod ( ) . given ( node ) . init ( any ( TbContext . class ) , any ( TbNodeConfiguration . class ) ) ;
willCallRealMethod ( ) . given ( node ) . saveAttr ( any ( ) , eq ( ctxMock ) , any ( TbMsg . class ) , anyString ( ) , anyBoolean ( ) ) ;
node . init ( ctxMock , tbNodeConfiguration ) ;
ObjectNode jsonNode = ( ObjectNode ) JacksonUtil . valueToTree ( new TbMsgAttributesNodeConfiguration ( ) . defaultConfiguration ( ) ) ;
jsonNode . remove ( updateAttributesOnlyOnValueChangeKey ) ;
assertThat ( jsonNode . has ( updateAttributesOnlyOnValueChangeKey ) ) . as ( "pre condition has no " + updateAttributesOnlyOnValueChangeKey ) . isFalse ( ) ;
TbMsgMetaData md = new TbMsgMetaData ( ) ;
if ( mdValue ! = null ) {
md . putValue ( NOTIFY_DEVICE_METADATA_KEY , mdValue ) ;
}
// dummy list with one ts kv to pass the empty list check.
var testTbMsg = TbMsg . newMsg ( TbMsgType . POST_TELEMETRY_REQUEST , deviceId , md , TbMsg . EMPTY_STRING ) ;
List < AttributeKvEntry > testAttrList = List . of ( new BaseAttributeKvEntry ( 0L , new StringDataEntry ( "testKey" , "testValue" ) ) ) ;
TbPair < Boolean , JsonNode > upgradeResult = node . upgrade ( 0 , jsonNode ) ;
node . saveAttr ( testAttrList , ctxMock , testTbMsg , DataConstants . SHARED_SCOPE , fals e) ;
ObjectNode resultNode = ( ObjectNode ) upgradeResult . getSecond ( ) ;
assertThat ( upgradeResult . getFirst ( ) ) . as ( "upgrade result has changes" ) . isTrue ( ) ;
assertThat ( resultNode . has ( updateAttributesOnlyOnValueChangeKey ) ) . as ( "upgrade result has key " + updateAttributesOnlyOnValueChangeKey ) . isTrue ( ) ;
assertThat ( resultNode . get ( updateAttributesOnlyOnValueChangeKey ) . asBoolean ( ) ) . as ( "upgrade result value [false] for key " + updateAttributesOnlyOnValueChangeKey ) . isFalse ( ) ;
ArgumentCaptor < Boolean > notifyDeviceCaptor = ArgumentCaptor . forClass ( Boolean . class ) ;
verify ( telemetryServiceMock , times ( 1 ) ) . saveAndNotify (
eq ( tenantId ) , eq ( deviceId ) , eq ( DataConstants . SHARED_SCOPE ) ,
eq ( testAttrList ) , notifyDeviceCaptor . capture ( ) , any ( )
) ;
boolean notifyDevice = notifyDeviceCaptor . getValue ( ) ;
assertThat ( notifyDevice ) . isEqualTo ( expectedArgumentValue ) ;
}
@Test
void testUpgrade_fromVersion0_alreadyHasupdateAttributesOnlyOnValueChange ( ) throws TbNodeException {
TbMsgAttributesNode node = mock ( TbMsgAttributesNode . class ) ;
willCallRealMethod ( ) . given ( node ) . upgrade ( anyInt ( ) , any ( ) ) ;
ObjectNode jsonNode = ( ObjectNode ) JacksonUtil . valueToTree ( new TbMsgAttributesNodeConfiguration ( ) . defaultConfiguration ( ) ) ;
jsonNode . remove ( updateAttributesOnlyOnValueChangeKey ) ;
jsonNode . put ( updateAttributesOnlyOnValueChangeKey , true ) ;
assertThat ( jsonNode . has ( updateAttributesOnlyOnValueChangeKey ) ) . as ( "pre condition has no " + updateAttributesOnlyOnValueChangeKey ) . isTrue ( ) ;
assertThat ( jsonNode . get ( updateAttributesOnlyOnValueChangeKey ) . asBoolean ( ) ) . as ( "pre condition has [true] for key " + updateAttributesOnlyOnValueChangeKey ) . isTrue ( ) ;
// Rule nodes upgrade
private static Stream < Arguments > givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig ( ) {
return Stream . of (
// default config for version 0
Arguments . of ( 0 ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":\"false\",\"sendAttributesUpdatedNotification\":\"false\"}" ,
true ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":false,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":false}" ) ,
// default config for version 1 with upgrade from version 0
Arguments . of ( 0 ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":false,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ,
false ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":false,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ) ,
// all flags are booleans
Arguments . of ( 1 ,
"{\"scope\":\"SHARED_SCOPE\",\"notifyDevice\":true,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ,
false ,
"{\"scope\":\"SHARED_SCOPE\",\"notifyDevice\":true,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ) ,
// no boolean flags set
Arguments . of ( 1 ,
"{\"scope\":\"CLIENT_SCOPE\"}" ,
true ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":true,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ) ,
// all flags are boolean strings
Arguments . of ( 1 ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":\"false\",\"sendAttributesUpdatedNotification\":\"false\",\"updateAttributesOnlyOnValueChange\":\"true\"}" ,
true ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":false,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ) ,
// at least one flag is boolean string
Arguments . of ( 1 ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":\"false\",\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ,
true ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":false,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ) ,
// notify device flag is null
Arguments . of ( 1 ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":\"null\",\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" ,
true ,
"{\"scope\":\"CLIENT_SCOPE\",\"notifyDevice\":true,\"sendAttributesUpdatedNotification\":false,\"updateAttributesOnlyOnValueChange\":true}" )
) ;
}
@ParameterizedTest
@MethodSource
void givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig ( int givenVersion , String givenConfigStr , boolean hasChanges , String expectedConfigStr ) throws TbNodeException {
// GIVEN
willCallRealMethod ( ) . given ( node ) . upgrade ( anyInt ( ) , any ( ) ) ;
JsonNode givenConfig = JacksonUtil . toJsonNode ( givenConfigStr ) ;
JsonNode expectedConfig = JacksonUtil . toJsonNode ( expectedConfigStr ) ;
TbPair < Boolean , JsonNode > upgradeResult = node . upgrade ( 0 , jsonNode ) ;
// WHEN
TbPair < Boolean , JsonNode > upgradeResult = node . upgrade ( givenVersion , givenConfig ) ;
ObjectNode resultNode = ( ObjectNode ) upgradeResult . getSecond ( ) ;
assertThat ( upgradeResult . getFirst ( ) ) . as ( "upgrade result has changes" ) . isFalse ( ) ;
assertThat ( resultNode . has ( updateAttributesOnlyOnValueChangeKey ) ) . as ( "upgrade result has key " + updateAttributesOnlyOnValueChangeKey ) . isTrue ( ) ;
assertThat ( resultNode . get ( updateAttributesOnlyOnValueChangeKey ) . asBoolean ( ) ) . as ( "upgrade result value [true] for key " + updateAttributesOnlyOnValueChangeKey ) . isTrue ( ) ;
// THEN
assertThat ( upgradeResult . getFirst ( ) ) . isEqualTo ( hasChanges ) ;
ObjectNode upgradedConfig = ( ObjectNode ) upgradeResult . getSecond ( ) ;
assertThat ( upgradedConfig ) . isEqualTo ( expectedConfig ) ;
}
}